Python装饰器的常见用法。所谓的装饰器,其实就是通过装饰器函数,来修改原函数的一些功能,使得原函数不需要修改。

函数也是对象,可以赋值给变量,可以做为参数,也可以嵌套在另一个函数内。

对于第三种情况,如果在一个函数的内部定义了另一个函数,外部的我们叫他外函数,内部的我们叫他内函数。在一个外函数中定义了一个内函数,内函数里运用了外函数的临时变量,并且外函数的返回值是内函数的引用。这样就构成了一个闭包

一般情况下,在我们认知当中,如果一个函数结束,函数的内部所有东西都会释放掉,还给内存,局部变量都会消失。但是闭包是一种特殊情况,如果外函数在结束的时候发现有自己的临时变量将来会在内部函数中用到,就把这个临时变量绑定给了内部函数,然后自己再结束。

装饰器从0到1

Decorators is to modify the behavior of the function through a wrapper so we don’t have to actually modify the function.

所谓的装饰器,其实就是通过装饰器函数,来修改原函数的一些功能,使得原函数不需要修改。实际工作中,装饰器通常运用在身份认证(登录认证)、日志记录、性能测试、输入合理性检查及缓存等多个领域中。合理使用装饰器,可极大提高程序的可读性及运行效率。

在面向对象(OOP)的设计模式中,decorator被称为装饰模式。OOP的装饰模式需要通过继承和组合来实现,而Python除了能支持OOP的decorator外,直接从语法层次支持decorator。Python的decorator可以用函数实现,也可以用类实现。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
def my_decorator(func):
    def inner_wrapper():
        print('inner_wrapper of decorator')
        func()
    return inner_wrapper

@my_decorator 
def hello():
    print('hello world')

hello()

"""
inner_wrapper of decorator
hello world
"""
my_decorator.__name__   
# 'my_decorator'
hello.__name__   
# 'inner_wrapper'

这里的@,我们称之为语法糖。@my_decorator 相当于 greet=my_decorator(greet)

对于需要传参数的函数,可以在在对应的装饰器函数inner_wrapper()上,加上相应的参数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
def my_decorator(func):
    def inner_wrapper(arg1):
        print('inner_wrapper of decorator')
        func(arg1)
    return inner_wrapper

@my_decorator
def hello(arg1):
    print('hello world')
    print(arg1)

hello("I'm arg1")

"""
inner_wrapper of decorator
hello world
I'm arg1
"""
my_decorator.__name__   # 'my_decorator'
hello.__name__   # 'inner_wrapper'

但是,假设我们有一个新函数需要两个参数,前面定义的@my_decorator就会不适用。如:

1
2
3
4
5
@my_decorator
def hello(arg1,arg2):
    print('hello world')
    print(arg1)
    print(arg2)

我们可以把*args**kwargs,作为装饰器内部函数inner_wrapper()的参数 ,表示接受任意数量和类型的参数,因此装饰器就可以写成下面的形式:

1
2
3
4
5
def my_decorator(func):
    def inner_wrapper(*args, **kwargs):
        print('inner_wrapper of decorator')
        func(*args, **kwargs)
    return inner_wrapper

还可以给decorator函数加参数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def loginfo(info, n):
    def my_decorator(func):
        def inner_wrapper(*args, **kwargs):
            for i in range(n):
                print(f'<{i}> loginfo: {info}')
                func(*args, **kwargs)
        return inner_wrapper
    return my_decorator

@loginfo("NOBUG", 3)
def hello(arg1):
    print('hello world')
    print(arg1)

hello("I'm arg1")

"""
<0> loginfo: NOBUG
hello world
I'm arg1
<1> loginfo: NOBUG
hello world
I'm arg1
<2> loginfo: NOBUG
hello world
I'm arg1
"""
my_decorator.__name__   # 'my_decorator'
hello.__name__   # 'inner_wrapper'

但是经过装饰器装饰之后,hello()函数的元信息被改变,它不再是以前的那个 hello()函数,而是被inner_wrapper()取代了:

1
2
3
4
5
6
7
8
hello.__name__ # 'inner_wrapper'

help(hello)
"""
Help on function inner_wrapper in module __main__:

inner_wrapper(*args, **kwargs)
"""

这个问题很好解决:

内置的装饰器@functools.wrap,它会帮助保留原函数的元信息(也就是将原函数的元信息,拷贝到对应的装饰器函数里)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import functools

def my_decorator(func):
    @functools.wraps(func)
    def inner_wrapper(*args, **kwargs):
        print('inner_wrapper of my_decorator.')
        func(*args, **kwargs)
    return inner_wrapper

