Decorators
Picture a present in wrapping paper. The present inside is the same, but to get to it, you unwrap the paper first. In Python, you can "wrap" a function the same way: every call first goes through the wrapping and only then reaches the function itself.
Why would you need it? Before each call of several functions, the line calling must be printed. You could add a print to the body of each of them, but then the same line repeats in many places. Instead, the function is passed to another function, which returns a new one with the added behavior:
def announce(func):
def wrapper(*args, **kwargs):
print("calling")
return func(*args, **kwargs)
return wrapper
def add(a, b):
return a + b
add = announce(add)
result = add(2, 3) # calling
print(result) # 5Let's walk through the example line by line:
add = announce(add)hands the functionaddtoannounceand getswrapperback. Now the nameaddrefers to it.add(2, 3)callswrapper, which printscalling.wrappercalls the originaladdwith the same arguments and returns its result,5.print(result)prints5.
The wrapper
A function that takes a function and returns a new one is called a decorator. The function it returns is a wrapper: that very wrapping paper.
Here wrapper is a closure: it remembers func, the original add. When called, the wrapper prints calling, then calls the original function and returns its result.
The *args and **kwargs parameters let the wrapper accept any arguments and pass them to the original function unchanged. That is why announce fits a function with any parameters. Before and after the call, the wrapper can do anything:
- print a line;
- count calls;
- change the result.
The line add = announce(add) calls announce once. The body of add does not run at that point: announce receives the function, not the result of calling it. After the assignment, every call of add goes through the wrapper. Only the wrapper itself can reach the original function, through func.
This is where the present picture stops working. Real paper is taken off once, but a wrapper runs on every call. It can also swap the result on the way back—which never happens to a present.
The @ syntax
The line @announce above a def does the same thing more briefly. It equals the assignment greet = announce(greet) right after the def:
@announce
def greet(name):
return f"hi {name}"
message = greet("ann") # calling
print(message) # hi annOne decorator fits many functions: the line @announce goes above each of them, while print is written once, inside wrapper.
The decorator runs once, when Python executes the def, not on every call. announce itself prints nothing at that point. calling appears on each call of greet, because the wrapper prints it.
Interviewers like to ask this: "What is a decorator, and what does @dec stand for?" The answer has two parts:
- a decorator is a function that takes a function and returns another one, usually a wrapper;
@decabovedef fis short forf = dec(f), and from then on the namefrefers to whateverdecreturned.
Common mistake
The wrapper is missing return:
def announce(func):
def wrapper(*args, **kwargs):
print("calling")
func(*args, **kwargs) # ✗
return wrapper
@announce
def add(a, b):
return a + b
print(add(2, 3))The program prints calling and then None instead of 5. The original add returned 5 to the wrapper, but the wrapper did not return it, and a function without return returns None. Write return func(*args, **kwargs).
Key points
- A decorator takes a function and returns a wrapper that adds behavior before or after the call.
@decabovedef fis the same asf = dec(f)right after the definition.- A wrapper with
*args, **kwargsfits a function with any parameters. - The wrapper must return the original function's result, or the call returns
None.
Subscribe to take the course
The course is free. Sign in with Telegram and subscribe to the channel to open every lesson.