Cohort 4Register

PART 2 · PYTHON FOUNDATIONS · CHAPTER 6

Functions, scope and closures

FreeLesson 29 of 78

Pipelines are built out of small functions. Know how arguments bind, where names are looked up, and what a closure keeps alive.

ARGUMENTSPYTHON 3.12
def load(path, *columns, sep=",", **options):
    print(path, columns, sep, options)

load("orders.csv")
load("orders.csv", "order_id", "amount", sep="|", skip=1)

def area(*, width, height):        # keyword-only
    return width * height
print(area(width=3, height=4))

def summary(values):
    return min(values), max(values), sum(values) / len(values)
lo, hi, avg = summary([747, 3498, 1048])
print(lo, hi, round(avg, 1))

Output

orders.csv () , {}
orders.csv ('order_id', 'amount') | {'skip': 1}
12
747 3498 1764.3

*columns collects extra positional arguments into a tuple, **options collects extra keywords into a dict. A bare * forces the rest to be passed by name, which stops mixups like area(4, 3).

SCOPE: UNBOUNDLOCALERRORPYTHON 3.12
count = 0
def bump():
    count += 1      # assigning makes it local
bump()

Output

UnboundLocalError: cannot access local variable 'count' where it is not associated with a value

Assigning to a name anywhere inside a function makes it local for the whole function. Python looks names up in order: Local, Enclosing, Global, Built-in (LEGB).

CLOSURE WITH NONLOCALPYTHON 3.12
def make_counter():
    count = 0
    def bump():
        nonlocal count  # closure keeps it alive
        count += 1
        return count
    return bump

c = make_counter()
print(c(), c(), c())

Output

1 2 3
LATE BINDING, AND FUNCTIONS AS VALUESPYTHON 3.12
fns = [lambda: i for i in range(3)]
print([f() for f in fns])               # late binding

fns = [lambda i=i: i for i in range(3)]
print([f() for f in fns])               # bind now

steps = [str.strip, str.lower, lambda s: s.replace(" ", "_")]
value = "  Order Date "
for step in steps:                      # functions are values
    value = step(value)
print(value)

Output

[2, 2, 2]
[0, 1, 2]
order_date
A PURE, TYPED TRANSFORM FUNCTIONPYTHON 3.12
STATUS_MAP = {"delivered": "delivered", "dlvd": "delivered",
              "cancelled": "cancelled", "canceled": "cancelled"}

def normalize_status(raw: str | None) -> str:
    "Map messy source statuses to one vocabulary."
    if not raw:
        return "unknown"
    return STATUS_MAP.get(raw.strip().lower(), "unknown")

assert normalize_status(" DLVD ") == "delivered"
assert normalize_status(None) == "unknown"
print([normalize_status(s) for s in ["Canceled", "shipped", ""]])
print(normalize_status.__doc__)

Output

['cancelled', 'unknown', 'unknown']
Map messy source statuses to one vocabulary.

Practice before you move on

  1. Write a closure make_counter() that returns a function counting how many times it has been called.
  2. Fix the late binding example so that each function keeps its own value.