Functions are the central abstraction, values that can be passed, returned, stored, composed.
def
def add(a, b):
return a + b
print(add(3, 4))7Default arguments
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
print(greet("world"))
print(greet("world", "Hi"))Hello, world!
Hi, world!Keyword arguments
def f(x, y, z):
return x * 100 + y * 10 + z
print(f(1, 2, 3))
print(f(x=1, z=3, y=2))
print(f(1, z=3, y=2))123
123
123Variadic: *args and **kwargs
def total(*nums):
return sum(nums)
print(total(1, 2, 3))
print(total(*[10, 20, 30]))6
60def opts(**kwargs):
return sorted(kwargs.items())
print(opts(host="api", port=443))[('host', 'api'), ('port', 443)]Keyword-only parameters
A bare * marks every following parameter as keyword-only, positional args never reach them.
def connect(host, *, port=80, secure=False):
return f"{host}:{port} secure={secure}"
print(connect("api"))
print(connect("api", port=443, secure=True))
try:
connect("api", 443) # positional past `*` is rejected
except TypeError:
print("rejected")api:80 secure=False
api:443 secure=True
rejectedArgument unpacking at the call site
def f(a, b, c):
return a + b + c
print(f(*[1, 2, 3]))
print(f(*[1, 2], 3))
print(f(**{"a": 1, "b": 2, "c": 3}))
print(f(1, **{"b": 2, "c": 3}))6
6
6
6lambda
Anonymous function; body is a single expression.
double = lambda x: x * 2
print(double(21))
add = lambda a, b: a + b
print(add(3, 4))
# With defaults
greet = lambda name, msg="Hi": f"{msg}, {name}"
print(greet("world"))42
7
Hi, worldFirst-class functions
Functions are values, store, pass, return.
ops = [abs, len, str]
print([f(-3) for f in ops])[3, 2, '-3']# Functions as dict values; replaces switch/case
handlers = {
"add": lambda a, b: a + b,
"mul": lambda a, b: a * b,
"max": max,
}
print(handlers["add"](3, 4))
print(handlers["mul"](3, 4))
print(handlers["max"](3, 4))7
12
4Higher-order functions
Functions that take or return functions.
def apply(f, x):
return f(x)
print(apply(lambda n: n * n, 5))
print(apply(abs, -10))25
10# Returning a function
def make_adder(n):
return lambda x: x + n
add5 = make_adder(5)
add10 = make_adder(10)
print(add5(3))
print(add10(3))8
13Closures
Functions capture their enclosing scope by reference.
def counter():
count = 0
def step():
nonlocal count
count += 1
return count
return step
tick = counter()
print(tick())
print(tick())
print(tick())1
2
3# Closures over loop variables; captured by reference
def make_adders(n):
return [lambda x, i=i: x + i for i in range(n)]
add0, add1, add2 = make_adders(3)
print(add0(10), add1(10), add2(10))10 11 12Currying
Partial application built from nested lambdas or closures.
add = lambda x: lambda y: x + y
print(add(3)(4))
add3 = add(3)
print(add3(10))
print(add3(100))7
13
103# Curry helper
def curry(f):
return lambda x: lambda y: f(x, y)
cmul = curry(lambda a, b: a * b)
double = cmul(2)
triple = cmul(3)
print(double(7), triple(7))14 21Function composition
def compose(*fns):
def piped(x):
for f in fns:
x = f(x)
return x
return piped
# Reads left-to-right: double, then square
pipeline = compose(lambda n: n * 2, lambda n: n * n)
print(pipeline(3)) # (3 * 2) ** 2
print([pipeline(x) for x in [1, 2, 3]])36
[4, 16, 36]Recursion
def factorial(n):
if n < 2:
return 1
return n * factorial(n - 1)
print(factorial(10))3628800# Mutual recursion
def is_even(n):
return True if n == 0 else is_odd(n - 1)
def is_odd(n):
return False if n == 0 else is_even(n - 1)
print(is_even(10), is_odd(10))True FalseGenerators
yield-bearing functions produce sequences lazily. Pull with next() or iterate with for.
def squares(n):
for i in range(n):
yield i * i
for x in squares(5):
print(x)0
1
4
9
16# Materialize a generator
def naturals(limit):
n = 1
while n <= limit:
yield n
n += 1
print(list(naturals(5)))[1, 2, 3, 4, 5]yield from
Delegate to another generator.
def nums():
yield from range(3)
yield from [10, 20]
print(list(nums()))[0, 1, 2, 10, 20]Generator expressions
Generators inline:
print(sum(x * x for x in range(5)))
print(max(i for i in [3, 1, 4, 1, 5]))30
5Decorators
A decorator wraps another callable. Applies to both functions and classes (see Classes):
def trace(f):
def wrapped(*args):
print(f"calling with {args}")
return f(*args)
return wrapped
@trace
def add(a, b):
return a + b
print(add(3, 4))calling with [3, 4]
7Stacked decorators apply bottom-up:
def double_result(f):
return lambda *a: f(*a) * 2
def add_one(f):
return lambda *a: f(*a) + 1
@double_result
@add_one
def base(x):
return x
# base(5) -> add_one -> 6 -> double_result -> 12
print(base(5))12Parameterised decorators are factories, a function taking decorator args and returning the actual decorator. The wrapped function captures both scopes.
def repeat(n):
def decorator(fn):
def wrapped(x):
for i in range(n):
fn(x)
return wrapped
return decorator
@repeat(3)
def greet(name):
print(f"hi {name}")
greet("world")hi world
hi world
hi world