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, noreturn) - 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.