Kyle's Notebook

Python 装饰器基本用法

Word count: 1.8kReading time: 8 min
2019/09/13

Python 装饰器基本用法

装饰器是函数闭包最重要的应用,在面试中经常会被问到,这是我参加面试/刷面试题时候遇到的装饰器用法总结。

保存元数据

元数据(MetaData)是关于数据的数据,是用来描述数据信息。

查看这些元数据的函数是 dir(func)

1
2
3
def func():
pass
dir(func)

执行结果:

1
['__annotations__', '__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__get__', '__getattribute__', '__globals__', '__gt__', '__hash__', '__init__', '__kwdefaults__', '__le__', '__lt__', '__module__', '__name__', '__ne__', '__new__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']

很显然元数据的数量与函数的复杂程度无关。
其中有几个重要的元数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def f(a, b=1, c=[]):
"""test function"""
print(a, b, c)

# __name__ test
# __doc__ test function
# __defaults__ (1, []),尽量不要使用可变类型为参数


# 获取函数内部变量
def f():
a = 3
return lambda k: a**k

g = f()
print(g.__closure__[0].cell_contents)
# 输出3,__closure__返回函数中的局部变量列表

在使用装饰器时,闭包默认不会保存原函数的元数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from functools import update_wrapper
def decorator(func):
def wrapper(*args, **kwargs):
"""wrapper test"""
print("decorator test")
func(*args, **kwargs)
return wrapper

@decorator
def func():
"""func test"""
print("func test")

print(func.__name__)
print(func.__doc__)

执行结果:

1
2
wrapper
wrapper test

很显然原函数的 __name__ 为 func,__doc__ 为 func test,但经过装饰后已经被修改为装饰器内部函数的元数据。

要使函数被装饰后仍保留原来的元数据可以使用以下两种方法定义装饰器:

1
2
3
4
5
6
7
8
def decorator(func):
@wraps(func) # 方法一
def wrapper(*args, **kwargs):
"""decorator test"""
print("wrapper test")
func(*args, **kwargs)
# update_wrapper(wrapper, func) # 方法二
return wrapper

两者取其一即可,装饰后重新执行刚才的函数:

1
2
func
func test

带参数的装饰器

除了接收被装饰函数的参数以外,装饰器本身也可以传入参数,下面的例子是利用装饰器参数对被装饰函数接收的参数类型进行判断:

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
from inspect import signature

def type_assert(*args, **kwargs):

def decorator(func):
sig = signature(func)
# 提取函数签名:返回参数名与参数类型的有序字典:
# OrderDict([("a", <class "int">), ("b", <class "str">), ("b", <class "list">)])
types = sig.bind_partial(*args, **kwargs).arguments

def wrapper(*args, **kwargs):
arg_dict = sig.bind(*args, **kwargs).arguments
# 其中 key 为参数名 (a), value 为参数值(1),types[key] 为类型(int)
for key, value in arg_dict.items():
if key in types and not isinstance(value, types[key]):
raise TypeError("'{}' must be '{}'".format(key, types[key]))
return func(*args, **kwargs)
return wrapper
return decorator


@type_assert(int, str, list)
def f(a, b, c):
print(a, b, c)

f(1, "2", [3])
f(1, 2, 3)

执行结果:

1
2
3
4
5
6
7
1 2 [3]
Traceback (most recent call last):
File "E:/Projects/PythonLearning/Notes/decorator.py", line 105, in <module>
f(1, 2, 3)
File "E:/Projects/PythonLearning/Notes/decorator.py", line 94, in wrapper
raise TypeError("'{}' must be '{}'".format(key, types[key]))
TypeError: 'b' must be '<class '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
27
28
29
30
31
def timer(timeout):
timeout = [timeout] # 必须使用 list 等可变类型,否则无法被 set_timeout 修改

def decorator(func):
def wrapper(*args, **kwargs):
start = time.time()
res = func(*args, **kwargs)
used = time.time() - start
# 当运行时间超过 timeout 时会输出日志
if used > timeout[0]:
print("'{}': {} > {}".format(func.__name__, used, timeout[0]))
return res

