欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Python中的函数装饰器和闭包原理

程序员文章站 2022-07-14 12:34:00
...

本文为《流畅的Python》——函数装饰器和闭包的学习笔记和总结

函数装饰器和闭包

1.1 装饰器概述

  • 装饰器是可调用对象,其参数是另一个函数(被装饰的函数)。可以返回原函数,也可以返回装饰器内部定义的函数。

    @decorate
    def target():
    	print('running target()')
    

    下边写法与上述代码效果一样:

    def target():
    	print('running target()')
    	
    target = decorate(target)
    

1.2 装饰器与被装饰对象执行顺序

  • 装饰器的一个关键特性:在被装饰的函数定义之后,装饰器会立即运行。通常是在导入时(即Python加载模块时)。

    自定义一个注册模块registration.py

    registry = [] 
    def register(func): 
        print('running register(%s)' % func) 
        registry.append(func) 
        return func 
        
    @register 
    def f1():
    	print('running f1()')
    	
    @register
    def f2():
    	print('running f2()')
    	
    def f3(): 
    	print('running f3()')
    	
    def main(): 
        print('running main()')
        print('registry ->', registry)
        f1()
        f2()
        f3()
        
    if __name__=='__main__':
    	main() 
    

    运行结果:

    $ python3 registration.py
    running register(<function f1 at 0x100631bf8>)
    running register(<function f2 at 0x100631c80>)
    running main()
    registry -> [<function f1 at 0x100631bf8>, <function f2 at 0x100631c80>]
    running f1()
    running f2()
    running f3()
    
    • if __name__=='__main__':相当于导入当前模块,可以发现在执行main()之前有两个函数被装饰,所以装饰器就执行了两次。

    • 也可以通过import registration查看:

      >>> import registration
      running register(<function f1 at 0x10063b1e0>)
      running register(<function f2 at 0x10063b268>)
      
  • 装饰器通常在一个模块中定义,在另一个模块中导入去装饰函数。

  • 多数装饰器会修改被装饰的函数。通常,它们会定义一个内部函数,然后将其返
    回,替换被装饰的函数。使用内部函数的代码几乎都要靠闭包才能正确运作。

1.3 变量作用域规则

  • 函数中变量分为局部变量和全局变量,如果在函数中修改全局变量,又没有使用global关键字修饰为全局变量,就会报错。

    >>> b = 6	# b为全局变量
    >>> def f1(a): # a为局部变量
    ... print(a)
    ... print(b)
    >>> f1(3)
    3
    6
    

    如果在函数内部修改b的值,又不声明b为全局变量或者为局部变量就会报错:

    >>> b = 6
    >>> def f2(a):
    ... print(a)
    ... print(b)
    ... b = 9
    ...
    >>> f2(3)
    3
    Traceback (most recent call last):
        File "<stdin>", line 1, in <module>
        File "<stdin>", line 3, in f2
    UnboundLocalError: local variable 'b' referenced before assignment
    

    正确的做法是使用global关键字:

    >>> b = 6
    >>> def f3(a):
    ... global b
    ... print(a)
    ... print(b)
    ... b = 9
    ...
    >>> f3(3)
    3
    6
    >>> b
    9
    

1.4 闭包

  • 当使用函数嵌套时就使用到了闭包,闭包指延伸了作用域的函数,其中包含函数体中引用、但是不在定义体中定义的非全局变量。

  • 比如下边的高阶函数(以函数作为参数或者返回值的函数),series = []对于make_averager函数来说是一个局部变量。

    # 计算移动平均值的高阶函数
    def make_averager():
    	series = []
    	
        def averager(new_value):
            series.append(new_value)
            total = sum(series)
            return total/len(series)
            
        return averager
    

    当我们调用make_averager函数时返回averager函数对象,然后我们使用averager对象传参,此时由于Python垃圾回收机制会将make_averager函数中的局部变量series回收,因为已经没有引用指向它了。

    但是averager却可以继续往既不是全局变量又不是局部变量的series中添加值,此时的series就是*变量,这就是利用了闭包的原理。

    >>> avg = make_averager()
    >>> avg(10)
    10.0
    >>> avg(11)
    10.5
    >>> avg(12)
    11.0
    

