Python 3

home

Higher-Order Functions and Decorators

Object References

Everything is an Object; Objects Assigned by Reference


object: a data value of a particular type variable: a name bound to an object


When we create a new object and assign it to a name, we call it a variable. This simply means that the object can now be referred to by that name.

a = 5                 # bind an int object to name a

b = 'hello'           # bind a str object to name b

Here are three classic examples demonstrating that objects are bound by reference and that assignment from one variable to another is simply binding a 2nd name to the same object (i.e. it simply points to the same object -- no copy is made).


Aliasing (not copying) one object to another name:

a = ['a', 'b', 'c']   # bind a list object to a (by reference)

b = a                 # 'alias' the object to b as well -- 2 references

b.append('d')

print(a)               # ['a', 'b', 'c', 'd']

a was not manipulated directly, but it changed. This underscores that a and b are pointing to the same object.


Passing an object by reference to a function:

def modifylist(this_list):   # The object bound to a
    this_list.append('d')    # Modify the object bound to a

a = ['a', 'b', 'c']

modifylist(a)                # Pass object bound to a by reference

print(a)                       # ['a', 'b', 'c', 'd']

The same dynamics at work: a is pointing at the same list object as this_list, so a change through one name is a change to the one object -- and the other name pointing to the same object will see the same changed object.


Alias an object as a container item:

a = ['a', 'b', 'c']          # list bound to a

xx = [1, 2, a]               # 3rd item is reference to list bound to a

xx[2].append('d')            # appending to list object referred to in list item

print(a)                       # ['a', 'b', 'c', 'd']

The same dynamic applied to a container item: the only difference here is that a name and a container item are pointing to the same object.





Function References: Renaming Functions

Functions are variables (objects bound to names) like any other; thus they can be renamed.

def doubleit(val):
    val2 = val * 2
    return val2

print(doubleit)         # <function doubleit at 0x105554d90>

newfunc = doubleit

xx = newfunc(5)        # 10

The output <function doubleit at 0x105554d90> is Python's way of visualizing a function (i.e. showing its printed value). The hex code refers to the function object's location in memory (this can be used for debugging to identify the individual function).





Function References: Functions in Containers

Functions are "first-class" objects, and as such can be stored in containers, or passed to other functions.


Functions can be stored in containers the same way any other object (int, float, list, dict) can be:

def doubleit(val):
    val2 = val * 2
    return val2

def tripleit(val):
    return val * 3

funclist = [ doubleit, tripleit ]

print(funclist[0](10))   # 20
print(funclist[1](10))   # 30




Higher-Order Built-in Functions: map(), grep() and sorted()

These functions allow us to pass a function as an argument to enable its behavior.


We can pass a function name (or a lambda, which is also a reference to a function) to any of these built-in functions. The function controls the built-in function's behavior.


One example is the function passed to sorted():

def by_last(name):
    first, last = name.split()
    return last

names = ['Joe Wilson', 'Zeb Applebee', 'Abe Zimmer']

snames = sorted(names, key=by_last)   # ['Zeb Applebee, 'Joe Wilson', 'Abe Zimmer']

In this example, we are passing the function by_last to sorted(). sorted() calls the function once with each item in the list as argument, and sorts that item by the return value from the function.


In the case of map() (apply a function to each item and return the result) and filter() (apply a function to each item and include it in the returned list if the function returns True), the function is required:

def make_intval(val):
    return int(val)

def over9(val):
    if int(val) > 99:
        return True
    else:
        return False

x = ['1', '100', '11', '10', '110']

# apply make_intval() to each item and sort by the return value
sx = sorted(x, key=make_intval)     # ['1', '10', '11', '100', '110']

# apply make_intval() to each item and store the return value in the resulting list
mx = map(make_intval, x)
print(list(mx))                      # [ 1, 100, 11, 10, 110 ]

# apply over9() to each item and if the return value is True, store in resulting list
fx = filter(over9, x)
print(list(fx))                      # [ '100', '110' ]




Higher-Order Functions: Functions as Arguments and as Return Values

A "higher order" function is one that can be passed to another function, or returned from another function.


