Iterators, generators, closures and decorators · about 45 min

Functions as objects, lambdas and closures

Why this matters

In Python a function is a value, just like a number or a list. You can store it in a variable, put it in a dict, pass it to another function, and return it from a function. This single fact powers a huge range of techniques: sorted(key=...), callbacks, event handlers, dispatch tables, and the decorators of the next lesson. Closures, functions that remember variables from where they were defined, let you package behavior together with private state without writing a class. Understanding them precisely also explains one of Python's most famous gotchas, late binding in loops.

Functions are objects

def does two things at run time: it creates a function object and binds a name to it.

def greet(name):
    return f"hello, {name}"

print(type(greet))
say = greet                 # another name for the same object
print(say("ada"))
print(greet.__name__, say is greet)

actions = {"greet": greet, "shout": str.upper}
print(actions["shout"]("hi"))
<class 'function'>
hello, ada
greet True
HI

say = greet copies no code; both names refer to one function object (names bind to objects, as always). The function object carries its compiled bytecode (greet.__code__), its name, its defaults (__defaults__) and a reference to the globals of its module. Calling it, greet("ada"), creates a new frame and runs the code.

Higher-order functions

A higher-order function takes a function as an argument or returns one. You have used several:

words = ["banana", "Kiwi", "apple"]
print(sorted(words))                    # by code point: uppercase first
print(sorted(words, key=str.lower))     # pass a function as the key
print(list(map(len, words)))
print(max(words, key=len))
['Kiwi', 'apple', 'banana']
['apple', 'banana', 'Kiwi']
[6, 4, 5]
banana

Note key=str.lower, not key=str.lower(): you hand sorted the function itself, and it calls the function once per element. Writing your own is just as easy:

def apply_twice(f, x):
    return f(f(x))

print(apply_twice(lambda s: s + "!", "hey"))
hey!!

lambda

lambda params: expression creates a small anonymous function whose body is a single expression, returned automatically:

square = lambda x: x * x
print(square(7))
pairs = [(3, "c"), (1, "a"), (2, "b")]
print(sorted(pairs, key=lambda p: p[1]))
49
[(1, 'a'), (2, 'b'), (3, 'c')]

A lambda is exactly a function object, just written inline and named <lambda>. It cannot contain statements (no if blocks, loops or assignments, though a conditional expression a if c else b is fine). Use it for short throwaway functions, mostly as arguments. If you are assigning a lambda to a name, a def is clearer and gives better tracebacks.

Closures

A function defined inside another function can use the outer function's local variables:

def make_greeter(greeting):
    def greet(name):
        return f"{greeting}, {name}!"
    return greet

hello = make_greeter("Hello")
hola = make_greeter("Hola")
print(hello("Ada"), hola("Ada"))
Hello, Ada! Hola, Ada!

Strange, if you think about it: by the time hello("Ada") runs, make_greeter has returned and its frame is gone. Where does greeting come from?

When the compiler sees that the inner function uses a variable of the enclosing function, it stores that variable in a cell: a small object that holds a reference to the value, shared by the outer frame and the inner function. Each call to make_greeter creates a new cell, and the inner function object keeps a reference to it in __closure__:

print(hello.__code__.co_freevars)
print(hello.__closure__[0].cell_contents)
print(hola.__closure__[0].cell_contents)
('greeting',)
Hello
Hola
 make_greeter("Hello") call               hello (function object)
 +----------------------+                 +-----------------------+
 | greeting --> [cell] -+------+          | __code__: greet code  |
 +----------------------+      +--------> | __closure__: (cell,)  |
    (frame discarded)          cell --> "Hello"

A function plus the cells it captured is a closure. Closures give you private, persistent state: nothing outside can reach greeting except through the function.

Late binding: closures capture variables, not values

A closure stores a reference to the variable (the cell), and looks up its value when the inner function runs:

def make():
    x = 1
    def show():
        return x
    x = 2
    return show

print(make()())
2

That explains the classic loop bug:

funcs = [lambda: i for i in range(3)]
print([f() for f in funcs])
[2, 2, 2]

All three lambdas share one variable i, whose final value is 2. Two standard fixes bind the value at creation time:

funcs = [lambda i=i: i for i in range(3)]           # default arg evaluated now
print([f() for f in funcs])

from functools import partial
def show(i):
    return i
funcs = [partial(show, i) for i in range(3)]        # partial stores the value
print([f() for f in funcs])
[0, 1, 2]
[0, 1, 2]

functools.partial(f, a) returns a callable that calls f(a, ...), with the argument values frozen in. It is a handy alternative to small lambdas.

Changing captured variables: nonlocal

Reading an enclosing variable is automatic. Assigning to a name inside a function makes it local to that function (the scope rule from the functions module), so this fails:

def make_counter():
    count = 0
    def inc():
        count += 1
        return count
    return inc

make_counter()()
UnboundLocalError: cannot access local variable 'count' where it is not associated with a value

