- 5.1 Function Definitions
- 5.2 Default Arguments
- 5.3 Variadic Arguments
- 5.4 Keyword Arguments
- 5.5 Variadic Keyword Arguments
- 5.6 Functions Accepting All Inputs
- 5.7 Positional-Only Arguments
- 5.8 Names, Documentation Strings, and Type Hints
- 5.9 Function Application and Parameter Passing
- 5.10 Return Values
- 5.11 Error Handling
- 5.12 Scoping Rules
- 5.13 Recursion
- 5.14 The lambda Expression
- 5.15 Higher-Order Functions
- 5.16 Argument Passing in Callback Functions
- 5.17 Returning Results from Callbacks
- 5.18 Decorators
- 5.19 Map, Filter, and Reduce
- 5.20 Function Introspection, Attributes, and Signatures
- 5.21 Environment Inspection
- 5.22 Dynamic Code Execution and Creation
- 5.23 Asynchronous Functions and await
- 5.24 Final Words: Thoughts on Functions and Composition
5.18 Decorators
A decorator is a function that creates a wrapper around another function. The primary purpose of this wrapping is to alter or enhance the behavior of the object being wrapped. Syntactically, decorators are denoted using the special @ symbol as follows:
@decorate def func(x): ...
The preceding code is shorthand for the following:
def func(x): ... func = decorate(func)
In the example, a function func() is defined. However, immediately after its definition, the function object itself is passed to the function decorate(), which returns an object that replaces the original func.
As an example of a concrete implementation, here is a decorator @trace that adds debugging messages to a function:
def trace(func): def call(*args, **kwargs): print('Calling', func.__name__) return func(*args, **kwargs) return call # Example use @trace def square(x): return x * x
In this code, trace() creates a wrapper function that writes some debugging output and then calls the original function object. Thus, if you call square(), you will see the output of the print() function in the wrapper.
If only it were so easy! In practice, functions also contain metadata such as the function name, doc string, and type hints. If you put a wrapper around a function, this information gets hidden. When writing a decorator, it’s considered best practice to use the @wraps() decorator as shown in this example:
from functools import wraps def trace(func): @wraps(func) def call(*args, **kwargs): print('Calling', func.__name__) return func(*args, **kwargs) return call
The @wraps() decorator copies various function metadata to the replacement function. In this case, metadata from the given function func() is copied to the returned wrapper function call().
When decorators are applied, they must appear on their own line immediately prior to the function. More than one decorator can be applied. Here’s an example:
@decorator1 @decorator2 def func(x): pass
In this case, the decorators are applied as follows:
def func(x): pass func = decorator1(decorator2(func))
The order in which decorators appear might matter. For example, in class definitions, decorators such as @classmethod and @staticmethod often have to be placed at the outermost level. For example:
class SomeClass(object): @classmethod # Yes @trace def a(cls): pass @trace # No. Fails. @classmethod def b(cls): pass
The reason for this placement restriction has to do with the values returned by @classmethod. Sometimes a decorator returns an object that’s different than a normal function. If the outermost decorator isn’t expecting this, things can break. In this case, @classmethod creates a classmethod descriptor object (see Chapter 7). Unless the @trace decorator was written to account for this, it will fail if decorators are listed in the wrong order.
A decorator can also accept arguments. Suppose you want to change the @trace decorator to allow for a custom message like this:
@trace("You called {func.__name__}") def func(): pass
If arguments are supplied, the semantics of the decoration process is as follows:
def func(): pass # Create the decoration function temp = trace("You called {func.__name__}") # Apply it to func func = temp(func)
In this case, the outermost function that accepts the arguments is responsible for creating a decoration function. That function is then called with the function to be decorated to obtain the final result. Here’s what the decorator implementation might look like:
from functools import wraps def trace(message): def decorate(func): @wraps(func) def wrapper(*args, **kwargs): print(message.format(func=func)) return func(*args, **kwargs) return wrapper return decorate
One interesting feature of this implementation is that the outer function is actually a kind of a “decorator factory.” Suppose you found yourself writing code like this:
@trace('You called {func.__name__}') def func1(): pass @trace('You called {func.__name__}') def func2(): pass
That would quickly get tedious. You could simplify it by calling the outer decorator function once and reusing the result like this:
logged = trace('You called {func.__name__}') @logged def func1(): pass @logged def func2(): pass
Decorators don’t necessarily have to replace the original function. Sometimes a decorator merely performs an action such as registration. For example, if you are building a registry of event handlers, you could define a decorator that works like this:
@eventhandler('BUTTON') def handle_button(msg): ... @eventhandler('RESET') def handle_reset(msg): ...
Here’s a decorator that manages it:
# Event handler decorator _event_handlers = { } def eventhandler(event): def register_function(func): _event_handlers[event] = func return func return register_function