Home Python装饰器
Post
Cancel

Python装饰器

基本概念

  • 函数可以作为参数传递的语言,可以使用装饰器
  • 装饰器是一个函数(或类),接一个函数(或类),返回一个函数(或类)
  • 装饰器不改变被装饰函数,可以拓展或修改被装饰函数的功能

一些用法

  • 打 log,debug
  • 访问控制
  • 计数,计时,api 调用限速
  • 人为增加延迟
  • 动态注册组件
  • 缓存调用结果

引入

Py 函数是一等对象,可以作为参数传给另一个函数

1
2
3
4
5
6
7
8
9
10
11
12
def say_hello(name):
    return f"Hello {name}"

def be_awesome(name):
    return f"Yo {name}, together we are the awesomest!"

def greet_bob(greeter_func):
    return greeter_func("Bob")

greet_bob(say_hello) # 'Hello Bob'
greet_bob(be_awesome) # 'Yo Bob, together we are the awesomest!'

在函数中定义函数

1
2
3
4
5
6
def parent():
    def child():
        print("child")
    return child

parent() # <function parent.<locals>.child at 0x7fe1c6921fc0>

Python 的 for 不引入作用域,整段代码除了 power 里面都是 global

1
2
3
4
5
6
7
8
9
10
11
12
13
14
powers = []
for i in range(3):
    def power(x):
        # i = i+1 # UnboundLocalError: local variable 'i' referenced before assignment
        return x ** i
    powers.append(power)

print(i) # 2

for p in powers:
    print(p(2))
# 4
# 4
# 4

wrapper 函数叫做闭包(保存一个函数和其运行环境)。闭包可以保存运行环境,变量绑定参数之后值一直不变

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
powers = []
for i in range(3):
    def maker(ic): # 参数叫i也一样,局部覆盖全局
        def power(x):
            return x ** ic
        return power
    powers.append(maker(i))

print(i) # 2

for p in powers:
    print(p(2))
# 1
# 2
# 4

绑定参数就可以固定变量指

1
2
3
4
5
6
7
8
9
10
11
12
13
powers = []
for i in range(3):
    def power(x, i=i):
        return x ** i
    powers.append(power)

print(i) # 2

for p in powers:
    print(p(2))
# 1
# 2
# 4

用法

基础

装饰器两种写法

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
def decorator(func):
    def wrapper(*args, **kwargs):
        print("Before")
        func(*args, **kwargs)
        print("After")
    return wrapper

def add(a, b):
    print(a+b)

'''
1. 调用装饰器函数,返回装饰过的函数
- 可以保留未装饰函数
'''
add = decorator(add)
print(add) # <function decorator.<locals>.wrapper at 0x7fe1c6922290>
add(2, 3)

'''
2. @
- 在定义时立即装饰
    - 递归也是装饰过的
    - 没有未被装饰的函数
'''
@decorator
def add(a, b):
    print(a+b)

print(add) # <function decorator.<locals>.wrapper at 0x7fe1c6922050>
add(2, 3)

注意

  • 不要忘了返回 wrapper
  • 注意区分外层函数参数和内层函数参数

装饰后,函数的一些元数据,比如namedoc之类的会变成 wrapper 的,用 wraps 工具把被装饰函数的属性贴到装饰过的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from functools import wraps

def decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("Before")
        func(*args, **kwargs)
        print("After")

    return wrapper

def add(a, b):
    print(a+b)

print(add) # <function add at 0x7f5d9deedfc0>
add = decorator(add)
print(add) # <function add at 0x7f5d9deee050> 不是同一个函数
add(2, 3)

叠加

装饰器可以叠加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def p(func):
    def wrapper(s):
        return "<p>" + func(s) + "</p>"
    return wrapper

def div(func):
    def wrapper(s):
        return "<div>" + func(s) + "</div>"
    return wrapper

@div
@p
def say(something):
    return something


say("hello") # '<div><p>hello</p></div>'

  • 相当于 div(p(say))
  • 每层装饰都是函数调用,可能有性能影响

装饰类的方法

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
class Circle:
    def __init__(self, radius):
        self._radius = radius

    @property
    def radius(self):
        """Get value of radius"""
        return self._radius

    @radius.setter
    def radius(self, value):
        """Set radius, raise error if negative"""
        if value >= 0:
            self._radius = value
        else:
            raise ValueError("Radius must be positive")

    @property
    def area(self):
        """Calculate area inside circle"""
        return self.pi() * self.radius**2

    def cylinder_volume(self, height):
        """Calculate volume of cylinder with circle as base"""
        return self.area * height

    @classmethod
    def unit_circle(cls):
        """Factory method creating a circle with radius 1"""
        return cls(1)

    @staticmethod
    def pi():
        """Value of π, could use math.pi instead though"""
        return 3.1415926535