Python中的函数装饰器和闭包原理

  • 所以,闭包是一种函数,它会保留定义函数时存在的*变量的绑定,这样调用函数时,虽然定义作用域不可用了,但是仍能使用那些绑定。

1.5 nonlocal声明

在1.4节中的make_averager函数中的*变量series是一个可变类型,如果在闭包中对不可变类型的*变量进行更改,就会抛出异常:

def make_averager():
    count = 0
    total = 0
    
    def averager(new_value):
        count += 1
        total += new_value
        return total / count
        
    return averager
>>> avg = make_averager()
>>> avg(10)
Traceback (most recent call last):
...
UnboundLocalError: local variable 'count' referenced before assignment
>>>

这是因为对不可变类型进行修改Python会隐式的创建一个局部变量来保存修改后的值,比如上边的count += 1等价于count = count + 1而此时的count已经不再是*变量而是局部变量了,此时就会报错count未定义。

可以使用nonlocal关键字来声明为*变量。

def make_averager():
    count = 0
    total = 0
    
    def averager(new_value):
        nonlocal count, total
        count += 1
        total += new_value
        return total / count
    
    return averager

1.6 自定义装饰器

1.6.1 不带参数的装饰器

如果直接使用闭包的方式创建一个装饰器,将会遮盖被装饰函数的__name____doc__属性, 使用functools.wraps装饰器可以把属性从被装饰函数复制到闭包函数中。

如:自定义装饰器 :在每次调用被装饰的函数时计时,然后把经过的时间、
传入的参数和调用的结果打印出来。

import time
import functools

def clock(func):
    @functools.wraps(func)
    def clocked(*args, **kwargs):
        t0 = time.time()
        result = func(*args, **kwargs)
        elapsed = time.time() - t0
        name = func.__name__
        arg_lst = []
        if args:
       	 	arg_lst.append(', '.join(repr(arg) for arg in args))
        if kwargs:
       	 	pairs = ['%s=%r' % (k, w) for k, w in sorted(kwargs.items())]
        	arg_lst.append(', '.join(pairs))
        arg_str = ', '.join(arg_lst)
        print('[%0.8fs] %s(%s) -> %r ' % (elapsed, name, arg_str, result))
        return result
    return clocked

1.6.2 带参数的装饰器

带有参数的装饰器:

registry = set()

# 注意此时的register并不是一个真正的装饰器而是一个装饰器工厂函数,
# 调用它会返回真正的装饰器decorate
def register(active=True): 

    def decorate(func): 
    	# 可以在装饰器中接收active参数
        print('running register(active=%s)->decorate(%s)'
        % (active, func))
        if active: 
        	registry.add(func)
        else:
        	registry.discard(func) 
        return func 
        
    return decorate 
    
    
# register(active=False)并不是一个装饰f1函数的装饰器,
# 返回的结果是一个装饰器
@register(active=False) 
def f1():
	print('running f1()')

# 所以即使不传递参数也要加(),是因为这里是函数调用
@register() 
def f2():
	print('running f2()')
	
def f3():
	print('running f3()')

如果不使用@语法 ,那就要像常规函数那样使用 register;若想把 f添加到 registry中,则装饰f函数的句法是register()(f);不想添加(或把它删除)的话,句法是register(active=False)(f):

>>> from registration_param import *
running register(active=False)->decorate(<function f1 at 0x10073c1e0>)
running register(active=True)->decorate(<function f2 at 0x10073c268>)
>>> registry 
{<function f2 at 0x10073c268>}
>>> register()(f3) 
running register(active=True)->decorate(<function f3 at 0x10073c158>)
<function f3 at 0x10073c158>
>>> registry 
{<function f3 at 0x10073c158>, <function f2 at 0x10073c268>}
>>> register(active=False)(f2) 
running register(active=False)->decorate(<function f2 at 0x10073c268>)
<function f2 at 0x10073c268>
>>> registry 
{<function f3 at 0x10073c158>}

