PythonStory_Operations.html
copyright © James Fawcett
Revised: 04/26/2026
4.0 Prologue
This chapter covers the ways Python transforms data: function definition and calling
conventions, anonymous functions, decorators for cross-cutting behavior, comprehensions
for concise collection building, and generators for lazy evaluation.
4.1 Functions
Functions are first-class objects: they can be stored in variables, passed as arguments,
and returned from other functions. Define them with def:
def power(base: float, exp: int = 2) -> float:
return base ** exp
power(3) # 9.0
power(2, 10) # 1024.0
power(base=5) # 25.0 — keyword argument
Positional-only parameters (before /) and
keyword-only parameters (after *) give fine control over
the calling convention:
def strict(pos_only, /, normal, *, kwd_only):
...
4.2 Variadic Arguments
*args collects extra positional arguments into a tuple;
**kwargs collects extra keyword arguments into a dict:
def report(title: str, *items: str, sep: str = ", ") -> str:
return title + ": " + sep.join(items)
report("Colors", "red", "green", "blue")
# Colors: red, green, blue
The splat operators unpack sequences and mappings at call sites:
func(*lst, **dct)
4.3 Lambda Expressions
A lambda is a single-expression anonymous function. Use them for
short callbacks where a full def would be verbose:
square = lambda x: x * x
pairs = [(1, 'b'), (3, 'a'), (2, 'c')]
pairs.sort(key=lambda p: p[0]) # sort by first element
Lambdas are limited to a single expression; prefer named functions for anything
more complex.
4.4 Closures
A function defined inside another function closes over the enclosing
scope's variables, capturing them by reference:
def make_adder(n: int):
def adder(x: int) -> int:
return x + n # n is captured from make_adder's scope
return adder
add5 = make_adder(5)
print(add5(3)) # 8
Use nonlocal to rebind an enclosing variable from inside the inner function.
4.5 Decorators
A decorator is a callable that wraps a function (or class) to add
behavior. The @name syntax is shorthand for
func = name(func):
import time, functools
def timed(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} took {elapsed:.4f}s")
return result
return wrapper
@timed
def slow_sum(n: int) -> int:
return sum(range(n))
slow_sum(1_000_000)
Parameterized decorators add an extra layer: the outermost callable
receives the parameters and returns the actual decorator.
Standard library decorators:
- @functools.lru_cache(maxsize=128) — memoization
- @functools.cached_property — lazy attribute computed once
- @staticmethod, @classmethod, @property
- @dataclasses.dataclass — auto-generate class boilerplate
4.6 Comprehensions
Comprehensions build collections concisely and are generally faster than equivalent
for loops:
# list comprehension
evens = [x for x in range(20) if x % 2 == 0]
# dict comprehension
word_len = {w: len(w) for w in ["apple", "fig", "banana"]}
# set comprehension
unique_lens = {len(w) for w in ["hi", "hello", "hey"]}
# nested comprehension — flatten a matrix
matrix = [[1, 2], [3, 4], [5, 6]]
flat = [n for row in matrix for n in row] # [1, 2, 3, 4, 5, 6]
4.7 Generators
A generator function uses yield to produce values one
at a time, suspending between yields. This enables memory-efficient pipelines over
large or infinite sequences:
def fibonacci():
a, b = 0, 1
while True:
yield a
a, b = b, a + b
fib = fibonacci()
print([next(fib) for _ in range(8)]) # [0, 1, 1, 2, 3, 5, 8, 13]
Generator expressions use () syntax and are consumed once:
total = sum(x*x for x in range(10**6))
yield from delegates to a sub-generator, enabling generator composition
without manual forwarding loops.
4.8 Epilogue
This chapter covered Python's function model: calling conventions, closures, decorators,
comprehensions, and generators. The next chapter applies these ideas inside classes.
4.9 References
Lambda expressions — python.org
Generators — python.org
PEP 289 — Generator Expressions
Decorators — Real Python