Skip to content
Lesson 6 of 9

Step 1 of 7 · Reading · ~3 min

Learn

Decorators

Function Decorators

You will write decorators that wrap, register, count and cache functions, and predict their order. A decorator is the closure from Higher-Order Functions whose captured variable is a function.

What @ does, and when

@d above a def means f = d(f), run once when the def executes, not per call. Returning the function unchanged makes a registration hook:

python

@on("save") calls on("save") first, then applies the result. Without parentheses, @on calls on(backup) and backup becomes the inner decorator.

The wrapper

To change behaviour, return a new function that accepts any call and returns what the original returned:

python

Two bugs: no return result, so the caller gets None, and area is now wrapper. @functools.wraps(func) on the wrapper copies __name__, __doc__ and __dict__ from func and sets __wrapped__ to the original.

Stacking and arguments

They apply bottom-up and run top-down:

python

Swap the two lines and the output is <i><b>hi</b></i>.

State, and the wraps trap

A counter can live on the wrapper, until another decorator is stacked above it:

python

wraps copied calls = 0 onto the outer wrapper when @loud ran, and that int never updates. Keep counts where every layer shares them, such as a module-level dict. (nonlocal suits state private to the wrapper.)

functools.lru_cache caches by argument; maxsize=None is unbounded and cache_info() reports what happened:

python

A hand-written cache is a dict keyed by the arguments; where it lives matters. At module level, two cached functions share it: double(5) then triple(5) both give 10. Created inside the wrapper, it is empty on every call.

@contextmanager

It turns a generator into a with block: code before yield is __enter__, code after is __exit__.

python

Without try/finally, an exception in the block skips the closing line. On a method, self is just args[0]; put @property and @classmethod above your decorator.

Common mistakes

  • No return in the wrapper: the caller gets None.
  • Missing @wraps: func.__name__ through a layer is 'wrapper', so a lookup by name finds nothing.
  • Counter and cache in the wrong order: @counted above @memoize counts hits too; below it, only misses.
  • One cache for several functions: a call returns another function's answer.

Your exercise

Build a command registry: @command registers, @counted counts, @memoize caches, and fib stacks all three, so stats shows how often its body really ran. The dispatcher is given; the decorators are yours. The most likely mistake is a counter that survives only some layers.

Up nextRegular ExpressionsReal-World Python

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…