Guide Book · Chapter 10
Functions & scope
Defining functions, every kind of parameter, return values, scope rules, closures, lambdas and decorators.
Defining and calling
python
def greet(name, greeting="Hello"):
"""Return a greeting."""
return f"{greeting}, {name}!"
print(greet("Ada"), greet("Bo", greeting="Hi"), greet(greeting="Yo", name="Cy"))
print(greet) # functions are objects tooA function without return returns None.
Every kind of parameter
python
def f(pos_only, /, normal, *args, kw_only, **kwargs):
print(pos_only, normal, args, kw_only, kwargs)
f(1, 2, 3, 4, kw_only=5, extra=6)
f(1, normal=2, kw_only=3)| Syntax | Meaning |
|---|---|
a, b | positional or keyword |
a=1 | default value |
/ | parameters before it are positional-only |
*args | extra positionals collected into a tuple |
* (bare) | parameters after it are keyword-only |
**kwargs | extra keywords collected into a dict |
python
def total(*nums, round_to=2):
return round(sum(nums), round_to)
values = [1.234, 2.345]
print(total(*values), total(1, 2, 3, round_to=0)) # * spreads a list into arguments
opts = {"sep": " | ", "end": "!\n"}
print("a", "b", **opts) # ** spreads a dict into keywordspython
def add(x, items=[]):
items.append(x)
return items
print(add(1), add(2)) # [1, 2] [1, 2]: the same list!
print(add.__defaults__)
def add_fixed(x, items=None):
items = [] if items is None else items
items.append(x)
return items
print(add_fixed(1), add_fixed(2))Scope: LEGB
A name is looked up in Local → Enclosing → Global → Built-in scope, in that order.
python
x = "global"
def outer():
x = "enclosing"
def inner():
return x # finds the enclosing x
return inner()
print(outer(), x, len) # len comes from builtinspython
count = 0
def broken():
try:
count += 1 # assignment makes count local → can't read it first
except UnboundLocalError as e:
print("UnboundLocalError:", e)
def works():
global count
count += 1
broken(); works()
print(count)Closures
An inner function remembers variables from the function that created it, even after that function has returned.
python
def make_counter():
n = 0
def step():
nonlocal n
n += 1
return n
return step
c = make_counter()
print(c(), c(), c())
print(c.__closure__[0].cell_contents) # the remembered variablepython
fs = [lambda: i for i in range(3)] print([f() for f in fs]) # [2, 2, 2] fs = [lambda i=i: i for i in range(3)] # a default freezes the current value print([f() for f in fs]) # [0, 1, 2]
Lambdas and functions as values
python
people = [("Ada", 36), ("Bo", 25)]
print(sorted(people, key=lambda p: p[1]))
ops = {"+": lambda a, b: a + b, "*": lambda a, b: a * b}
print(ops["*"](6, 7), list(map(str.upper, ["a", "b"])))Decorators
A decorator takes a function and returns a replacement: @deco above def f means f = deco(f).
python
import functools, time
def timed(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
try:
return func(*args, **kwargs)
finally:
print(f"{func.__name__} took {(time.perf_counter() - start) * 1000:.2f} ms")
return wrapper
@timed
def slow_sum(n):
return sum(range(n))
print(slow_sum(1_000_000))python
import sys
def depth(n):
return depth(n + 1) if n < 50 else sys._getframe().f_back.f_code.co_name
print(sys.getrecursionlimit(), depth(0))🧭 Scenario: Caching an expensive function (an API lookup, or a slow recursive computation) is one decorator: @functools.cache. The second call with the same arguments returns instantly from a dict.
python
import functools
@functools.cache
def fib(n):
return n if n < 2 else fib(n - 1) + fib(n - 2)
print(fib(100), fib.cache_info())