Code Now

Python Basics: Decorators

· LuYanFCP

A decorator is a callable object whose argument is another function (the decorated function). The decorator may process the decorated function and then return it, or replace it with another function or callable object.

1@test
2def target():
3    print("run target function!")

It works the same as the following code

1def target():
2    print("run target function!")
3target = test(target) # In the test function, the function will be enhanced or replaced

Function decorators run at import time, while the decorated function only runs when it is explicitly called.

Python does not require variable declarations, but it assumes that a variable assigned to inside a function body is a local variable. This is far better than the behavior of javascript; javascript also does not require variable declarations, but if you forget to declare a variable as local (using var), you may end up accessing a global variable without realizing it.

An example of a decorator — logging program runs

Goal: every time the program runs, output log, output the program’s runtime, results, and so on, and write log to terminal.

 1import time
 2def log(func):
 3    def ff(*args, **kwargs):
 4        t0 = time.time()
 5        run_time = time.asctime(time.localtime(t0))
 6        res = func(*args, **kwargs)
 7        cost = time.time() - t0
 8        name = func.__name__
 9        args_str = ', '.join(repr(arg) for arg in args)  # Convert repr into a human-readable form
10        print('[{}] {}({})->{} cust_time={} s'.format(run_time, name, args_str, res, cost))
11        return res
12    return f
13
14@log
15def f(n):
16    if n <= 1:
17        return 1
18    return n * f(n-1)
19
20>> f(5)
21[Fri Mar 20 16:04:05 2020] f(1)->1 cust_time=0.0 s
22[Fri Mar 20 16:04:05 2020] f(2)->2 cust_time=0.0 s
23[Fri Mar 20 16:04:05 2020] f(3)->6 cust_time=0.000997304916381836 s
24[Fri Mar 20 16:04:05 2020] f(4)->24 cust_time=0.000997304916381836 s
25[Fri Mar 20 16:04:05 2020] f(5)->120 cust_time=0.000997304916381836 s
26120

The example above implements this functionality, but it isn’t complete yet, because it doesn’t support keyword arguments, and it also masks attributes of the decorated function such as __name__ and __doc__. Bring in functools.wraps to help the decorator.

 1def log(func):
 2    @functools.wraps(func)
 3    def f(*args, **kwargs):
 4        t0 = time.time()
 5        run_time = time.asctime(time.localtime(t0))
 6        res = func(*args, **kwargs)
 7        cost = time.time() - t0
 8        name = func.__name__
 9        args_str = ', '.join(repr(arg) for arg in args)  # Convert repr into a human-readable form
10        print('[{}] {}({})->{} cust_time={} s'.format(run_time, name, args_str, res, cost))
11        return res
12    return f

Parameterized decorators

We’ll often see wrap taking parameters to help out, like

1from functools import lru_cache
2@lru_cache(max_size=16)
3def f(n):
4    if n <= 1:
5        return 1
6    return n * f(n-1)

which is equivalent to

1f = lru_cache(max_size=16)(f)

So if you want to add parameters, you have to add another layer to the wraper function. Still using the example above, in this case you can treat fmt as a parameter:

 1import functools, time
 2fmt='[{}] {}({})->{} cust_time={} s'
 3def log(fmt):
 4    def log_f(func):
 5        @functools.wraps(func)
 6        def f(*args, **kwargs):
 7            t0 = time.time()
 8            run_time = time.asctime(time.localtime(t0))
 9            res = func(*args, **kwargs)
10            cost = time.time() - t0
11            name = func.__name__
12            args_str = ', '.join(repr(arg) for arg in args)  # Convert repr into a human-readable form
13            print(fmt.format(run_time, name, args_str, res, cost))
14            return res
15        return f
16    return log_f

References

《Fluent Python》


View the original issue