Skip to content
On this page

Decorators

Since functions are also objects and function objects can be assigned to variables, functions can be called through variables as well.

python
>>> def now():
...     print('2024-6-1')
...
>>> f = now
>>> f()
2024-6-1

Function objects have a __name__ attribute (note: it has two underscores before and after), which can be used to get the name of the function:

python
>>> now.__name__
'now'
>>> f.__name__
'now'

Now, suppose we want to enhance the functionality of the now() function, for example, automatically printing a log before and after the function call, but we don't want to modify the definition of the now() function. This way of dynamically adding functionality during the runtime of the code is called a "Decorator."

Essentially, a decorator is a higher-order function that returns a function. So, to define a decorator that can print logs, we can define it as follows:

python
def log(func):
    def wrapper(*args, **kw):
        print('call %s():' % func.__name__)
        return func(*args, **kw)
    return wrapper

Observe the log above. Because it is a decorator, it accepts a function as a parameter and returns a function. We use Python's @ syntax to place the decorator above the function definition:

python
@log
def now():
    print('2024-6-1')

Calling the now() function will not only execute the now() function itself but also print a log line before running the now() function:

python
>>> now()
call now():
2024-6-1

Placing @log above the now() function definition is equivalent to executing the statement:

python
now = log(now)

Since log() is a decorator that returns a function, the original now() function still exists, but the now variable points to the new function. Therefore, calling now() will execute the new function, which is the wrapper() function returned by the log() function.

The wrapper() function is defined with (*args, **kw) as its parameters, so it can accept calls with any arguments. Inside the wrapper() function, it first prints the log and then immediately calls the original function.

If the decorator itself needs to accept parameters, you need to write a higher-order function that returns a decorator, which becomes more complex. For example, to customize the log text:

python
def log(text):
    def decorator(func):
        def wrapper(*args, **kw):
            print('%s %s():' % (text, func.__name__))
            return func(*args, **kw)
        return wrapper
    return decorator

The usage of this three-layer nested decorator is as follows:

python
@log('execute')
def now():
    print('2024-6-1')

The execution result is as follows:

python
>>> now()
execute now():
2024-6-1

Compared to a two-layer nested decorator, the effect of a three-layer nested decorator is as follows:

python
>>> now = log('execute')(now)

Let's analyze the statement above. First, log('execute') is executed, which returns the decorator function. Then, the returned function is called with the now function as its parameter, and the final return value is the wrapper function.

Both definitions of decorators above are correct, but there is one final step missing. Because we mentioned that functions are also objects with attributes like __name__, but if you look at the functions after being decorated, their __name__ has changed from the original 'now' to 'wrapper':

python
>>> now.__name__
'wrapper'

Because the returned wrapper() function is named 'wrapper', we need to copy the original function's __name__ and other attributes to the wrapper() function. Otherwise, some code that depends on the function's signature may fail.

There's no need to write wrapper.__name__ = func.__name__ manually; Python's built-in functools.wraps does this. Therefore, a complete decorator is written as follows:

python
import functools

def log(func):
    @functools.wraps(func)
    def wrapper(*args, **kw):
        print('call %s():' % func.__name__)
        return func(*args, **kw)
    return wrapper

Or for a decorator that accepts parameters:

python
import functools

def log(text):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kw):
            print('%s %s():' % (text, func.__name__))
            return func(*args, **kw)
        return wrapper
    return decorator

import functools imports the functools module. The concept of modules will be explained later. For now, just remember to add @functools.wraps(func) before defining the wrapper() function.

Exercise

Please design a decorator that can be applied to any function and prints the execution time of that function:

python
import time, functools

def metric(fn):
    print('%s executed in %s ms' % (fn.__name__, 10.24))
    return fn

# Test
@metric
def fast(x, y):
    time.sleep(0.0012)
    return x + y;

@metric
def slow(x, y, z):
    time.sleep(0.1234)
    return x * y * z;

f = fast(11, 22)
s = slow(11, 22, 33)
if f != 33:
    print('Test failed!')
elif s != 7986:
    print('Test failed!')

Please write a decorator that prints 'begin call' and 'end call' logs before and after the function call.

Think again about whether you can write a @log decorator that supports both:

python
@log
def f():
    pass

and:

python
@log('execute')
def f():
    pass

Summary

In Object-Oriented Programming (OOP) design patterns, a decorator is known as the Decorator Pattern. The OOP Decorator Pattern requires implementation through inheritance and composition, whereas Python not only supports OOP decorators but also directly supports decorators at the syntax level. Python's decorators can be implemented using functions or classes.

Decorators can enhance the functionality of functions. Although defining them is somewhat complex, they are very flexible and convenient to use.

Decorators has loaded