Python 闭包与装饰器
Synopsis: 在函数内部再定义一个函数,并且内部函数用到了外部函数作用域里的变量(enclosing),那么将这个内部函数以及用到的外部函数内的变量一起称为闭包(Closure)。装饰器(decorator)接受一个callable对象(可以是函数或者实现了__call__方法的类)作为参数,并返回一个callable对象,它经常用于有切面需求的场景,比如:插入日志、性能测试(函数执行时间统计)、事务处理、缓存、权限校验等场景。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码并继续重用
1. 闭包
Python中函数也是对象,允许把函数本身作为参数传入另一个函数,还可以把函数作为结果值返回
在函数内部再定义一个函数,并且内部函数用到了外部函数作用域里的变量(enclosing)
,那么将这个内部函数以及用到的外部函数内的变量一起称为闭包(Closure)
:
In [1]: def line(a, b): ...: def get_y_axis(x): ...: return a * x + b # 内部函数使用了外部函数的变量a和b ...: return get_y_axis # 返回值是闭包函数名,注意不是函数调用没有小括号 ...: ...: In [2]: L1 = line(1, 1) # 创建一条直线: y=x+1 In [3]: L1 Out[3]: <function __main__.line.<locals>.get_y_axis(x)> In [4]: L1(3) # 获取第一条直线中,横坐标是3时,纵坐标的值 Out[4]: 4 In [5]: L2 = line(2, 3) # 创建一条直线: y=2x+3 In [6]: L2 Out[6]: <function __main__.line.<locals>.get_y_axis(x)> In [7]: L2(3) # 获取第二条直线中,横坐标是3时,纵坐标的值 Out[7]: 9 In [8]: L3 = line(2, 3) # 再创建一条直线: y=2x+3 In [9]: L3 Out[9]: <function __main__.line.<locals>.get_y_axis(x)> In [10]: L2 == L3 # 每次调用line()返回的都是不同的函数,即使传入相同的参数 Out[10]: False In [11]: L2 is L3 Out[11]: False
注意: 闭包中不要引用外部函数中
任何循环变量
或后续会发生变化的变量
# 1. 错误的用法 def count(): fs = [] for i in range(1, 4): def f(): return i*i fs.append(f) return fs f1, f2, f3 = count() # f1()、f2()和f3()的输出结果都是9,原因是调用这三个函数时,闭包中引用的外部函数中变量i的值已经变成3 # 2. 正确的用法 def count(): def f(j): return lambda: j * j fs = [] for i in range(1, 4): fs.append(f(i)) # f(i)立刻被执行,因此i的当前值被传入闭包lambda: j * j return fs f1, f2, f3 = count() print(f1()) # 输出1 print(f2()) # 输出4 print(f3()) # 输出9
2. 装饰器
装饰器(decorator)
接受一个callable对象
(可以是函数或者实现了__call__
方法的类)作为参数,并返回一个callable对象
它经常用于有切面需求的场景,比如:插入日志、性能测试(函数执行时间统计)、事务处理、缓存、权限校验等场景。装饰器是解决这类问题的绝佳设计,有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码并继续重用。
举个实例,假设你写好了100个Flask中的路由函数,现在要在访问这些路由函数前,先判断用户是否有权限,你不可能去这100个路由函数中都添加一遍权限判断的代码(如果权限判断代码为5行,你得加500行)。那么,你可能想把权限认证的代码抽离为一个函数,然后去那100个路由函数中调用这个权限认证函数,这样只要加100行。但是,根据开放封闭
原则,对于已经实现的功能代码建议不能修改, 但可以扩展,因为可能你在这些路由函数中直接添加代码会导致原函数出现问题,那么最佳实践是使用装饰器
2.1 被装饰的函数无参数
如果原来是调用f1()、f2() ... ,我们只要让用户还是调用f1()、f2() ... ,即他们调用的函数名还是保持不变,但实际执行的函数体代码已经变了(Python中函数也是对象,函数名只是变量,可能改变它引用的函数体对象):
没有使用装饰器之前:
def f1(): print('function f1...') def f2(): print('function f1...') f1() # 输出function f1... f2() # 输出function f2...
创建装饰器,接收函数参数
,返回一个闭包
函数inner:
def login_required(func): def inner(): # inner是一个闭包,它使用了外部函数的变量func,即传入的原函数引用f1、f2... if func.__name__ == 'f1': # 这里是权限验证的逻辑判断,此处简化为只能调用f1 print(func.__name__, ' 权限验证成功') func() # 执行原函数,相当于f1()或f2()... else: print(func.__name__, ' 权限验证失败') return inner
使用装饰器:
def f1(): print('function f1...') def f2(): print('function f2...') new_f1 = login_required(f1) # 将f1传入装饰器,返回inner引用,并赋值给新的变量new_f1 new_f1() # 执行函数,即执行inner(),这个闭包中使用的func变量指向原f1函数体 new_f2 = login_required(f2) # 将f2传入装饰器,返回inner引用,并赋值给新的变量new_f2 new_f2() # 执行函数,即执行inner(),func变量指向原f2,所以它不会通过权限验证,即不会执行func() # 输出结果: f1 权限验证成功 function f1... f2 权限验证失败
上面使用装饰器有个问题,就是用户原来是调用f1()、f2()... ,现在你让他们调用new_f1()、new_f2()... , 这样肯定不行,所以需要修改如下:
f1 = login_required(f1) # 将f1引用传入装饰器,此时func指向了原f1函数体。返回inner引用,并赋值给f1,即现在是func指向原函数体,而f1重新指向了返回的inner闭包 f1() # 执行函数,即执行inner(),这个闭包中使用的func变量指向原f1函数体
上述两个步骤可以用 Python 的语法糖 @
简写为:
2.2 被装饰的函数有参数
def login_required(func): def inner(): if func.__name__ == 'f1': print(func.__name__, ' 权限验证成功') func() else: print(func.__name__, ' 权限验证失败') return inner @login_required def f1(a): print('function f1, args: a=', a) f1(10)
如果被装饰的函数有参数,调用f1(10)
此时实际调用的是inner(10)
,而装饰器中的闭包inner没有定义参数,所以会报错:
Traceback (most recent call last): File "test.py", line 14, in <module> f1(10) TypeError: inner() takes 0 positional arguments but 1 was given
那么如果我给inner定义一个形参呢?
def login_required(func): def inner(a): # inner定义了一个形参,名字随意 if func.__name__ == 'f1': print(func.__name__, ' 权限验证成功') func() else: print(func.__name__, ' 权限验证失败') return inner @login_required def f1(a): print('function f1, args: a=', a) f1(10)
还是会报错,原因是执行inner函数体时,当执行到func()
时,它指向传入装饰器的原函数f1的引用,而原f1需要一个位置参数:
0 条评论
评论者的用户名
评论时间暂时还没有评论.