@my_decorator
def hello():
    print("hello world")

hello.__name__
# 'hello'

上面的例子可以写成:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import functools

def loginfo(info,n):
    def my_decorator(func):
        @functools.wraps(func)
        def inner_wrapper(*args, **kwargs):
            for i in range(n):
                print(f'<{i}> loginfo: {info}')
                func(*args, **kwargs)
        return inner_wrapper
    return my_decorator

@loginfo("NOBUG",3)
def hello(arg1):
    print('hello world')
    print(arg1)

hello("I'm arg1")

"""
<0> loginfo: NOBUG
hello world
I'm arg1
<1> loginfo: NOBUG
hello world
I'm arg1
<2> loginfo: NOBUG
hello world
I'm arg1
"""
my_decorator.__name__   # 'my_decorator'
hello.__name__   # 'hello'

用类作为装饰器

绝大多数装饰器都是基于函数和闭包实现的,但这并非构造装饰器的唯一方式。事实上,Python 对某个对象是否能通过装饰器( @decorator)形式使用只有一个要求:decorator 必须是一个“可被调用(callable)的对象

函数自然是“可被调用”的对象。但除了函数外,我们也可以让任何一个类(class)变得“可被调用”(callable),只要自定义类的 __call__ 方法即可。

因此不仅仅是函数,类也可以做为装饰器来用。但作为装饰器的类需要包含__call__()方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import functools

class Count:
    def __init__(self, func):
        self.func = func
        self.num_calls = 0
        functools.update_wrapper(self, func)
        # 类似于函数方法中的:@functools.wraps(func)

    def __call__(self, *args, **kwargs):
        self.num_calls += 1
        print('num of calls is: {}'.format(self.num_calls))
        return self.func(*args, **kwargs)

@Count
def hello():
    print("hello world")

hello()

# # 输出
# num of calls is: 1
# hello world

hello()

# # 输出
# num of calls is: 2
# hello world

hello()

# # 输出
# num of calls is: 3
# hello world

hello.__name__
# 'hello'

通过名为__call__的特殊方法,可以使得类的实例能像python普通函数一样被调用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class Count:
    def __init__(self, num_calls=5):
        self.num_calls = num_calls

    def __call__(self):
        print('num of calls is: {}'.format(self.num_calls))

a = Count(666)
a()

"""
num of calls is: 666
"""

装饰器的嵌套使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import functools

