Functions are objects: lambda and closures

Part of the free LogicWiz Python course, Module 11: Functions in depth. The examples and practice run in the app.

Meher wants every counter to have its own token machine: A1, A2, A3 at one counter and B1, B2 at the other, without any global variables. The trick is a function that remembers.

You'll learn to

  • Store, pass and return functions like any other value
  • Write short functions with `lambda` and use them as `key=`
  • Build closures that remember values, and change them with `nonlocal`
  • Explain and fix the late-binding trap in loops

Learn

A function is just an object

def greet(...) creates a function object and points the name greet at it, the same way x = 5 points x at 5.

So a function can be:

  • given another name: say = greet
  • put in a list or dict
  • passed to another function
  • returned from a function

The brackets make the difference: greet is the function, greet("Meher") is a call that gives its result.

Names pointing at a function

def greet(name):
    return "Namaste, " + name

say = greet              # no brackets: no call
print(say("Meher"))
print(say.__name__)
tools = [len, str.upper, greet]
print(tools[2]("Kabir"))

Passing a function in

def apply_to_all(fn, values):
    result = []
    for v in values:
        result.append(fn(v))
    return result

def add_gst(price):
    return round(price * 1.05, 2)

print(apply_to_all(add_gst, [20, 40, 100]))
print(apply_to_all(str, [1, 2, 3]))

discounts = {"student": lambda p: p * 0.9, "staff": lambda p: p * 0.8}
print(discounts["staff"](50))

A dict of functions replaces a long if/elif chain. Interviewers call this a dispatch table.

lambda: a function in one expression

lambda x: x * 2 is a function with no name. It takes x and returns x * 2.

Rules:

  • only one expression, no statements (no if: blocks, no loops, no return)
  • you can use a conditional expression: lambda m: 'pass' if m >= 33 else 'fail'

Its best use is a short throwaway function passed as key= to sorted, min or max. If you're giving a lambda a name, write a def instead.

lambda as a key

menu = {"samosa": 15, "chai": 10, "vada pav": 20}

by_price = sorted(menu.items(), key=lambda kv: kv[1])
print(by_price)
print(max(menu, key=lambda name: menu[name]))

names = ["Sneha", "Aarav", "Kabir", "Riya"]
print(sorted(names, key=lambda n: (len(n), n)))

Closures remember

Define a function inside another and return it. The inner function keeps access to the outer function's variables, even after the outer one has finished. That inner function plus its remembered variables is a closure.

Reading a remembered variable is free. To change it, say nonlocal count first, or Python thinks count += 1 creates a new local variable and raises UnboundLocalError.

A token machine per counter

def make_counter(prefix):
    count = 0
    def next_token():
        nonlocal count
        count += 1
        return prefix + str(count)
    return next_token

counter_a = make_counter("A")
counter_b = make_counter("B")
print(counter_a(), counter_a())
print(counter_b())
print(counter_a())

Each call to make_counter makes a new count. The two machines never share.

The late-binding trap

fns = []
for i in range(3):
    fns.append(lambda: i)
print([f() for f in fns])      # surprise!

fixed = []
for i in range(3):
    fixed.append(lambda i=i: i)   # default grabs the value now
print([f() for f in fixed])

A closure remembers the variable, not its value at that moment. It looks the value up when it is called. By then the loop has finished and i is 2, so all three lambdas return 2. A default argument (i=i) is evaluated at def time, which freezes the current value.

In one line

Functions are objects you can pass and return; a closure remembers outer variables (change them with nonlocal) and looks them up at call time.

Interview corner

What is a closure in Python?

A closure is an inner function that remembers variables from the enclosing function even after that function has returned. Example: make_adder(5) returns add, which still knows n = 5. Python stores these in add.__closure__. Closures are how decorators and function factories keep state without globals.

What can a lambda not do that a def can?

A lambda holds one expression only: no statements, no loops, no assignments (except :=), no docstring, and it has no real name (__name__ is "<lambda>"), which makes tracebacks less clear. Use it for short key= functions; use def for anything you name or reuse.

Explain the late-binding closure trap.

[lambda: i for i in range(3)] gives three functions that all return 2, because closures look up i when called, and the loop has left it at 2. Fix by binding the value at creation: lambda i=i: i, or with functools.partial.

What does "functions are first-class objects" mean?

Functions can be assigned to names, stored in data structures, passed as arguments and returned from other functions, just like ints or lists. That is what makes sorted(key=...), callbacks, dispatch tables and decorators possible.