Lecture 35

Functions and decorators

MCS 275 Spring 2024
Emily Dumas

View as:   Presentation   ·   PDF-exportable  ·   Printable

Lecture 35: Functions and decorators

Reminders and announcements:

Variadic functions

A function is variadic if it can accept a variable number of arguments. This is general CS terminology.

Python supports these. The syntax

def f(a,b,*args):
means that the first argument goes into variable a, the second into variable b, and any other arguments are "packed" into a tuple which is assigned to args.

Variadics and keyword arguments

Similarly, the syntax

def f(a,b,**kwargs):
or
def f(a,b,*args,**kwargs):
packs all extra keyword arguments into a dictionary called kwargs.

It is traditional to use the names args and kwargs, but it is not required.

Argument unpacking

Take arguments from a list or tuple:

L = [6,11,16]
f(1,*L) # calls f(1,6,11,16)

Take keyword arguments from a dict:

d = { "mcs275": "fun", "x": 42 }
f(1,z=0,**d) # calls f(1,z=0,mcs275="fun",x=42)

Think of * as "remove the brackets", and ** as "remove the curly braces".

Functions

Suppose we define a function:


def f(x):
    "Compute the square of `x`"
    return x*x

Then the name f refers to a function object.

You can work with a function object like any other value, e.g. assign other things to it, pass it as an argument, ...

Aside

Like other objects, function objects have attributes (e.g. `__name__`, `__doc__`).

You can think of def as the "function object constructor".

lambda

In Python, you can create a function with no name—an anonymous function—using the syntax:


        lambda x: x*x    # takes x, returns x*x
        lambda x,y: x-y  # takes x and y, returns value x-y
    

lambda then evaluates to a function object, so the expression


        lambda x,y: x-y
    

behaves just like the name


        diff
    

if you previously defined


        def diff(x,y):
            return x-y
    

When to use lambda

Functions definitely deserve names if they are used in several places, or if they are complicated.

But lambda is good for simple functions used once. Then, the definition and the only place of use are not separated.

Common use for lambda

The built-in functions max, min, and list.sort accept a keyword argument key that is a function which is applied to elements before making comparisons.

e.g. if L is a list of words, then max(L,key=len) is the longest word.

Function arguments

Functions in Python can accept functions as arguments.

def dotwice(f):
    """Call function f twice"""
    f()
    f()

A better version works with functions that accept arguments:

def dotwice(f,*args,**kwargs):
    """Call function f twice (allowing arguments)"""
    f(*args,**kwargs)
    f(*args,**kwargs)

Here, *args means any number of positional arguments, and **kwargs means any number of keyword arguments.

Returning functions

Functions in Python can return functions. Often this is used to make "function factories".

def power_function(n):
    def inner(x): # function inside a function!
        """Raise x to a power"""
        return x**n
    return inner

Modifying functions

def return_twice_doer(f):
    """Return a new function which calls f twice"""
    def inner(*args,**kwargs):
        """Call a certain function twice"""
        f(*args,**kwargs)
        f(*args,**kwargs)
    return inner

Replacing functions

In some cases we might want to replace an existing function with a modified version of it (e.g. as returned by some other function).

def g(x):
    """Print the argument with a message"""
    print("Function got value",x) 

# actually, I wanted to always print that message twice!
g = return_twice_doer(g)

Decorator syntax

There is a shorter syntax to replace a function with a modified version.

@modifier
def fn(x,y):
    """Function body goes here"""

is equivalent to

def fn(x,y):
    """Function body goes here"""

fn = modifier(fn)

The symbol @modifier (or any @name) before a function definition is called a decorator.

Returning values

Usually, the inner function of a decorator should return the value of the (last) call to the argument function.

def return_twice_doer(f):
    """Return a new function which calls f twice"""
    def inner(*args,**kwargs):
        """Call a certain function twice"""
        f(*args,**kwargs)
        return f(*args,**kwargs)
    return inner

Decorator arguments

Python allows @name(arg1,arg2,...).

In that case, name should be a decorator factory.

E.g.

@dec(2)
def printsq(x):
    print(x*x)
is equivalent to
thisdec = dec(2)

@thisdec
def printsq(x):
    print(x*x)

Note a decorator factory is a function that returns a function that return a function!

A few built-in decorators

  • @functools.cache — Memoize up to 100 recent calls to a function.*
  • @classmethod — Make a method a class method (callable from the class itself, gets class as first argument). E.g. for alternate constructors.
  • @atexit.register — Make sure this function is called just before the program exits.

Note: functools.cache is new in Python 3.9

Multiple decorators

Allowed. Each must be on its own line.

@dec1
@dec2
@dec3
def f(x):
    """Function body goes here"""

replaces f with dec1(dec2(dec3(f))).

So the decorator closest to the function name acts first.

References

  • See Lutz, Chapter 18 for more about *args and **kwargs.
  • See Lutz, Chapter 39 for a detailed discussion of Python decorators.
  • See Beazley & Jones, Chapter 9 for several examples of decorators.

Acknowledgment

When preparing an earlier version of this lecture, I reviewed course materials by Danko Adrovic and Jan Verschelde (other MCS instructors).

Revision history

  • 2023-04-13 Finalized the 2023 lecture this was based on
  • 2024-04-05 Initial publication