Python Advanced: Decorators, Generators and Typing
Closures and Decorators
In this lesson you will
- Pass functions around and return them from other functions
- Explain what a closure is and use nonlocal
- Write decorators, including ones with arguments, using functools.wraps
Introduction
In Python, a function is just another value, like a number or a string. You can store it in a variable, put it in a list, pass it to another function, or return it. This one idea unlocks closures and decorators, two of the most powerful tools in the language.
Functions as values
A function that takes or returns another function is called a higher-order function. sorted(words, key=len) and map(str.upper, words) are examples you already know.
Functions that make functions
A function can build and return a new function.
Notice that multiply still knows factor even after make_multiplier has finished. This is a closure: an inner function that remembers variables from the scope where it was created.
Quick quiz
What is a closure?
Closures with state: nonlocal
An inner function can read outer variables. To change one, declare it nonlocal.
Each counter has its own private count, with no global variable needed.
Decorators
Suppose you want to time several functions, or log each call. Rather than editing each one, write a decorator: a function that takes a function and returns an improved version of it.
The @ syntax is a neat shortcut for exactly that line:
*args, **kwargs lets the wrapper accept any arguments and pass them straight on, so the decorator works for any function.
A useful decorator: logging calls
Keep the original name: functools.wraps
After decoration, greet.__name__ would be "wrapper", which confuses debugging tools. functools.wraps copies the original's name and docstring onto the wrapper. Use it in every decorator.
Quick quiz
What does the line @logged above def add() mean?
Decorators with arguments
To configure a decorator, add one more layer: a function that takes the settings and returns the real decorator.
Built-in decorators worth knowing
functools.lru_cache remembers results, so repeated calls with the same arguments are instant. Try it on a slow recursive function:
Without the cache, fib(80) would take longer than a human lifetime, since it repeats the same work billions of times. Others you have seen or will meet: @property, @staticmethod, @classmethod and @dataclass.
Try it yourself
Write a decorator timed that measures how long a function takes using time.perf_counter() and prints it, such as slow took 0.02 seconds. Apply it to a function that does some work. (The exact time will differ each run.)
Summary
- Functions are values: store them, pass them in, and return them.
- A closure is an inner function that remembers its outer variables, and
nonlocallets it change them. - A decorator takes a function and returns a replacement.
@decoratoris shorthand forfunc = decorator(func). - Use
*args, **kwargsto wrap any function, andfunctools.wrapsto keep its name and docstring.
Lesson complete
+20 XP
Nice work. Keep going while it is fresh.
Sources and further reading