def my_decorator1(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print('execute decorator1')
        func(*args, **kwargs)
    return wrapper

def my_decorator2(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print('execute decorator2')
        func(*args, **kwargs)
    return wrapper

def my_decorator3(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print('execute decorator3')
        func(*args, **kwargs)
    return wrapper

@my_decorator1
@my_decorator2
@my_decorator3
def hello(message):
    print(message)
# 类似于调用:decorator1(decorator2(decorator3(func)))

hello('hello world')
hello.__name__

# 输出
# execute decorator1
# execute decorator2
# execute decorator3
# hello world
# 'hello'

装饰器的一些常见用途

1. 记录函数运行时间(日志)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import time
import functools

def log_execution_time(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        res = func(*args, **kwargs)
        end = time.perf_counter()
        print(f'{func.__name__} took {(end - start) * 1000} ms')
        return res
    return wrapper
    
@log_execution_time
def calculator():
    for i in range(1000000):
        i = i**2**(1/3)**(1/6)
    return i

calculator()
"""
calculator took 109.1254340026353 ms
48525172657.38456
"""
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import functools

def log(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        print(f'call {func.__name__}():')
        return func(*args, **kwargs)
    return wrapper

@log
def now():
    print('2019-3-25')
    
def logger(text):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            print(f'{text} {func.__name__}():')
            return func(*args, **kwargs)
        return wrapper
    return decorator

@logger('DEBUG')
def today():
    print('2019-3-25')

now()
# call now():
# 2019-3-25
today()
# DEBUG today():
# 2019-3-25
today.__name__
# today

2. 登录验证

有些网页的权限是需要登录后才有的。可以写一个装饰器函数验证用户是否登录,而不需要重复写登录验证的逻辑。

3. 输入合理性检查

对于一些需要做合理性检验的地方,可以抽象出合理性检验的逻辑,封装为装饰器函数,实现复用。例如:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
def validate_summary(func):
      @functools.wraps(func)
       def wrapper(*args, **kwargs):
           data = func(*args, **kwargs)
           if len(data["summary"]) > 80:
               raise ValueError("Summary too long")
           return data
       return wrapper

   @validate_summary
   def fetch_customer_data():
       # ...

   @validate_summary
   def query_orders(criteria):
       # ...

   @validate_summary
   def create_invoice(params):
       # ...

4. 缓存

LRU cache,在 Python 中的表示形式是@lru_cache,它会缓存进程中的函数参数和结果,当缓存满了以后,会删除 least recenly used 的数据。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
from functools import lru_cache

@lru_cache(maxsize=16) # default :128
def sum2(a, b):
    print(f"Invoke func: sum2()")
    print(f"Calculating {a} + {b}")
    return a + b

print(sum2(1, 2))
print("====================")
print(sum2(1, 2))
print("====================")
print(sum2.cache_info())
print(sum2.cache_clear())
print(sum2.cache_info())

"""
Invoke func: sum2()
Calculating 1 + 2
3
====================
3
CacheInfo(hits=1, misses=1, maxsize=16, currsize=1)
None
CacheInfo(hits=0, misses=0, maxsize=16, currsize=0)
"""

5. 类中常用的@staticmethod@classmethod

  • @classmethod 装饰的类方法
  • @staticmethod装饰的静态方法
  • 不带装饰器的实例方法

@classmethod修饰的方法,第一个参数不是表示实例本身的self,而是表示当前对象的类本身的clf@staticmethod是把函数嵌入到类中的一种方式,函数就属于类,同时表明函数不需要访问这个类。通过子类的继承覆盖,能更好的组织代码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
class A(object):
    def foo(self, x):
        print("executing foo(%s,%s)" % (self, x))
        print('self:', self)
    @classmethod
    def class_foo(cls, x):
        print("executing class_foo(%s,%s)" % (cls, x))
        print('cls:', cls)
    @staticmethod
    def static_foo(x):
        print("executing static_foo(%s)" % x)    

if __name__ == '__main__':
    a = A()
    # foo方法绑定对象A的实例,class_foo方法绑定对象A,static_foo没有参数绑定。
    print(a.foo)
    # <bound method A.foo of <__main__.A object at 0x0278B170>>
    print(a.class_foo)
    # <bound method A.class_foo of <class '__main__.A'>>
    print(a.static_foo)
    # <function A.static_foo at 0x02780390>

普通的类方法foo()需要通过self参数隐式的传递当前类对象的实例。 @classmethod修饰的方法class_foo()需要通过cls参数传递当前类对象。@staticmethod修饰的方法定义与普通函数是一样的。

selfcls的区别不是强制的,只是PEP8中一种编程风格。self通常用作实例方法的第一参数,cls通常用作类方法的第一参数。即通常用self来传递当前类对象的实例,cls传递当前类对象。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# foo可通过实例a调用,类对像A直接调用会参数错误。
a.foo(1)
"""
executing foo(<__main__.A object at 0x0278B170>,1)
self: <__main__.A object at 0x0278B170>
"""
A.foo(1)
"""
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: foo() missing 1 required positional argument: 'x'
"""

# 但foo如下方式可以使用正常,显式的传递实例参数a。
A.foo(a, 1)
"""
executing foo(<__main__.A object at 0x0278B170>,1)
self: <__main__.A object at 0x0278B170>
"""

# class_foo通过类对象或对象实例调用。
A.class_foo(1)
"""
executing class_foo(<class '__main__.A'>,1)
cls: <class '__main__.A'>
"""
a.class_foo(1)
"""
executing class_foo(<class '__main__.A'>,1)
cls: <class '__main__.A'>
"""
a.class_foo(1) == A.class_foo(1)
"""
executing class_foo(<class '__main__.A'>,1)
cls: <class '__main__.A'>
executing class_foo(<class '__main__.A'>,1)
cls: <class '__main__.A'>
True
"""

# static_foo通过类对象或对象实例调用。
A.static_foo(1)
"""
executing static_foo(1)
"""
a.static_foo(1)
"""
executing static_foo(1)
"""
a.static_foo(1) == A.static_foo(1)
"""
executing static_foo(1)
executing static_foo(1)
True
"""

继承与覆盖普通类函数是一样的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class B(A):
    pass
b = B()
b.foo(1)
b.class_foo(1)
b.static_foo(1)
"""
executing foo(<__main__.B object at 0x007027D0>,1)
self: <__main__.B object at 0x007027D0>
executing class_foo(<class '__main__.B'>,1)
cls: <class '__main__.B'>
executing static_foo(1)
"""

REFERENCE