带有参数的装饰器,通常会把被装饰的函数替换掉,而且结构上需要多一层嵌套:因为利用的是工厂函数的设计模式。

1.7 标准库中的装饰器

functools模块中的三个装饰器:

  • functools.wraps:协助构建行为良好的装饰器

  • functools.lru_cache: 把耗时的函数的结果保存起来,避免传入相同的参数时重复计算 ,缓存不是永久缓存当一段时间不使用就会被扔掉。

    使用前边的clock装饰器来装饰一个斐波那契数函数:

    from clockdeco import clock
    
    @clock
    def fibonacci(n):
        if n < 2:
        	return n
        return fibonacci(n-2) + fibonacci(n-1)
    
    if __name__=='__main__':
    	print(fibonacci(6))
    
    $ python3 fibo_demo.py
    [0.00000095s] fibonacci(0) -> 0
    [0.00000095s] fibonacci(1) -> 1
    [0.00007892s] fibonacci(2) -> 1
    [0.00000095s] fibonacci(1) -> 1
    [0.00000095s] fibonacci(0) -> 0
    [0.00000095s] fibonacci(1) -> 1
    [0.00003815s] fibonacci(2) -> 1
    [0.00007391s] fibonacci(3) -> 2
    [0.00018883s] fibonacci(4) -> 3
    [0.00000000s] fibonacci(1) -> 1
    [0.00000095s] fibonacci(0) -> 0
    [0.00000119s] fibonacci(1) -> 1
    [0.00004911s] fibonacci(2) -> 1
    [0.00009704s] fibonacci(3) -> 2
    [0.00000000s] fibonacci(0) -> 0
    [0.00000000s] fibonacci(1) -> 1
    [0.00002694s] fibonacci(2) -> 1
    [0.00000095s] fibonacci(1) -> 1
    [0.00000095s] fibonacci(0) -> 0
    [0.00000095s] fibonacci(1) -> 1
    [0.00005102s] fibonacci(2) -> 1
    [0.00008917s] fibonacci(3) -> 2
    [0.00015593s] fibonacci(4) -> 3
    [0.00029993s] fibonacci(5) -> 5
    [0.00052810s] fibonacci(6) -> 8
    8
    

    因为是递归所以有很多参数相同结果相同的重复调用,导致程序运行时间长。

    使用 lru_cache 装饰后,性能会显著提升:

 import functools
 
 from clockdeco import clock
 
 # 注意这个装饰器使用必须带括号,因为functools.lru_cache()不是装饰器
 # 其返回的对象是个装饰器,这里只是函数调用
 @functools.lru_cache() 
 @clock 
 def fibonacci(n):
     if n < 2:
     	return n
     return fibonacci(n-2) + fibonacci(n-1)
 
 if __name__=='__main__':
 	print(fibonacci(6))
  $ python3 fibo_demo_lru.py
  [0.00000119s] fibonacci(0) -> 0
  [0.00000119s] fibonacci(1) -> 1
  [0.00010800s] fibonacci(2) -> 1
  [0.00000787s] fibonacci(3) -> 2
  [0.00016093s] fibonacci(4) -> 3
  [0.00001216s] fibonacci(5) -> 5
  [0.00025296s] fibonacci(6) -> 8

其中 lru_cache 可以使用两个可选的参数来配置 functools.lru_cache(maxsize=128, typed=False)

  • maxsize 参数指定存储多少个调用的结果。缓存满了之后,旧的结果会被扔掉,腾出空间。为了得到最佳性能, maxsize 应该设为 2 的幂。
  • typed 参数如果设为 True,把不同参数类型得到的结果分开保存,即把通常认为相等的浮点数和整数参数(如 1 和 1.0)区分开。

注:被lru_cache装饰的函数的参数必须是可散列的。