Handbooks / Python / Chapter 2
Functions and Closures
7 pages · ~5 min✓ Reviewed
Builds on Types and Data Structures. Next up: OOP in Python.
Functions as Objects: Mental Model
1. Functions Are Objects
defbuilds a function object and binds it to a name- Assign it, or store it in a list or dict
- Pass it as an argument, or return it
python
def f(x): return x * 2 g = f ops = {'dbl': f} del f print(g(2), ops['dbl'](3))
del f removes one name, not the object
output
4 6
2. Higher-Order Functions
| Call | Function argument |
|---|---|
sorted(words, key=len) | len computes the sort key |
map(str.upper, xs) | str.upper applied to each item |
filter(None, xs) | None keeps only truthy items |
def make(): return f | returns a function |
3. Names and Callables
g = f: two names, one objectcallable(obj)is True for functions, classes, objects with__call__
4. Interview Point
- Function as dict value? It is an ordinary object
- What makes it callable? A
__call__method fvsf()?fis the object,f()runs it
Arguments: Positional, Keyword, *args, **kwargs
1. Signature order
- Positionala
- Defaultb=1
- *argstuple
- Keyword-onlyc, d=2
- **kwargsdict
*argscollects extra positional args into a tuple**kwargscollects extra keyword args into a dict
python
def show(a, b=1, *args, c, d=2, **kw): print(a, b, args, c, d, kw) show(1, 2, 3, 4, c=5, x=6)
output
1 2 (3, 4) 5 2 {'x': 6}
2. Keyword-only, positional-only
| Marker | Rule | Example |
|---|---|---|
* or *args | after it, name required | def f(a, *, key); f(1, 2) is a TypeError |
/ | before it, no names | def f(a, b, /, c) |
3. Call site and binding
f(*[1,2], **{'k': 3})expands a sequence and a mapping*in def vs call? def collects, call expands- Why keyword-only? call sites stay readable:
f(x, key=1) - Binding order? positional, then keyword, then defaults
Duplicate value
f(1, a=2) binds a twice: TypeError, multiple values for argument 'a'.
Default Argument Pitfalls
1. Evaluated Once
- Defaults are built once, at
deftime - Stored in
f.__defaults__, reused on every call - Safe: int, str, tuple, None
- Danger: a default that is mutated or reused
2. The Trap and the Fix
add(1)thenadd(2)share one list, giving[1, 2]
python
def add(x, acc=[]): acc.append(x) return acc add(1); add(2) print(add.__defaults__)
the default list itself changed
output
([1, 2],)
- Fix:
acc=None, thenacc = [] if acc is None else acc time.time()ordatetime.now()as a default freezes at def time- Compute it inside the body
3. Interview Point
- Why does the list persist? One list is made at
deftime and every call reuses it - How to inspect a default? Read
f.__defaults__ - When is it intentional? A memo cache, e.g.
def fib(n, memo={})
Scope (LEGB) and Closures
1. LEGB and Rebinding
- Localinside def
- Enclosingouter def
- Globalmodule
- Built-inlen, print
- First match wins; no match raises
NameError - Assigning anywhere in a def makes the name local for the whole function
- Reading it before the assignment raises
UnboundLocalError global x/nonlocal xonly to rebind; reading or mutating needs neither
2. Closures and Late Binding
- Closure = inner function keeping outer variables after the outer returns
- Captured cells show in
f.__closure__
python
def counter(): n = 0 def inc(): nonlocal n n += 1 return n return inc c = counter(); c(); print(c(), c.__closure__[0].cell_contents)
output
2 2
Late-binding trap
Closures capture the variable, not its value. [lambda: i for i in range(3)] all return 2. Fix: lambda i=i: i
3. Interview Point
- What is LEGB? Local, Enclosing, Global, Built-in lookup order
- nonlocal vs global? Rebinds an outer-function name vs a module name
- Loop lambdas all return 2? They share
i; bind it withi=i
Lambdas and Decorators
1. Lambda
lambda args: expr= one expression, anonymous- No statements, no annotations
- Best as a
key=:sorted(users, key=lambda u: u.age)
2. Decorators
@decabovedef fis exactly f = dec(f)- Wrapper takes
*args, **kwargs: fits any signature - Add @functools.wraps(fn) or
__name__and__doc__are lost
python
import functools def log(fn): @functools.wraps(fn) def wrapper(*args, **kwargs): return fn(*args, **kwargs) return wrapper
standard template
3. Decorator with Arguments
- retry(3)returns decorator
- decorator(fn)returns wrapper
- wrapper(*a, **kw)calls fn
4. Interview Point
- Timing decorator?
time.perf_counter()before and afterfn(...), then return the result - Why wraps? Keeps
__name__and__doc__ - @retry(3)? Expands to
f = retry(3)(f) - Stacking? Applied bottom-up; the top decorator is outermost
Three layers
Arguments add one outer function: factory → decorator → wrapper.
Cheat Sheet: Functions, Scope and Decorators
1. Arguments and defaults
python
def f(a, b=1, *args, kw, kw2=2, **kwargs): ... def g(a, /, b, *, c): ...
order: positional, default, *args, keyword-only, **kwargs
/ends positional-only;*or*argsstarts keyword-only- Mutable default like
acc=[]is shared by every call - Fix:
acc=None, then createacc = []inside the body
2. Scope and closures
- Local
- Enclosing
- Global
- Built-in
nonlocalorglobalonly to rebind; reading needs neither- Closure = inner function + captured variables
- Loop capture:
lambda: isees the last i; fix withlambda i=i: i
3. Decorators
python
def deco(fn): @functools.wraps(fn) def wrapper(*a, **kw): return fn(*a, **kw) return wrapper
- Stacked decorators apply bottom-up: the one nearest
defwraps first
Keep the identity
Skip @functools.wraps and the function loses its __name__ and __doc__.
Check yourself
What does the last line print?
[1, 2]- The default list is built once at
deftime, so both calls share it - Fix:
acc=None, thenacc = [] if acc is None else acc
def add(x, acc=[]): acc.append(x) return acc add(1) print(add(2))
What is printed, and how do you get [0, 1, 2]?
[2, 2, 2]- Each lambda looks up
iwhen called, after the loop has ended - Bind it early:
lambda i=i: i
fs = [lambda: i for i in range(3)] print([f() for f in fs])
Why does inc() raise an error, and what fixes it?
UnboundLocalErrorn += 1assigns ton, sonis local for the whole function- Declare
global nto rebind the module-level name
n = 0 def inc(): n += 1 inc()
What does f.__name__ give, and why?
'wrapper'@timedmeansf = timed(f), sofis now the wrapper- Fix: put
@functools.wraps(fn)on the wrapper
def timed(fn): def wrapper(*a, **kw): return fn(*a, **kw) return wrapper @timed def f(): pass