第7章 函数装饰器和闭包

67 阅读3分钟

函数装饰器和闭包

装饰器基础知识

  • 装饰器是可调用的对象,其参数是另一个函数(被装饰的函数)。 装饰器可能会处理被装饰的函数,然后把它返回,或者将其替换成另一个函数或可调用对象。

    @dacorate
    def target():
      print("running target()")
    
    def target():
      print("running target()")
    target = decorate(target)
    
  • 上述两种写法的最终结果一样,但执行完毕后得到的target不一定是原来那个target函数,而是decorate(target)返回的函数。严格来说装饰器只是语法糖。其可以像常规的可调用对象那样调用,其参数是另一个函数。其一大特性是能把被装饰的函数替换成其他函数。第二个特性是,装饰器在加载模块时立即执行,并且函数装饰器在导入模块时立即执行,而被装饰的函数只有在明确调用时运行。

    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()
        
    running register(<function f1 at 0x107bfb200>)
    running register(<function f2 at 0x107bfb3b0>)
    running main()
    registry -> [<function f1 at 0x107bfb200>, <function f2 at 0x107bfb3b0>]
    running f1()
    running f2()
    running f3()
    

    变量作用域规则

    Python不要求声明变量,但是假定在函数定义体中赋值的变量是局部变量。

    >>> b = 6
    >>> def f2(a):
    ... print(a)
    ... print(b)		// 在编译函数的定义体时,判断b为局部变量,但是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
      
    In [4]: b = 6                                                                                            
    In [5]: def f3(a): 
       ...:     global b 
       ...:     print(a) 
       ...:     print(b) 
       ...:     b = 9                                                                                                 
    In [6]: f3(3)                                                                                            
    3
    6
    In [7]: b                                                                                                
    Out[7]: 9
    

    闭包

    闭包指延伸了作用域的函数,其中包含函数定义体中的引用,但不是在定义体中定义的非全局变量。它会保留定义函数时存在的自由变量的绑定,这样调用函数时,虽然定义作用域不可用了,但是仍能使用那些绑定。

    在此提到一个新名词:自由变量。自由变量有别于局部变量和全部变量,它指未在本地作用域中绑定的变量。在嵌套函数中使用了上级函数的局部变量,但并没有绑定新的值,那么这个变量便会成为自由变量。

    自由变量绑定新值变为局部变量后可以使用nonlocal再次标记为自由变量。

    自由变量

装饰器

一个简陋的装饰器

import time

def clock(func):
    def clocked(*args):
        t0 = time.perf_counter()
        result = func(*args)   #此处func为自由变量,并且为绑定新值,形成闭包。
        elapsed = time.perf_counter() - t0
        name = func.__name__
        arg_str = ', '.join(repr(arg) for arg in args)
        print('[%0.8fs] %s(%s) -> %r' % (elapsed, name, arg_str, result))
        return result
    return clocked

@clock
def snooze(seconds):
    time.sleep(seconds)
    
@clock
def factorial(n):
    return 1 if n < 2 else n * factorial(n - 1)
# 等价于
# def factorial(n):
#   	return 1 if n < 2 else n * factorial(n - 1)
# factorial = clock(factorial)
# 所以说现factorial函数不再是原来的factorial函数,__name__,__doc__属性也发生改变。
# 并且这样写我们不能传入关键字参数。
  
print('*' * 40, 'Calling snooze(.123)')
snooze(.123)
print('*' * 40, 'Calling factorial(6)')
print('6! =', factorial(6))
print(factorial.__name__)

**************************************** Calling snooze(123)
[0.12405610s] snooze(.123) -> None 
**************************************** Calling factorial(6)
[0.00000191s] factorial(1) -> 1
[0.00004911s] factorial(2) -> 2
[0.00008488s] factorial(3) -> 6
[0.00013208s] factorial(4) -> 24
[0.00019193s] factorial(5) -> 120
[0.00026107s] factorial(6) -> 720
6! = 720
clocked

改造后的装饰器

import time 
import functools

def clock(func):
    @functools.wraps(func) #该装饰器可以把相关的属性从func复制到clocked中
    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

@clock
def factorial(n):
    return 1 if n < 2 else n * factorial(n - 1)
# 等价于
# clocked = functools.wraps(clocked)
# factorial = clock(factorial)