上一篇文章介绍了装饰器的基本概念和语法,并且实现了一个简单的装饰器。但这些仅仅是学习装饰器的第一步,本文介绍如何实现一个更好的装饰器。

保留函数属性

上一篇文章已经提到在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

  1. 定义一个函数,并为其写一个说明文档
  2. 函数的__name__属性,一般是定义的时候的变量名
  3. 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

  1. 定义一个装饰器
  2. 为内部函数写一个说明文档
  3. 将装饰器应用于add(作用和使用@是一样的)
  4. add__name__属性变了
  5. __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
  1. 定义装饰器工厂函数
  2. 这个稍后再讲
  3. 定义装饰器

上述代码实现了一个装饰器工厂函数,根据不同的参数生成不同的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,然后在装饰器内部使用这个变量,让我们分析一下这个装饰器加载过程。

  1. 调用timeit_factory,并定参数
  2. 定义fmt
  3. 定义装饰器
  4. 在装饰器内部使用了fmt,但是并没有保存
  5. 返回装饰器,timeit_factory函数结束,同时局部命名空间也被销毁

那么问题来了,再后来的函数调用中,是肯定使用到了fmt,但是这个fmt保存到哪里了呢。并且又上面的例子可以看出,针对f2f4,应该是有两个取值不同的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

看出看出,这个函数同样满足需求,并且返回的两个函数avgavg2并不相互影响。

在函数avg内没有定义totalsum,那么后来使用的这两个变量存在哪里了?

答案是闭包

维基百科给闭包的定义:引用了自由变量的函数。这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。

也就是说,avgavg2引用了自由变量,也就是外部定义的变量,那么这个变量将和这两个函数一起存在,即使定义这些自由变量的环境后也不例外。

>>> avg.__code__.co_freevars # 1
('size', 'total')

>>> avg.__closure__[0].cell_contents # 2
3
>>> avg.__closure__[1].cell_contents  # 3
18
  1. avg使用的自由变量
  2. avg的闭包中保存着sizetotal

总结

本文介绍了如何实现一个更好的装饰器(保留函数属性以及使用装饰器工厂),并且解释了自由变量和函数闭包。

10-06 14:28