FUNCTIONS PART II

Objectives

  • Use the * and ** operator as parameters to a function and outside of a function
  • Leverage dictionary and tuple unpacking to create more flexible functions
  • Understand what a lambda is and how they are used
  • Explain what closure is and how it works in Python
  • Use built in functions to sort, reverse and calculate aggregate information
*args

A special operator we can pass to functions

Gathers remaining arguments as a tuple

This is just a parameter - you can call it whatever you want!

Example

def sum_all_values(*args):
    total = 0
    for val in args:
        total += val

    return total

sum_all_values(1, 2, 3) # 6

sum_all_values(1, 2, 3, 4, 5) # 15

Another Example

def ensure_correct_info(*args):
    if "Colt" in args and "Steele" in args:
        return "Welcome back Colt!"

    return "Not sure who you are..."

ensure_correct_info() # Not sure who you are...

ensure_correct_info(1, True, "Steele", "Colt")

The order does not matter!

**kwargs

A special operator we can pass to functions

Gathers remaining keyword arguments as a dictionary

This is just a parameter - you can call it whatever you want!

Example

def favorite_colors(**kwargs):
    for key, value in kwargs.items():
        print(f"{key}'s favorite color is {value}")

favorite_colors(rusty='green', colt='blue')

# rusty's favorite color is green
# colt's favorite color is blue

Another Example

def special_greeting(**kwargs):
    if "Colt" in kwargs and kwargs["Colt"] == "special":
        return "You get a special greeting Colt!"
    elif "Colt" in kwargs:
        return f"{kwargs["Colt"]} Colt!"

    return "Not sure who this is..."

special_greeting(Colt='Hello') # Hello Colt!
special_greeting(Bob='hello') # Not sure who this is...
special_greeting(Colt='special') # You get a special greeting Colt!

Parameter Ordering

  1. parameters
  2. *args
  3. default parameters
  4. **kwargs

Combined Example

def display_info(a, b, *args, instructor="Colt", **kwargs):
  return [a, b, args, instructor, kwargs]

display_info(1, 2, 3, last_name="Steele", job="Instructor")

[1, 2, (3,), 'Colt', {'job': 'Instructor', 'last_name': 'Steele'}]

What's going on with with that (3,) ?

When you have a tuple with one item - Python needs to distinguish between parenthesis and a tuple!

Using * as an Argument:

Argument Unpacking

def sum_all_values(*args):
    # there's a built in sum function - we'll see more later!
    return sum(args)

sum_all_values([1, 2, 3, 4]) # nope...
sum_all_values((1, 2, 3, 4)) # this does not work either...

sum_all_values(*[1, 2, 3, 4]) # 10
sum_all_values(*(1, 2, 3, 4)) # 10

We can use * as an argument to a function to "unpack" values

Using ** as an Argument:

Dictionary Unpacking

def display_names(first, second):
    return f"{first} says hello to {second}"

names = {"first": "Colt", "second": "Rusty"}

display_names(names) # nope..

display_names(**names) "Colt says hello to Rusty"

We can use ** as an argument to a function to "unpack" dictionary values into keyword arguments

Example with **

as an Argument

def display_names(first, second):
    return f"{first} says hello to {second}"

names = {"first": "Colt", "second": "Rusty"}

display_names(names) # nope..

display_names(**names) "Colt says hello to Rusty"

YOUR TURN

Lambdas

def first_function():
    return 'Hello!'

first_function() # 'Hello!'

first_function.__name__ # first_function'

But lambdas are anonymous functions!

first_lambda = lambda x: x + 5

first_lambda(10) # 15

first_lambda.__name__ # '<lambda>'

Normal functions have names...

Lambda Syntax

add_values = lambda x, y: x + y

multiply_values = lambda x, y: x + y

add_values(10, 20) # 30

multiply_values(10, 20) # 200

lambda parameters : body of function

map
l = [1, 2, 3, 4]

doubles = list(map(lambda x: x * 2, l))

evens # [2, 4, 6, 8]

A standard function that accepts at least two arguments, a function and an "iterable"

iterable - something that can be iterated over (lists, strings, dictionaries, sets, tuples)

runs the lambda for each value in the iterable and returns a map object which can be converted into another data structure

in Action

l = [1,2,3,4]

doubles = list(map(lambda x: x*2, l))

evens # [2,4,6,8]
names = [
    {'first':'Rusty', 'last': 'Steele'}, 
    {'first':'Colt', 'last': 'Steele', }, 
    {'first':'Blue', 'last': 'Steele', }
]

first_names = list(map(lambda x: x['first'], names))

first_names # ['Rusty', 'Colt', 'Blue']
map 
filter
l = [1,2,3,4]

evens = list(filter(lambda x: x % 2 == 0, l))

evens # [2,4]
  • There is a lambda for each value in the iterable.

  • Returns filter object which can be converted into other iterables

  • The object contains only the values that return true to the lambda

Combining filter and map

names = ['Lassie', 'Colt', 'Rusty']

Given this list of names:

list(map(lambda name: f"Your instructor is {name}",
     filter(lambda value: len(value) < 5, names)))

# ['Your instructor is Colt']

Return a new list with the string

"Your instructor is " + each value in the array,

but only if the value is less than 5 characters

What about

List Comprehension?

names = ['Lassie', 'Colt', 'Rusty']

Given this list of names:

[f"Your instructor is {name}" for name in names if len(name) < 5]

Return a new list with the string:

"Your instructor is " + each value in the array,

but only if the value is less than 5 characters

