有些人踌躇满志想学Python,
而我劝退他们的方式,就是丢给他们一份介绍装饰器的文档。
注:完整目录、以及以下演示的所有代码,可在此GitHub仓库中找到。
使用Google的Colab运行jupyter notebook或许是个不错的选择,Colab给的CPU时间还是很宽裕的(每天最多12小时?)。Colab支持直接打开GitHub仓库,无需手动拉取。
https://github.com/PythonNotebook/Notebook
闭包
闭包,指的是延伸了作用域的函数。其中包含函数定义体中引用、但是不在定义体中定义的非全局变量。
通过闭包,我们可以创建一个特别的函数对象,它不仅有函数本身的定义,还有函数被创建时的环境。
这是一个基本的闭包示例:
def outer_function(x):
def inner_function(y):
return x + y
return inner_function
closure = outer_function(10)
print(closure(5)) # 15
print(closure(10)) # 20
通过这个例子,我们可以看到,x=10
这个值在 closure
被创建后并没有消失,而是被保存在了 closure
中。
因此,“保持状态”是闭包的一个重要特性。利用这个性质可以实现一个简单的计数器:
def counter():
count = 0
def inner():
nonlocal count
count += 1
return count
return inner
c = counter()
print(c()) # 1
print(c()) # 2
这个例子中,count
被保存在了 inner
函数中,每次调用 inner
,count
都会更新。
Python的垃圾回收机制:Python通过引用计数机制来追踪对象的引用数。如果引用数不为0,说明对象仍然被引用,Python的垃圾回收机制就不会销毁这个对象;否则,垃圾回收机制会认为这是一个垃圾对象,并且在合适的时机将其销毁(不一定立即销毁)。
闭包能够保持状态,也与Python垃圾回收机制中的引用计数机制有关。例如,当我们执行 c = counter()
时,Python解释器会在 counter()
函数的作用域中创建一个 count
变量,然后将 inner
函数绑定到这个 count
变量上。
此时,c
保持着对 inner
函数的引用,而 inner
函数又保持着对 count
变量的引用,因此,这个作用域中的 count
变量的引用计数不会归零,对象不会被销毁。
工厂函数
工厂函数是一种返回函数的函数,它可以动态地创建函数。工厂函数是闭包的一个重要应用场景。
案例1:日志封装
由于不同级别日志的区别仅是保存位置的不同,
因此可以创建一个自定义的方法,来记录不同级别的日志:
def create_logger(log_file):
def logger(message):
with open(log_file, 'a') as file:
file.write(message + '\n')
return logger
error_logger = create_logger('error.log')
event_logger = create_logger('event.log')
error_logger('Error occurred: Division by zero')
event_logger('User "admin" logged in')
类似的,你可以创建更多个长期生效、互不干扰的 create_logger()
实例,每个实例都维护着自己作用域内的 log_file
变量。
案例2:面积计算器
class Circle:
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14 * self.radius ** 2
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
def create_shape(shape_type, *args, **kwargs):
if shape_type == 'circle':
return Circle(*args, **kwargs)
elif shape_type == 'rectangle':
return Rectangle(*args, **kwargs)
else:
raise ValueError('Invalid shape type')
circle = create_shape('circle', 5)
print(circle.area())
rectangle = create_shape('rectangle', 3, 4)
print(rectangle.area())
在这个例子中,create_shape()
是一个工厂函数,它能根据传入的 shape_type
参数动态地调度需要的函数。这种方式可以让我们在尽可能不修改原有代码的情况下,轻松地复用现有的代码,创建更多方法。
装饰器
学Python不会用装饰器,就像读大学不读宁波工程学院。
——沃兹基硕德
指正:Copilot说的
装饰器是Python中的一个极为重要的概念,它是一种高级函数,也是闭包的终极应用之一。
装饰器的本质是一个闭包,它把一个函数作为输入并返回经过包装的函数。这种设计使得我们可以简单地将某个功能应用于多个函数上。
装饰器将被装饰的函数作为参数整个传入装饰器函数,然后返回一个新的函数,这个函数通常在原函数前后实现一些操作,而后调用原函数并返回其结果。
例如,可以定义一个装饰器,来记录函数的执行时间:
import time
def benchmark(func):
def wrapper(*args, **kwargs):
start = time.perf_counter()
return_value = func(*args, **kwargs)
end = time.perf_counter()
print(f'函数 {func.__name__} 运行耗时 {end - start:.2f} 秒')
return return_value
return wrapper
@benchmark
def count_prime_number(n):
count = 0
for num in range(2, n + 1):
prime = True
for i in range(2, num):
if num % i == 0:
prime = False
break
if prime:
count += 1
return count
count_prime_number(10000)
这个例子中,benchmark
是一个装饰器,它返回的函数 wrapper
调用了原函数 count_prime_number
,并在前后分别获取时间,以计算整个函数的运行时间。
如果不使用装饰器,那么上面的代码会变成这样:
import time
def count_prime_number(n):
count = 0
for num in range(2, n + 1):
prime = True
for i in range(2, num):
if num % i == 0:
prime = False
break
if prime:
count += 1
return count
def benchmark(func, *args, **kwargs):
start = time.perf_counter()
return_value = func(*args, **kwargs)
end = time.perf_counter()
print(f'函数 {func.__name__} 运行耗时 {end - start:.2f} 秒')
return return_value
benchmark(count_prime_number, 10000)
显然使用装饰器的代码更加简洁、易读且可拓展。
装饰器还可以带参数(参见Flask神教)。
例如,以下示例代码展示了一个带参数的装饰器,来将不同的命令分配给不同函数:
def cmd_register():
commands = {}
def register(name):
def wrapper(func):
commands[name] = func
return func
return wrapper
def run(name, *args, **kwargs):
return commands[name](*args, **kwargs)
return register, run
command, runner = cmd_register()
@command('add')
def add(a, b):
return a + b
@command('sub')
def sub(a, b):
return a - b
print(runner('add', 1, 2))
print(runner('sub', 3, 1))
你应该已经注意到了,上述代码中不仅仅有 register
这个装饰器,cmd_register
函数本身也利用了闭包的特性,它在函数的作用域范围内维护了一个名为 commands
的字典,用于保存命令和函数的映射关系。也就是说,这里的装饰器与上文的例子不同。它并不是直接执行函数,而是注册函数,等待调用。
此外,cmd_register
一次性返回了 register
和 run
两个函数,分别用于注册和执行命令,它们共享了上级作用域中的 commands
对象。
因此,你可以通过 command
装饰器快捷地注册命令和函数的映射关系,然后通过 runner
函数来执行这些命令。
个人认为上面的例子已经比较复杂了,如果能够完全理解,说明你对Python的函数对象的特性已经有了较深的理解,不再浮于命令式编程的阶段。
但是,这还没有结束,
我相信你已经准备好迎接挑战了,真正的挑战(感谢我吧,我可是写上注释了):
from numbers import Number
def cmd_register():
commands = {}
def register(name):
def wrapper(func):
commands[name] = func
return func
return wrapper
def run(name, *args, **kwargs):
return commands[name](*args, **kwargs)
return register, run
def checker(args_checker, kwargs_checker):
# 传入的两个检查器是两个函数,分别检查args和kwargs;如果是Bool类型-> True表示始终通过检查,False表示不能有参数
def wrapper(func):
def inner(*args, **kwargs):
for val, check in zip([args, kwargs], [args_checker, kwargs_checker]):
if check is True:
continue
elif check is False and len(val) > 0:
raise ValueError(f"Argument error in function {func.__name__}")
elif not isinstance(check, bool) and not check(val):
raise ValueError(f"Argument check failed in function {func.__name__}")
return func(*args, **kwargs)
return inner
return wrapper
command, runner = cmd_register()
@command('add')
# 这里的匿名函数检查条件:传入参数都是Number类型
@checker(lambda x: all(isinstance(i, Number) for i in x), False)
def add(*args):
return sum(args)
@command('dev')
# 这里的匿名函数检查条件:传入参数都是Number类型,且只有两个参数,且第二个参数不能为0
@checker(lambda x: all(isinstance(i, Number) for i in x) and len(x) == 2 and x[1]!=0, False)
def dev(*args):
return args[0] / args[1]
if __name__ == '__main__':
print(runner('add', 1, 2))
print(runner('dev', 3, 1))
这个例子包含了:
- 装饰器套娃
- 闭包
- 装饰器
- 装饰器带参数
- 闭包加装饰器参数套娃
- 匿名函数
- 可变参数
- 可变参数解包
- 匿名函数处理可变参数解包