A decorator takes a function and hands back a new one that wraps it, adding behaviour before and after.
# simple decorator
def my_decorator(func):
def wrapper():
print("Before")
func()
print("After")
return wrapper
@my_decorator
def say_hello():
print("Hello!")
say_hello()
# ans: Before
# ans: Hello!
# ans: After
@my_decorator passes say_hello in and stores the returned wrapper under the name say_hello.say_hello() now runs wrapper, which calls the original in the middle.
# decorator for functions with args
def my_decorator(func):
def wrapper(*args, **kwargs):
print("Before")
result = func(*args, **kwargs)
print("After")
return result
return wrapper
@my_decorator
def add(a, b):
return a + b
add(5, 3)
# ans: Before, After, returns 8
*args, **kwargs accept any arguments and pass them straight through; return result hands back the original's value.
# without @ syntax
def greet():
print("Hello")
greet = my_decorator(greet)
# ans: equivalent to @my_decorator
@my_decorator
def greet():
print("Hello")
=
def greet():
print("Hello")
greet = my_decorator(greet)
from functools import wraps
# preserve function info
def my_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@my_decorator
def my_func():
"""Docstring"""
pass
my_func.__name__
# ans: "my_func" (not "wrapper")
@wraps(func) copies the original's name and docstring onto the wrapper.
Predict the answer, then reveal it. Some slides then animate why.
# what is returned?
def decorator(func):
def wrapper():
return func()
return wrapper
@decorator
def func():
return 5
func()
# ans: 5
# decorator execution?
def decorator(func):
print("Decorating")
return func
@decorator
def func():
pass
# ans: "Decorating" (at definition time)
# nested decorators?
def dec1(func):
def wrapper():
print("1")
return func()
return wrapper
def dec2(func):
def wrapper():
print("2")
return func()
return wrapper
@dec1
@dec2
def func():
pass
func()
# ans: 1, 2
# ans: (applied bottom up; dec1's wrapper runs first)
# decorator without wrapper?
def decorator(func):
return func
@decorator
def func():
pass
# ans: valid (identity decorator)
# accessing original?
def decorator(func):
def wrapper():
return func()
wrapper.original = func
return wrapper
@decorator
def func():
pass
func.original
Functions are objects, so the wrapper can carry attributes: func.original is the undecorated function.
# decorator modifying return?
def double(func):
def wrapper(*args):
return func(*args) * 2
return wrapper
@double
def add(a, b):
return a + b
add(2, 3)
# ans: 10
# decorator with closure?
def make_bold(func):
def wrapper():
return f"<b>{func()}</b>"
return wrapper
@make_bold
def get_text():
return "Hello"
get_text()
# ans: "<b>Hello</b>"
# class as decorator?
class Decorator:
def __init__(self, func):
self.func = func
def __call__(self, *args):
return self.func(*args)
@Decorator
def func():
pass
Any callable works: func becomes a Decorator instance, and calling it runs __call__.
# without @wraps?
def decorator(func):
def wrapper():
return func()
return wrapper
@decorator
def func():
pass
func.__name__
# ans: "wrapper"
# decorator returns None?
def decorator(func):
def wrapper():
func()
return wrapper
@decorator
def func():
return 5
func()
# ans: None (wrapper doesn't return)
@d means f = d(f)It runs once, when the function is defined.
Calls go to the wrapper, which decides when (and whether) to call the original.
Use *args, **kwargs and return the result, or the caller gets None.
The decorator nearest the def wraps first; the top one runs first.
@wraps(func)Keeps __name__ and the docstring of the original.