Python 闭包与装饰器

  • 原创
  • Madman
  • /
  • /
  • 0
  • 2089 次阅读

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 的语法糖 @ 简写为:

# 1. 定义时
@login_required
def f1():
    print('function f1...')

# 2. 调用时
f1()

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需要一个位置参数:

f1  权限验证成功
Traceback (most recent call last):
  File "test.py", line 14, in <module>
    f1(10)
  File "test.py", line 5, in inner
    func()
TypeError: f1() missing 1 required positional argument: 'a'

所以正确的做法是:

未经允许不得转载: LIFE & SHARE - 王颜公子 » Python 闭包与装饰器

分享

作者

作者头像

Madman

如需 Linux / Python 相关问题付费解答,请按如下方式联系我

0 条评论

暂时还没有评论.

专题系列