装饰器与闭包不为人知的一面

目录

Python装饰器在何时被执行

变量作用域规则

闭包

nonlocal声明

变量查找逻辑

实现一个简单的装饰器


 

装饰器是 Python 中一种强大的工具,它允许你在不修改原始函数代码的情况下,动态地修改或扩展函数的行为。装饰器通常用于修改函数的行为、添加额外的功能、验证输入、记录日志等。

装饰器是一种可调用对象,它接受一个函数作为参数,并返回一个新的函数或可调用对象。装饰器通常会在调用原始函数之前或之后执行一些额外的代码。

也就是说:

# 写法1
@decorate
def f():
    print('running f()')
# 写法2
def f():
    print('running f()')
f = decorate(f)

上述写法1和写法2完全是等价的。f名称绑定到decorate(f)返回的函数,f可能是原来那个名为f函数,也可能是另一个新的函数。

严格来讲,装饰器只是语法糖。装饰器有3个基本性质:

  • 装饰器是一个函数或者其他可调用对象
  • 装饰器可以把被装饰的函数替换成别的函数
  • 装饰器在加载模块时立即执行

Python装饰器在何时被执行

上面大概介绍了装饰器的前面两个性质,现在重点讲解第三点,Python何时执行装饰器:装饰器在被装饰的函数定义之后立即执行,通常在导入时(即加载模块时)。以下面代码为例:

decorator_func = []

def decorate(func):
    print(f'运行被装饰函数:{func}')
    decorator_func.append(func)
    def inner():
        func()
        return func
    return inner

@decorate
def f1():
    print('运行f1')

@decorate
def f2():
    print('运行f2')

def f3():
    print('运行f3')

if __name__ == '__main__':
    print('运行main函数')
    print(f'被装饰函数输出:{decorator_func}')
    f1()
    f2()
    f3()

输出结果:

运行被装饰函数:<function f1 at 0x000001BDD50A60C0>
运行被装饰函数:<function f2 at 0x000001BDD518CAE0>
运行main函数
被装饰函数输出:[<function f1 at 0x000001BDD50A60C0>, <function f2 at 0x000001BDD518CAE0>]
运行f1
运行f2
运行f3

decorator_func保存的是被装饰函数的引用。从输出结果可以看出,main里面的print('运行main函数')和print(f'被装饰函数输出:{decorator_func}')并没有先依次输出,而是在装饰器里的print(f'运行被装饰函数:{func}')以及append行为先于main执行了两次(因为被装饰了两个函数f1和f2)。

上述示例主要想强调,函数装饰器在导入模块时立即执行,而被装饰的函数只在显示调用时运行。

变量作用域规则

这部分直接通过代码的形式来讲解,我们先定义一个函数,如下所示:

def f(a):
    print(a)
    print(b)
f(1) # NameError: name 'b' is not defined

很明显,就算你是刚学Python的小白也可能明白会出错,但是我们给b设置为全局变量且赋值,那上面的代码就会正常运行。

b=3
def f(a):
    print(a)
    print(b)
f(1) # 输出1,3

那我们再看下面的代码,在函数f内再给b赋值,你就会发现神奇之处,程序出错啦!

b = 3
def f(a):
    print(a)
    print(b)
    b = 6

f(1) # UnboundLocalError: cannot access local variable 'b' where it is not associated with a value

是不是很惊讶,明明b是全局变量,而且局部变量b是在print(b)以后才赋值的,还是会出错。可事实就摆在眼前,我们来探究其中的原因,事实上是Python编译函数主体时,判断b是局部变量,因为在函数内给它赋值了,所以,Python会尝试从局部作用域中获取b,后面调用f(1)时,f函数的主体顺利获取并打印局部变量a的值,但是在尝试获取局部变量b的值时,发现b并没有绑定值。

这并不是bug,而是一种设计选择:Python不要求声明变量,但是会假定在函数主体中赋值的变量是局部变量。这比javaScript的行为好多了,JavaScript也不要求声明变量,但是如果忘记把变量声明为局部变量,则可能会在不知情的情况下破坏全局变量。

在函数中赋值,如果想让解释器把b当成全局变量,为它分配一个新值,就要使用global声明:

b = 3

def f(a):
    global b
    print(a)  # 1 
    print(b)  # 3
    b = 6
    print(b)  # 6

f(1)

闭包

闭包是指在一个函数内部定义的函数,并且内部函数引用了外部函数的变量。其实闭包就是延伸了作用域的函数,包括函数主体中引用的非全局变量和局部变量。闭包的关键在于能不能访问主体之外定义的非全局变量。

文本描述可能有点抽象,下面直接看代码,我有一个需求,计算不断增加的数据的平均值,这些数据是不断增加的,每增加一个我都要重新计算平均值。那么问题来了,我们怎么保存历史数据呢。初学者可能会这样编写代码,基于类的实现。

