5. Functions: arguments, scope, and closures
5.1 Parameters in full
def connect(host: str, port: int = 443, *, timeout: float = 5.0, verify: bool = True) -> str:
return f"{host}:{port} timeout={timeout} verify={verify}"
print(connect("example.com"))
print(connect("example.com", 8443, timeout=1.5))
connect("example.com", 8443, 1.5)
# TypeError: connect() takes from 1 to 2 positional arguments but 3 were given
Everything after a bare * is keyword-only. Use keyword-only arguments
for flags and options; a call like connect(h, 443, 1.5, False) is
unreadable, and connect(h, timeout=1.5, verify=False) explains itself.
Parameters before a / are positional-only, which lets you rename
them later without breaking callers:
def clamp(x: float, /, lo: float = 0.0, hi: float = 1.0) -> float:
return max(lo, min(hi, x))
Variable arguments:
def log(msg: str, *args: object, **fields: object) -> None:
print(msg % args if args else msg, fields)
log("user %s logged in from %s", "ada", "10.0.0.7", request_id="r-1", ms=12)
# user ada logged in from 10.0.0.7 {'request_id': 'r-1', 'ms': 12}
opts = {"timeout": 2.0, "verify": False}
print(connect("example.com", **opts)) # unpack a dict into keyword arguments
hosts = ["a", "b"]
print(*hosts, sep=", ") # unpack a list into positional arguments
5.2 Functions are objects
def double(x: int) -> int:
return x * 2
ops = {"double": double, "square": lambda x: x * x}
print(ops["square"](7)) # 49
print(sorted(["b10", "a2", "c1"], key=lambda s: int(s[1:]))) # ['c1', 'a2', 'b10']
print(list(map(double, [1, 2, 3]))) # [2, 4, 6]
lambda is a one-expression anonymous function. Use it for short key=
functions; give anything longer a name.
5.3 Scope: LEGB
A name is looked up in four scopes, in order: Local, Enclosing function, Global (module), Built-in.
x = "global"
def outer():
x = "enclosing"
def inner():
print(x) # finds the enclosing x
inner()
outer() # enclosing
Assignment anywhere in a function makes the name local to the whole function, which produces a confusing error:
count = 0
def bump():
count += 1 # assignment makes `count` local; reading it first fails
bump()
# UnboundLocalError: cannot access local variable 'count' where it is not associated with a value
global count or nonlocal count (for an enclosing function) declare
otherwise. Needing them is usually a sign the state belongs in an object or
a return value.
5.4 Closures
An inner function remembers the variables of the scope where it was defined:
def make_counter():
n = 0
def counter() -> int:
nonlocal n
n += 1
return n
return counter
c = make_counter()
print(c(), c(), c()) # 1 2 3
The late-binding trap: closures capture variables, not values.
# WRONG: all three lambdas see the final value of i
fns = [lambda: i for i in range(3)]
print([f() for f in fns]) # [2, 2, 2]
# Right: bind the current value as a default argument
fns = [lambda i=i: i for i in range(3)]
print([f() for f in fns]) # [0, 1, 2]
Closures are the mechanism behind decorators (Ch 14) and callbacks.