目录
装饰器是 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