Menu

Earn Premium with Referrals

Invite your friends and earn Premium rewards through our referral program.

See how it works and start inviting friends.

Part 2: Functions, Scope & Decorators
PYTHON

Part 2: Functions, Scope & Decorators

Master function parameters, LEGB scoping, closures, decorators, generators, lambda expressions, and related function concepts.

1. Function fundamentals

  • Arguments: positional, keyword, *args, **kwargs, defaults, and only positional/keyword-only (/ and * markers).
  • Mutable default parametre trap: evaluated once at definition — shared across all calls.
def add(x, items=[]):
    items.append(x)
    return items

print(add(1))   # [1]
print(add(2))   # [1, 2]  -- same list!

Fix: def add(x, items=None): items = [] if items is None else items.

  • Call-by-assignment: mutations of mutable args affect the caller; rebinding the name locally does not.
  • return without a value returns None.

2. LEGB scoping

  • LEGB: Local → Enclosing → Global → Built-in. Name lookup climbs outward.
  • global x / nonlocal y — declare binding at those scopes.
  • Assignment anywhere in a function makes the name local — reading a global before assignment raises UnboundLocalError.
x = 10
def f():
    print(x)      # UnboundLocalError!
    x = 5
  • Closures: inner function captures enclosing scope variables by reference (cell), still alive after outer returns.

3. Decorators

  • A decorator is a callable taking a function and returning a (usually wrapped) function.
  • @decorator = func = decorator(func).
  • Preserve metadata with functools.wraps.
  • Stacking: applied bottom-up, wraps innermost-out.
import functools, time
def timing(fn):
    @functools.wraps(fn)
    def wrap(*args, **kwargs):
        t = time.perf_counter()
        r = fn(*args, **kwargs)
        print(f"{fn.__name__}: {time.perf_counter()-t:.3f}s")
        return r
    return wrap

@timing
def work(): ...
  • Decorators with args need one more nesting level: @limit(3) → returns the decorator.
  • Class-based decorators implement __call__.

4. Generators & yield

  • A function with yield is a generator function; calling it returns a generator object and runs nothing until iterated.
  • Lazy: values produced on demand — memory-efficient for large streams.
  • next(gen) fires until next yield; StopIteration when exhausted.
  • yield from delegates to a sub-generator.
  • A generator with only return (no yield) is a plain function, not a generator.
def gen():
    print("start")
    yield 1
    yield 2

g = gen()            # nothing printed yet
print(next(g))       # "start" then 1

5. Lambda

  • One-expression anonymous function: lambda x: x * 2.
  • Useful for small sorted(...key=), map/filter.
  • Gotcha: a lambda capturing loop variable binds by reference → classic [lambda: i for i in range(3)] returns the final i for all (fix with default arg lambda i=i: i).

6. Interview checkpoint

  • Mutable default / late binding (closures, lambda default).
  • *args/**kwargs — closure of external libs.
  • Decorator ordering + functools.wraps.
  • Generator vs list — when one-shot lazy beats eager (huge data, infinite streams).
  • returnNone fallback.
  • Recursion depth limit (sys.setrecursionlimit) — derived depth depends on each call frame.

My Private Notes

Notes are auto-saved locally to this device.