# 设置超时时间函数:被装饰函数可调用
def set_timeout(value):
timeout[0] = value

# 把 set_timeout 设置为被装饰函数的方法
wrapper.set_timeout = set_timeout
return wrapper

return decorator


@timer(2)
def test():
time.sleep(1)

test() # 无输出:runtime(1s) < timeout(2)
test.set_timeout(0.5)
test() # 有输出:runtime(1s) > timeout(0.5)

第一次执行时函数运行需要 1 秒,超时时间为 2 秒,所以没有日志输出;

第二次执行时函数运行同为 1 秒,但超时时间改为 0.5 秒,所以有日志输出。

类的装饰器

除了函数以外,类也可以使用装饰器,其中最典型的应用是实现设计模式中的单例模式(同时也是装饰器模式):

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):

instances = {}

def singleton(*args, **kwars):
# 第一次实例化时创建一个对象存放在 instances
# 下次再使用这个类实例化时只从 instances 取用,而不再创建对象
if cls not in instances:
instances[cls] = cls(*args, **kwars)
return instances[cls]

return singleton


@Singleton
class Student(object):

def __init__(self, name):
self.name = name

s1 = Student("ywh")
print(s1.name)
s2 = Student("123")
print(s2.name)

执行结果:

1
2
ywh
ywh

虽然两次创建了 Student 对象,但执行的效果却是同一个 Student 对象,因为根据闭包的特性,函数执行完毕其内部的 instances 字典仍然保留在内存中,所以下次调用 Student 类实例化时会从 instances 取用。

递归与缓存

在 Python 中使用程序计算 Fibonacci 数列只需要一行代码:

1
2
def fibonacci(n):
return fibonacci(n-1) + fibonacci(n-2) if n > 2 else 1

但这种方式计算效率不是一般的低(我家电脑 i5-6200U + 8G DDR4 计算到 40 项左右就已经很吃力了……),缺点在于求任意一项都需要前面两项的值,充斥着大量的重复计算(例如计算第 5 项需要第 3 和第 4 项,计算第 4 项需要第 2 和第 3 项的值,光是这两步第 3 项就被算了两次了 = =)。

如果在计算的同时把中间结果保留到缓存中,下次直接从缓存获取就不需要重新计算了,测试:

1
2
3
4
5
6
7
8
9
10
11
12
def decorator(func):
cache = {}

def wrapper(*args, **kwargs):
if args not in cache:
cache[args] = func(*args)
return cache[args]
return wrapper

@decorator
def fibonacci(n):
return fibonacci(n-1) + fibonacci(n-2) if n > 2 else 1

之前已经说过因为闭包的特性 cache 里面的元素会一直保存在内存中,所以下次递归时就能先尝试从 cache 中取值,就能大幅度提高效率:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@decorator
def f1(n):
return f1(n - 1) + f1(n - 2) if n > 2 else 1

def f2(n):
return f2(n - 1) + f2(n - 2) if n > 2 else 1

start = time.time()
res = f1(40)
time.sleep(0.1)
end = time.time()
print("{}, {}s".format(res, end - start))


start = time.clock()
res = f2(40)
time.sleep(0.1)
end = time.clock()
print("{}, {}s".format(res, end - start))

执行结果:

1
2
102334155, 0.10854005813598633s
102334155, 39.62842997040642s

装饰器的用法灵活多变(例如还可以把类的实例方法作为装饰器函数,此时在被装饰函数中就可以持有实例对象,便于修改实例的属性、拓展功能),在这里无法一一列出,更多细节和有趣的用法还需要在日常开发中发现和积累。

CATALOG
  1. 1. Python 装饰器基本用法
    1. 1.1. 保存元数据
    2. 1.2. 带参数的装饰器
    3. 1.3. 属性可修改的装饰器
    4. 1.4. 类的装饰器
    5. 1.5. 递归与缓存