装饰类本身

1
2
3
4
5
6
from dataclasses import dataclass

@dataclass
class PlayingCard:
    rank: str
    suit: str

装饰器接参数

  • @函数()的话,函数只会接到()里的参数,函数应该返回一个装饰器函数
  • @函数 的话,函数只会接到被装饰函数
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
def repeat(times=2):
    print("times", times)
    def decorator(func):
        def wrapper():
            for _ in range(times):
                func()
        return wrapper
    return decorator

@repeat(times=3)
def hello():
    print("hello")

# times 3
hello()
# hello
# hello
# hello

@repeat
def hi():
    print("hi")
# times <function hi at 0x7fd774eedfc0> 不带()调用,被装饰函数被当作times传进去了

hi() # TypeError: repeat.<locals>.decorator() missing 1 required positional argument: 'func'

让装饰器同时支持 @装饰器 和 @装饰器() 调用方法

TODO: 为什么要 _ ,没有也能跑 def repeat(_func=None, _,times=2):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def repeat(_func=None, times=2):
    def decorator(func):
        def wrapper():
            for _ in range(times):
                func()
        return wrapper
    if _func is None:
        return decorator
    else:
        return decorator(_func)

@repeat
def hi():
    print("hi")

hi()

@repeat(times=3)
def hello():
    print("hello")

hello()

带状态的装饰器

函数属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def counter(func):
    def wrapper():
        wrapper.num_called += 1
        print(f"Calling the {wrapper.num_called} time")
        return func()
    wrapper.num_called = 0
    return wrapper

@counter
def func():
    pass

func() # Calling the 1 time
func() # Calling the 2 time
print(func.num_called) # 2
func() # Calling the 3 time

装饰器类

__init__接被装饰函数,__call__允许实例被调用。调用这个装饰器的返回是类的一个实例,所以call就是 wrap 函数,不应该返回 wrap 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from functools import update_wrapper


class Counter:
    def __init__(self, func):
        update_wrapper(self, func)
        self._func = func
        self._count = 0
    def __call__(self):
        self._count += 1
        print(f"Calling the {self._count} time")
        self._func()

@Counter
def func():
    pass

func()

装饰器实现 singleton

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def singleton(cls):
    def wrapper():
        if wrapper.instance is None:
            wrapper.instance = cls()
            return wrapper.instance
        return wrapper.instance
    wrapper.instance = None
    return wrapper

class Hi:
    pass


Hi() # <__main__.Hi object at 0x7f4ad4b151b0>
Hi() # <__main__.Hi object at 0x7f4ad4b15e40>

@singleton
class Hello:
    pass

Hello() # <__main__.Hello object at 0x7f4ad4b15ae0>
Hello() # <__main__.Hello object at 0x7f4ad4b15ae0>

缓存调用结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def cacher(func):
    def wrapper(*args, **kwargs):
        params = args + tuple(kwargs.items())
        wrapper.num_call += 1
        if params in wrapper.cache:
            return wrapper.cache[params]
        else:
            wrapper.num_calc += 1        
            wrapper.cache[params] = func(*args, **kwargs)
        return wrapper.cache[params]
    wrapper.cache = {}
    wrapper.num_call = 0
    wrapper.num_calc = 0
    return wrapper

@cacher
def fib(idx):
    if idx < 2:
        return idx
    return fib(idx-1 ) + fib(idx-2)

fib(10)
print(fib.num_call) # 19
print(fib.num_calc) # 11

使用functool内置LRU cache

1
2
3
4
5
6
7
8
9
10
import functools
@functools.lru_cache(maxsize=5)
def fib(idx):
    if idx < 2:
        return idx
    return fib(idx-1 ) + fib(idx-2)

fib(10)
fib.cache_info() # CacheInfo(hits=8, misses=11, maxsize=5, currsize=5)

!r repr !s str !a ascii

time.perf_counter()

https://realpython.com/primer-on-python-decorators/

https://peps.python.org/pep-0318/

https://wiki.python.org/moin/PythonDecorators

https://wiki.python.org/moin/PythonDecoratorLibrary

https://github.com/micheles/decorator

All Rights Reserved.

Windows Sub Linux

Python类型注解