The charge() function takes a function as an argument, and uses it to calculate its return value:

def charge(func, val):
    newval = func(val) + val
    return '${}'.format(newval)

def tax_ny(val):
    val2 = val * 0.085
    return val2

def tax_ca(val):
    val2 = val * 0.065
    return val2

nyval = charge(tax_ny, 10)          # pass tax_ny to charge():  $10.85
caval = charge(tax_ca, 10)          # pass tax_ca to charge():  $10.65

Any function that takes a function as an argument or returns one as a return value is called a "higher order" function.





Using a function to build another function

A function can be kind of a "factory" of functions.


This function returns a function as return value, after "seeding" it with a value:

def makemul(mul):
    def times(startval):
        return mul * startval
    return times

doubler = makemul(2)
tripler = makemul(3)

print(doubler(5))      # 10
print(tripler(5))      # 15

In the two examples above, the values 2 and 3 are made part of the returning function -- "seeded" as built-in values .





Decorators

A decorator accepts a function as an argument and returns a replacement function as a return value.


Python decorators return a function to replace the function being decorated -- when the original function is called in the program, Python calls the replacement. A decorator can be added to any function through the use of the @ sign and the decorator name on the line above the function.


Here's a simple example of a function that returns another function (from RealPython blog):

def my_decorator(func):

    def wrapper():
        print("Something is happening before func() is called.")

        func()

        print("Something is happening after func() is called.")
    return wrapper


def whoop():
    print("Wheee!")


# now the same name points to a replacement function
whoop = my_decorator(whoop)

# calling the replacement function
whoop()

This is not a decorator yet, but it shows the concept and mechanics


If we wished to use this as a Python decorator, we can simply use the @ decorator notation:

def my_decorator(func):

    def wrapper():
        print("Something is happening before func() is called.")

        func()

        print("Something is happening after func() is called.")
    return wrapper

@my_decorator
def whoop():
    print('Wheee!')

whoop()
                       # Something is happening before...
                       # Whee!
                       # Something is happening after...

The benefit here is that, rather than requiring the user to explicitly pass a function to a processing function, we can simply decorate each function to be processed and it will behave as advertised.





*args and **kwargs to capture all arguments

To allow a decorated function to accept arguments, we must accept them and pass them to the decorated function.


Here's a decorator function that adds integer 1 to the return value of a decorated function:

def addone(oldfunc):
    def newfunc(*args, **kwargs):
        retvals = oldfunc(*args, **kwargs)
        retvals = retvals + 1
        return retvals
    return newfunc

@addone
def sumtwo(val1, val2):
    return val1 + val2

y = sumtwo(5, 10)
print(y)                  # 16

Now look closely at def newfunc(*args, **kwargs): *args in a function definition means that all positional arguments passed to it will be collected into a tuple called args. **kwargs in a function definition means that all keyword arguments passed to it will be collected into a dictionary called kwargs. (The * and ** are not part of the variable names; they are notations that allow the arguments to be passed into the tuple and dict.) Then on the next line, look at return oldfunc(*args **kwargs) + 1 *args in a function call means that the tuple args will be passed as positional arguments (i.e., the reverse of what happened above) **kwargs in a function call means that the dict kwargs will be passed as keyword arguments (i.e., the reverse of what happened above) This means that if we wanted to decorate a function that takes different arguments, *args and **kwargs would faithfully pass on those arguments as well.


Here's another example, adapted from the Jeff Knupp blog:

def currency(f):                              # decorator function
    def wrapper(*args, **kwargs):
        retvals = f(*args, **kwargs)
        return '$' + str(retvals)
    return wrapper

@currency
def price_with_tax(price, tax_rate_percentage):
    """Return the price with *tax_rate_percentage* applied.
    *tax_rate_percentage* is the tax rate expressed as a float, like
    "7.0" for a 7% tax rate."""

    return price * (1 + (tax_rate_percentage * .01))

print(price_with_tax(50, .10))           # $50.05

In this example, *args and **kwargs represent "unlimited positional arguments" and "unlimited keyword arguments". This is done to allow the flexibility to decorate any function (as it will match any function argument signature).





[pr]