class Averager:
    def __init__(self):
        self.goods = []

    def __call__(self, new_goods):
        self.goods.append(new_goods)
        return sum(self.goods) / len(self.goods)

avg = Averager()
print(avg(10))  # 10.0
print(avg(11))  # 10.5
print(avg(12))  # 11.0

或者是下面基于函数的实现

def make_averager():
    goods = []

    def averager(new_goods):
        goods.append(new_goods)
        return sum(goods) / len(goods)

    return averager

avg = make_averager()
print(avg(10))  # 10.0
print(avg(11))  # 10.5
print(avg(12))  # 11.0

上面代码基于类实现和基于函数的实现有异曲同工之处。但是基于类的实现,avg在哪里存储历史值很明显:实例属性self.goods。但是基于函数的实现中在哪里寻找goods值呢?goods值是make_averager的局部变量,调用avg(10)时,make_averager函数已经返回,局部作用域也应该会销毁啊。

没有被销毁原因是在average函数中,goods是自由变量,自由变量是一个术语,指未在局部作用域中绑定的值。

闭包是一个函数,它保留了定义函数时存在的自由变量的绑定,如此一来,调用函数时,虽然作用域不可以了,但是仍能使用那些绑定。

nonlocal声明

前面实现求平均值的效率并不高,我们需要把所有值存储到队列中,如果每次调用averager时使用sum求和。更好的方法时只存储目前的总和和项数,根据这两个计算平均值。

def make_averager():
    count, total = 0, 0

    def averager(new_goods):
        count += 1
        total += new_goods
        return total / count

    return averager

avg = make_averager()
print(avg(10))  # UnboundLocalError: cannot access local variable 'count' where it is not associated with a v

哈哈,报错了,为什么呢,明明和上面的差不多啊。原因是对于数值和不可变类型,count+=1的语句相当于count=count+1,因此,实际上我们在averager的主体中为count赋值了,这会把count变成局部变量,total变量也是一样的受影响,

但是为什么上面使用列表没有出现这个问题呢?因为我们没有给列表goods赋值,只是调用goods.append,并没有把他传给sum和len。也就是说,我们利用了“列表是可变对象”这一事实。

但是数值、字符串、元组等不可变类型只能读取,不能更新。如果像count=count+1这样尝试重新绑定,则会隐式创建局部变量count。如此一来,count就不在是自由变量了,因此不会保存到闭包中。

为了解决这个问题,Python3引入了关键字nonlocal,它的作用就是把变量标记为自由变量。如果为nonlocal声明的变量赋予新值,那么闭包中保存的绑定也会随之更新,例如下面的示例。 

def make_averager():
    count, total = 0, 0

    def averager(new_goods):
        nonlocal count, total
        count += 1
        total += new_goods
        return total / count

    return averager

avg = make_averager()
print(avg(10))  # 10.0
print(avg(11))  # 10.5
print(avg(12))  # 11.0

变量查找逻辑

  • 如果是global x声明,则x来自全局作用域,并赋予那个作用域中x的值
  • 如果是nonloacl x声明,则x来自最近一个定义它的外层函数,并赋予那个函数中局部变量x的值
  • 如果x是参数,或者在函数主体中赋值,那么x是局部变量
  • 如果引用了x,但是没有赋值也不是参数,则遵循以下规则:
    • 在外层函数主体的局部作用域内查找
    • 如果在外层作用域内未找到,则从模块全局作用域内读取
    • 如果模块全局作用域内未找到,则从__builtins__.__dict__中读取

实现一个简单的装饰器

import time

def clock(func):
    def clocked(*args):
        t0 = time.perf_counter()
        result = func(*args)
        elapsed = time.perf_counter() - t0
        name = func.__name__
        arg_str = ', '.join(repr(arg) for arg in args)
        print(f'[{elapsed:0.8f}s] {name}({arg_str}) ->{result!r}')
        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)

if __name__ == '__main__':
    print('*' * 40, 'Calling snooze(.123)')
    snooze(.123)
    print('*' * 40, 'Calling factorial(6)')
    print('6! =', factorial(6))

上面定义的装饰器用于在每次调用被装饰的函数时计时,把运行时间、传入的参数和调用的结果打印出来。

但是上面装饰器有几个缺点:不支持关键字参数,而且遮盖了被装饰函数的__name和__doc__属性。下面的装饰器是一个通用的装饰器示例。

def clock(func):
    @functools.wraps(func)
    def clocked(*args, **kwargs):
        t0 = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - t0
        name = func.__name__
        arg_lst = [repr(arg) for arg in args]
        arg_lst.extend(f'{k}={v!r}' for k, v in kwargs.items())
        arg_str = ', '.join(arg_lst)
        print(f'[{elapsed:0.8f}s] {name}({arg_str}) ->{result!r}')
        return result
    return clocked

 

 

 

  • 17
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值