上一篇文章介绍了装饰器的基本概念和语法,并且实现了一个简单的装饰器。但这些仅仅是学习装饰器的第一步,本文介绍如何实现一个更好的装饰器。
保留函数属性
上一篇文章已经提到在python中函数也是一个对象,并且使用了它的__name__
属性。事实上,python中的函数作为一个function
类的一个实例,有很多的属性,其中比较重要的属性石是__name__
和__doc__
, 前者已经介绍过了,后者是一个函数的说明文档。
>>> def add(a, b): # 1
... '''
... the sum of a and b
... '''
... return a + b
...
>>> print(add.__name__) # 2
add
>>> print(add.__doc__) # 3
the sum of a and b
- 定义一个函数,并为其写一个说明文档
- 函数的
__name__
属性,一般是定义的时候的变量名 - python会自动将函数定义语句后的字符串赋值给函数的
__doc__
属性(定义类的时候也是)。可以通过print(add.__doc__)
或者help(add)
来查看这个说明文档,建议第二种方式。
定义一个装饰器, 并应用于add
>>> def decorator(func): # 1
... def wrap(*args, **kwargs):
... # 2
... '''
... inner wrapper
... '''
... # do something
... return func(*args, **kwargs)
... return wrap
...
>>> add = decorator(add) # 3
>>>
>>> add.__name__ # 4
'wrap'
>>>
>>> print(add.__doc__) $# 5
inner wrapper
- 定义一个装饰器
- 为内部函数写一个说明文档
- 将装饰器应用于
add
(作用和使用@
是一样的) add
的__name__
属性变了__doc__
属性同样变了
将装饰器应用于add
之后,add
所指向的对象实际上是decorator
内部定义的函数wrap
,只是二者的功能是一样的,但是它的一些属性变得和原来不在一样以后,这显然不是我们想要的。所以在定义装饰器的时候需要一点修改。
>>> def decorator(func):
... def wrap(*args, **kwargs):
... '''
... inner wrapper
... '''
... # do something
... return func(*args, **kwargs)
... wrap.__name__ = func.__name__ # 显示的修改wrap的属性
... wrap.__doc__ = func.__doc__
... return wrap
python为我们提供了更简单的方式。
>>> import functools
>>> def decorator(func):
... @functool.wraps(func) # python内置的装饰器
... def wrap(*args, **kwargs):
... '''
... inner wrapper
... '''
... # do something
... return func(*args, **kwargs)
... return wrap
可以看到,我们使用了一个新的装饰器,这个装饰器和我们之前所使用的装饰器都不太一样,因为它接收了一个参数func
。接下来介绍,带参数的装饰器。
参数化的装饰器
函数可以根据参数返回对应的结果,在python中这个返回值可以是一个函数。既然装饰器是一个可以返回函数的函数,那自然也可以定义一个工厂函数,让它根据不同的参数返回订制的装饰器。
>>> import time
>>> def timeit(func):
... def wrapper(*args, **kwargs):
... start_time = time.time()
... result = func(*args, **kwargs)
... print('<run %s, cost time:%.4fs>' %(func.__name__, time.time() - start_time))
... return result
... return wrapper
这是上一篇文章中的例子,功能是在函数执行完成后打印运行时长。假如现在有一个需求,可以订制精确到小数点后几位。我们不用针对每一个精度都写一个对应的装饰器,只需要实现一个装饰器工厂函数,根据不同的精度,返回不同的装饰器。
>>> def timeit_factory(i): # 1
... fmt = 'cost time:{:.%df}s' % i # 2
... def timeit(func): # 3
... @functools.wraps(func)
... def wrapper(*args, **kwargs):
... start_time = time.time()
... result = func(*args, **kwargs)
... cost = time.time() - start_time
... print(fmt.format(cost))
... return result
... return wrapper
... return timeit
- 定义装饰器工厂函数
- 这个稍后再讲
- 定义装饰器
上述代码实现了一个装饰器工厂函数,根据不同的参数生成不同的fmt
,继而控制精确到小数点后几位。
>>> @timeit_factory(2)
... def f2(a, b):return a + b
...
>>> @timeit_factory(4)
... def f4(a, b):return a + b
...
>>> f2(1, 2)
cost time:0.00s
3
>>> f4(1, 2)
cost time:0.0000s
3
闭包
上一段代码中,在timeit
之前,也就是真的装饰器之前,定义了一个fmt
,然后在装饰器内部使用这个变量,让我们分析一下这个装饰器加载过程。
- 调用
timeit_factory
,并定参数 - 定义
fmt
- 定义装饰器
- 在装饰器内部使用了
fmt
,但是并没有保存 - 返回装饰器,
timeit_factory
函数结束,同时局部命名空间也被销毁
那么问题来了,再后来的函数调用中,是肯定使用到了fmt
,但是这个fmt
保存到哪里了呢。并且又上面的例子可以看出,针对f2
和f4
,应该是有两个取值不同的fmt
。
如果这个例子不够清晰,再举一个例子。
现在需要一个函数,功能是计算不断增加的一系列值的平均值。需要实现的功能如下
>>> avg(4)
4.0
>>> avg(5)
4.5
>>> avg(9)
6.0
要实现这个功能肯定是需要保存每次调用时输入进去的值,问题在于保存在哪里呢。
可以用一个类来实现这个功能。
>>> class Average():
... def __init__(self):
... self.sum = 0
... self.size = 0
... def __call__(self, val):
... self.sum += val
... self.size += 1
... return self.sum / self.size
定义了__call__
属性后的类,其实例可以像一个函数一样被调用,具体不在此说明。
在python中,还有另一种实现方法,不需要类。
>>> def Average():
... total = 0
... size = 0
... def avg(val):
... nonlocal total, size # 标明引用外部变量
... total += val
... size += 1
... return total / size
... return avg
>>> avg(4)
4.0
>>> avg(5)
4.5
>>> avg2 = Average()
>>> avg2(3)
3.0
>>> avg2(5)
4.0
看出看出,这个函数同样满足需求,并且返回的两个函数avg
和avg2
并不相互影响。
在函数avg内没有定义total
和sum
,那么后来使用的这两个变量存在哪里了?
答案是闭包
维基百科给闭包的定义:引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。
也就是说,avg
和avg2
引用了自由变量,也就是外部定义的变量,那么这个变量将和这两个函数一起存在,即使定义这些自由变量的环境后也不例外。
>>> avg.__code__.co_freevars # 1
('size', 'total')
>>> avg.__closure__[0].cell_contents # 2
3
>>> avg.__closure__[1].cell_contents # 3
18
avg
使用的自由变量- 在
avg
的闭包中保存着size
和total
总结
本文介绍了如何实现一个更好的装饰器(保留函数属性以及使用装饰器工厂),并且解释了自由变量和函数闭包。