reduce
from functools import reduce

l = [1,2,3,4]

product = reduce(lambda x, y: x * y, l)

l = [1,2,3,4]

total = reduce(lambda x, y: x + y, l, 10)

runs a function of two arguments cumulatively to the items of iterable, from left to right, which reduces the iterable to a single value

You will not be using reduce frequently so it's good to know it exists, but you will not find yourself using it since we have a better option in most cases

reduce or List Comprehension?

from functools import reduce

l = [1,2,3,4]

product = reduce(lambda x, y: x * y, l)

For almost all problems especially at this stage, use list comprehension - you will see it far more in the wild 

YOUR TURN

Closures

Accessing variables defined in outer functions after they have returned!

- private variables

- not using global variables

Example

Let's imagine we want a counter variable and would like to keep track of it

"Public" Counter

count = 0

def counter():
    global count
    count += 1
    return count

This works, but anyone can change count!

"Private" Counter

def counter():
    count = 0
    count += 1
    return count


No one can change count directly, but it keeps getting redefined!

Closures using nonlocal

def counter():
    count = 0
    def inner():
        nonlocal count
        count += 1
        return count
    return inner


Here we're making a variable count inside the counter function, which can only be accessed by counter and inner.

Once we return inner, we can still remember count through closure!

Closures using Objects

def counter():
    counter.count = 0
    def inner():
        counter.count += 1
        return counter.count
    return inner

Here we're making a property on the counter function which can only be accessed by counter and inner.

Once we return inner, we can still remember the count property through closure!

Partial Application with Closures

def outer(a):
    def inner(b):
        return a+b
    return inner

result = outer(10)

result(20) # 30

You will see this pattern again when you learn about decorators!

When you are just using (not modifying) a variable through closure, you don't need to use nonlocal or objects!

YOUR TURN

Built-in

Functions

all
all([0,1,2,3]) # False

all([char for char in 'eio' if char in 'aeiou'])

all([num for num in [4,2,10,6,8] if num % 2 == 0]) # True

Return True if all elements of the iterable are truthy (or if the iterable is empty)

any
any([0, 1, 2, 3]) # True

any([val for val in [1,2,3] if val > 2]) # True

any([val for val in [1,2,3] if val > 5]) # False

Return True if any element of the iterable is truthy. If the iterable is empty, return False

sorted
# sorted (works on anything that is iterable)

more_numbers = [6,1,8,2]
sorted(more_numbers) # [1, 2, 6, 8]
print(more_numbers) # [6, 1, 8, 2]

Returns a new sorted list from the items in iterable

reversed
more_numbers = [6, 1, 8, 2]
reversed(more_numbers) # <list_reverseiterator at 0x1049f7da0>
print(list(reversed(more_numbers))) # [2, 8, 1, 6]

Return a reverse iterator

Use slices or .reverse!

YOUR TURN

max
# max (strings, dicts with same keys)

max([3,4,1,2]) # 4
max((1,2,3,4)) # 4
max('awesome') # 'w'
max({1:'a', 3:'c', 2:'b'}) # 3

Return the largest item in an iterable or the largest of two or more arguments.

min
# min (strings, dicts with same keys)

min([3,4,1,2]) # 1
min((1,2,3,4)) # 1
min('awesome') # 'a'
min({1:'a', 3:'c', 2:'b'}) # 1

Return the smallest item in an iterable or the smallest of two or more arguments.

len 
len('awesome') # 7
len((1,2,3,4)) # 4
len([1,2,3,4]) # 4
len(range(0,10) # 10

len({1,2,3,4}) # 4
len({'a':1, 'b':2, 'c':2} # 3

Return the length (the number of items) of an object. The argument may be a sequence (such as a string, tuple, list, or range) or a collection (such as a dictionary, set)

abs
abs(-5) # 5
abs(5)  # 5

Return the absolute value of a number. The argument may be an integer or a floating point number.

sum
sum([1,2,3,4]) # 10

sum([1,2,3,4], -10) # 0
  • Takes an iterable and an optional start.
  • Returns the sum of start and the items of an iterable from left to right and returns the total.
  • start defaults to 0
round
round(10.2) # 10
round(1.212121, 2) # 1.21

Return number rounded to ndigits precision after the decimal point. If ndigits is omitted or is None, it returns the nearest integer to its input.

zip
first_zip = zip([1,2,3], [4,5,6])

list(first_zip) # [(1, 4), (2, 5), (3, 6)]

dict(first_zip) # {1: 4, 2: 5, 3: 6}
  • Make an iterator that aggregates elements from each of the iterables.

  • Returns an iterator of tuples, where the i-th tuple contains the i-th element from each of the argument sequences or iterables.

  • The iterator stops when the shortest input iterable is exhausted.

zip
five_by_two = [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5)]

list(zip(*five_by_two))

[(0, 1, 2, 3, 4), (1, 2, 3, 4, 5)]

Very common when working with more complex data structures!

YOUR TURN

Recap

  • *args is useful for accepting a variable number of arguments 

  • **kwargs is useful when accepting a variable number of keyword arguments

  • you can use * to unpack argument values

  • you can use ** to unpack dictionary values

  • closures are very useful for private variables 

  • lambdas are annonymous functions that are useful with map, filter and reduce

  • map is useful for transforming lists into different lists of the same size

  • filter is useful for transforming lists into  lists of different sizes

  • Python has quite a few built in functions - make sure to spend the time learning them!

Functions Part II

By colt

Functions Part II

  • 13,171