装饰器与闭包

变量作用域

在完成一个题目:利用带参数的装饰器限制函数执行的次数
我遇到一个很疑惑的问题,可以简化如下:

>>>a = 1
def test():
    print(a)
>>>test()
1

然而,当改为如下形式

>>>a = 1
def test():
    print(a)
    a -= 1
>>>test()
1

会报错 UnboundLocalError: local variable 'a' referenced before assignment
这个问题实际上是变量作用域的原因,python编译函数的定义体时,由于a的运算操作判断a为局部变量,而在运行时,尝试获取局部变量a,发现a没有值。
可以用global将a转为全局变量

>>>a = 1
def test1():
    global a
    print(a)
    a-= 1
>>>test1()
1
>>>a
0

闭包

闭包指延伸了作用域的函数。其中包含函数定义体中引用、但是不在定义体中定义的非全局变量。函数是不是匿名的没有关系,关键是它能访问定义体之外定义的非全局变量。
假如有个名为 avg 的函数,它的作用是计算不断增加的系列值的均值。例如,整个历史中某个商品的平均收盘价。每天都会增加新价格,因此平均值要考虑至目前为止所有的价格。
起初,avg 是这样使用的:

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

我们可以用类实现

class Averager():
    def __init__(self):
        self.series = []
    def __call__(self, new_value):
        self.series.append(new_value)
        total = sum(self.series)
        return total/len(self.series)
    
>>>avg = Averager()
>>>avg(10)
10.0
>>>avg(11)
10.5
>>>avg(12)
11.0

也可以用闭包来实现

def outer():
    series = []
    def avg(new_value):
        series.append(new_value)
        total = sum(series)
        return total/len(series)
    return avg
>>>avg = outer()
>>>avg(10)
10.0
>>>avg(11)
10.5
>>>avg(12)
11.0

两种方式都是通过调用Averager()outer得到一个可调用对象avg,计算均值。
关于第二种实现,我们知道outer已经执行完成,也不存在本地作用域,那么series如何保存的?
实际上在outer函数中,series是自由变量(free variable),指未在本地作用域绑定的变量,series绑定到了创建的函数的__closure__ 属性中。avg.__closure__中的各个元素cell对应于avg.__code__.co_freevars 中的一个名称,每个cellcell_contents即为对应的内容。

>>>avg.__code__.co_freevars
('series',)
>>>avg.__code__.co_varnames
('new_value', 'total')
>>>avg.__closure__
(<cell at 0x0000026CBAC173D0: list object at 0x0000026CBF3EBC80>,)
>>>type(avg.__closure__[0])
<class 'cell'>
>>>avg.__closure__[0].cell_contents
[10, 11, 12]

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

nonlocal

前面实现求均值的效率并不高,我们只存储总值和个数

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

这是因为,当counttotal为不可变类型或者数字时 (list和dict引用时不会创建局部变量),count += 1相当于count = count+1,会隐式创建局部变量counttotal也是如此。

为了避免创建局部变量,py3提供了nonlocal将变量标记为自由变量。如果为 nonlocal 声明的变量赋予新值,闭包中保存的绑定会更新。

def outer():
    total = 0.0
    count = 0
    def avg(new_value):
        nonlocal count, total
        total += new_value
        count += 1
        return total/count
    return avg

py2因为没有nonlocal,可以采用dict保存变量:

def outer():
    d = {'total': 0.0, 'count': 0}
    def avg(new_value):
        
        d['total'] += new_value
        d['count'] += 1
        return d['total'] / d['count']
    return avg

十分感谢 python 装饰器(二):装饰器基础(二)变量作用域规则,闭包,nonlocal声明

装饰器-限制函数执行次数

最后,回到我刚开始的问题,利用装饰器限制函数执行的次数:

def limit_func(num):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            nonlocal num
            if num > 0:
                num -= 1
                return func(*args, **kwargs)
            else:
                raise TypeError('%s 执行超过限制' % func.__name__)
        return wrapper
    return decorator

py2

def limit_func(num):
    def decorator(func):
        d = {func.__name__: num}
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            if d[func.__name__] > 0:
                d[func.__name__] -= 1
                return func(*args, **kwargs)
            else:
                raise TypeError('%s 执行超过 %d 次' % (func.__name__, num))
        return wrapper
    return decorator

装饰器的原理

看下面例子:

def limit_func(num):
    def decorator(func):
        print('deco')
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            nonlocal num
            if num > 0:
                num -= 1
                return func(*args, **kwargs)
            else:
                raise TypeError('%s 执行超过规定次数' % func.__name__)
        return wrapper
    return decorator


@limit_func(2)
def test():
    print(1)
    
deco
>>>test.__code__.co_freevars
('func', 'num')
>>>test.__closure__[1].cell_contents
2
>>>test()
1
>>>test()
1
>>>>print(test.__name__)
test

在定义test函数时,装饰器函数已经执行,并将变量num写入wrapper的自由变量中,可以看出,返回的是wrapper函数,这里实际上将test函数转为wrapper函数,并修改了wrapper函数的名字为test,后续的执行也是wrapper函数。
装饰器在函数的执行流程limit_func(num)(func)(*args, **kwargs)

你可能感兴趣的:(python,python)