count += 1 is an assignment, so the compiler treats count as a local of inc, and it is read before being set. Declare it nonlocal to rebind the enclosing variable instead:

def make_counter():
    count = 0
    def inc():
        nonlocal count
        count += 1
        return count
    return inc

c = make_counter()
print(c(), c(), c())
1 2 3

Mutating an object does not need nonlocal (items.append(x) is not an assignment to items), but rebinding a name does. A closure with nonlocal state is a lightweight alternative to a class with one method; when you need several methods or the state grows, write a class.

Common mistakes

Recap

Going further: read the functools docs for partial and reduce, and inspect fn.__code__.co_freevars and fn.__closure__ for your own closures.

Exercises

Exercise 1 · quiz

Late binding in a loop

What does this print?

funcs = []
for i in range(3):
    funcs.append(lambda: i)
print([f() for f in funcs])
Answer
  • [2, 2, 2]

Each lambda refers to the variable i, not to the value it had when the lambda was created. Names in a function body are looked up when the function runs. By then the loop has finished and i is 2, so all three return 2. Fix it by binding the current value at definition time, e.g. lambda i=i: i (default values are evaluated once, when the function is created).

Exercise 2 · write code

Function factories

In solution.py, write:

  1. make_multiplier(k) returning a function that multiplies its argument by k. make_multiplier(3)(5) is 15.
  2. compose(f, g) returning a function h with h(x) == f(g(x)).
  3. apply_n(f, n) returning a function that applies f to its argument n times (with n == 0 it returns the argument unchanged). apply_n(make_multiplier(2), 10)(1) is 1024.

All three return new functions; none of them calls f immediately.

File: solution.py

Starting point

def make_multiplier(k):
    pass


def compose(f, g):
    pass


def apply_n(f, n):
    pass
Hint 1

Define an inner function with def inside the outer one, and return it without calling it (no parentheses).

Hint 2

The inner function can use the outer function's parameters (k, f, g, n): that is a closure.

Hint 3

For apply_n, the inner function loops n times doing x = f(x).

Reference solution
def make_multiplier(k):
    def multiply(x):
        return x * k
    return multiply


def compose(f, g):
    def h(x):
        return f(g(x))
    return h


def apply_n(f, n):
    def repeated(x):
        for _ in range(n):
            x = f(x)
        return x
    return repeated

Exercise 3 · write code

State in a closure with nonlocal

In solution.py, write make_counter(start=0) that returns a pair of functions (increment, get):

The count must live in the closure (a local variable of make_counter), not in a global or a class. Two counters made by separate calls are independent.

inc, get = make_counter(10)
inc()       # 11
inc(5)      # 16
get()       # 16

File: solution.py

Starting point

def make_counter(start=0):
    count = start

    def increment(step=1):
        count = count + step
        return count

    def get():
        return count

    return increment, get
Hint 1

Assigning to count inside increment makes it a new local variable, so count + step fails with UnboundLocalError.

Hint 2

Declare nonlocal count at the top of increment so the assignment rebinds the enclosing variable.

Reference solution
def make_counter(start=0):
    count = start

    def increment(step=1):
        nonlocal count
        count += step
        return count

    def get():
        return count

    return increment, get

Exercise 4 · write code

Lambdas as sort keys

Read lines from standard input. Each line is name,age,city. Print the records sorted by city alphabetically, then by age descending within a city, then by name alphabetically for ties. Print each as city: name (age).

Use sorted with a key=lambda .... (Hint: to sort an integer descending inside an ascending tuple key, negate it.)

For input

ann,31,Boston
bob,25,Austin
cid,31,Boston
dee,40,Boston

the output is

Austin: bob (25)
Boston: dee (40)
Boston: ann (31)
Boston: cid (31)

File: main.py

Starting point

import sys

rows = [line.strip().split(",") for line in sys.stdin if line.strip()]
for name, age, city in sorted(rows):
    print(f"{city}: {name} ({age})")

Example

input

ann,31,Boston
bob,25,Austin
cid,31,Boston
dee,40,Boston

expected output

Austin: bob (25)
Boston: dee (40)
Boston: ann (31)
Boston: cid (31)

Example

input

zed,9,Denver
amy,10,Denver
kai,100,Albany

expected output

Albany: kai (100)
Denver: amy (10)
Denver: zed (9)
Hint 1

Convert age to int, otherwise "9" sorts after "10".

Hint 2

A key function can return a tuple; tuples compare element by element.

Hint 3

key=lambda r: (r[2], -r[1], r[0]) for rows stored as (name, age, city).

Reference solution
import sys

rows = []
for line in sys.stdin:
    line = line.strip()
    if line:
        name, age, city = line.split(",")
        rows.append((name, int(age), city))

for name, age, city in sorted(rows, key=lambda r: (r[2], -r[1], r[0])):
    print(f"{city}: {name} ({age})")