深入理解函数装饰器和闭包
前言:本文不展示装饰器和闭包的基础语法,默认读者懂得装饰器和闭包的基础使用。文中的装饰器指的都是函数装饰器,本文不涉及类装饰器
函数是一等对象
要真正理解装饰器和闭包,你首先要明白,在python中函数并不只是一堆操作的集合,它首先是一个对象(Object),一个一等对象!
什么是一等对象:
-
在运行时创建(函数是在代码运行到
def时才被创建出来的)。 -
能赋值给变量(
f = my_func)。 -
能作为参数传给函数(
sorted(data, key=len))。 -
能作为函数的返回值(闭包和装饰器的基础)。
闭包
变量作用域规则
要想深刻理解闭包,必须得搞清楚Python中的变量作用域规则。
L (Local) - 局部作用域: 当前函数内部。
(比如函数里定义的变量,或者是函数的参数)
E (Enclosing) - 闭包作用域: 外层嵌套函数的内部。
(这就是闭包发生的地方,闭包函数的外层函数定义的变量)
G (Global) - 全局作用域: 当前模块(文件)的顶层。
(比如你在
.py文件最开头定义的变量)B (Built-in) - 内置作用域: Python 自带的预定义变量。
(比如
len,dict等)- 查找原则:由内向外,找到即止。 如果在 Local 找到了,就不去 Enclosing 找了。如果找遍了 Built-in 还没找到,就报
NameError。- 坑点:在只读的场景下,就是按照L -> E -> G -> B的顺序去查找。但是要是一旦你做了赋值操作,比如只要你在当前作用域写了x = ...,Python就会认为你这个变量是这个作用域的私有财产,会无视外层作用域的x变量。
读取场景:
x = "Global" def outer(): x = "Enclosing" def inner(): # 这里没有赋值,只是读取 # 局部作用域没找到 -> 去闭包作用域找 -> 找到了! print(x) inner() outer() # 输出: Enclosing赋值场景:
x = "Global" def outer(): x = "Enclosing" def inner(): print(x) # 这一行报错! # 对x进行赋值 x = "Local" inner() outer()
- 读者运行一下上述代码可以看到报错了,这对于不熟悉python变量作用域的人来说是非常反直觉的,笔者在一开始也是感到震惊。
- 但是,这不是 bug,而是一种设计选择:Python 不要求声明变量,但是会假定在函数主体中赋值的变量是局部变量。
- 报错的行是print打印x这一行,而真正是原因是x = "Local"这一行,因为这一行赋值了,所以Python把x假定为inner函数的局部变量,不在会遵循上面的LEGB原则去一层一层查找,而是只在局部变量中找,找不到就报错!
我们要想让上述的示例不报错,你可以在inner函数里面定义一个变量x,也可以使用nonlocal声明x变量的作用域为闭包作用域。
x = "Global" def outer(): x = "Enclosing" def inner(): # 声明x的作用域为闭包作用域 nonlocal x print(x) # 对x进行赋值,在局部作用域中,但是修改的是闭包作用域的变量! x = "Local" inner() # 会看到inner的闭包作用域也即outer的局部作用域中的x被修改 print(x) outer()函数也有状态
在 Java 或 C++ 的传统思维里:
函数:只负责干活(逻辑)。算完
a+b就完了,除了返回值什么都不留。类(对象):才负责记事(状态)。
但是在Python中并非如此,函数不仅仅是逻辑(Logic),函数还可以拥有状态(State)。
通过一个示例来演示Python函数的状态,我们要实现一个可以不断根据输入的数字得出平均值的计算器。你会怎么设计?
在这个需求里面求平均值就是逻辑,而累加输入的值,就是状态。
用类思维去实现:
class Averager: def __init__(self): self.series = [] # 必须显式定义属性 def __call__(self, new_value): self.series.append(new_value) return sum(self.series) / len(self.series) # 使用时: avg = Averager() # 先实例化 print(avg(10)) print(avg(8)) print(avg(66))这样固然可以实现需求,但是一点都不Pythonic,在python中为了这一点点功能去写一个类,实现这么多丑陋的样板代码,有点杀鸡用牛刀了。
使用闭包实现:
def make_averager(): series = [] # 这里的 series 充当了 self.series 的角色 def averager(new_value): series.append(new_value) return sum(series) / len(series) return averager # 返回闭包 # 使用时: avg = make_averager() print(avg(10)) print(avg(8)) print(avg(66))可以看到,我们没有为了一点点功能就去大动干戈写了一个类,同时也完美地实现了我们的需求。从这里示例可以看出,闭包不仅仅是一段逻辑,还把外部函数的状态“打包”了。这个状态会一直跟随到闭包函数被销毁。
在 averager 函数中,series 是自由变量(free variable)。自由变量是一个术语,指未在局部作用域中绑定的变量。
装饰器
装饰器的基本概念:
装饰器是一种可调用对象,其参数是另一个函数(被装饰的函数)。
装饰器可能会对被装饰的函数做些处理,然后返回函数,或者把函数替换成另一个函数或可调用对象。
一个简单的示例:
def decorate(func): # 传入的func就是被装饰函数 # 对应我们上面讲到的的,函数这个对象能作为参数传给函数 print("decorate...") # 把函数返回,函数这个对象也能作为函数的返回值 return func # 装饰target函数 @decorate def target(): print('running target()') # 调用target,可以看到先打印了decorate再执行target target()装饰在函数上的@其实只是一个语法糖, 上面使用@decorate的代码和下面这段代码本质一样
def decorate(func): print("decorate...") # 传入的func就是被装饰函数 return func # @decorate def target(): print('running target()') # 上面@decorate装饰器的这个2语法糖实际上是帮我们完成了这一步。 target = decorate(target) # 调用target,可以看到先打印了decorate再执行target target()
Python 何时执行装饰器
装饰器是在 导入时 (Import Time) 执行的。
什么是导入时:
当 Python 解释器读取并加载你的.py模块时(无论是直接运行脚本,还是
import这个模块),它会从上到下扫描代码。 一旦它看到@decorator语法,它会立刻运行这个装饰器函数。它不会等到你调用那个函数时才运行装饰器,而是定义函数时就运行了。
registry = [] # 注册表 def register(func): # 这一步在导入时就会执行 print(f'==> 正在运行装饰器 register({func.__name__})') registry.append(func) return func # 必须返回函数,否则原函数就变成 None 了 @register # <--- 解释器读到这里,立刻执行 register(f1) def f1(): print('正在运行 f1()') @register # <--- 解释器读到这里,立刻执行 register(f2) def f2(): print('正在运行 f2()') print('收集到的被注册了的函数->', registry) # 调用f1,f2 f1() f2()通过上面的示例,可以知道装饰器是在导入时运行的,而不是等到被装饰函数被调用的时候再执行。
实现一个简单的日志打印装饰器
import functools def logger(func): # 这个装饰器用于保留原函数的元信息 @functools.wraps(func) def wrapper(*args, **kwargs): """ 这是一个闭包(内部函数)。 它捕获了外面的变量 'func'。 """ # --- A. 执行前的逻辑 --- print(f"🔔 [开始] 正在调用函数: {func.__name__}") print(f" 参数: args={args}, kwargs={kwargs}") # --- B. 执行原函数 --- # 这里的 func 是从闭包作用域里拿到的 result = func(*args, **kwargs) # --- C. 执行后的逻辑 --- print(f"✅ [结束] {func.__name__} 返回结果: {result}") print("-" * 30) # --- D. 返回结果--- return result # 返回这个“增强版”的新函数(闭包) return wrapper这是装饰器的典型行为:把被装饰的函数替换成新函数,新函数接受的参数与被装饰的函数一样,而且(通常)会返回被装饰的函数本该返回的值,同时还会做一些额外操作。
日志装饰器使用:
import functools def logger(func): # 这个装饰器用于保留原函数的元信息 @functools.wraps(func) def wrapper(*args, **kwargs): """ 这是一个闭包(内部函数)。 它捕获了外面的变量 'func'。 """ # --- A. 执行前的逻辑 --- print(f"🔔 [开始] 正在调用函数: {func.__name__}") print(f" 参数: args={args}, kwargs={kwargs}") # --- B. 执行原函数 --- # 这里的 func 是从闭包作用域里拿到的 result = func(*args, **kwargs) # --- C. 执行后的逻辑 --- print(f"✅ [结束] {func.__name__} 返回结果: {result}") print("-" * 30) # --- D. 返回结果--- return result # 返回这个“增强版”的新函数(闭包) return wrapper @logger def add(x, y): """简单的加法函数""" return x + y @logger def square(n): """简单的平方函数""" return n * n # --- 测试调用 --- # 此时的 add 实际上已经是 wrapper 了 print(add(10, 20)) print(square(n=5))
在目标函数上使用装饰器,前面说了@的写法只是语法糖而已,把它装饰在add上本质就是add = logger(add)。logger函数本质就是闭包,之前我们说过闭包还会把外部函数的状态也给打包了,记得自由变量吗?其实这就是带参数装饰器的本质。现在我们加强上面的日志装饰器,加入日志等级这个参数。
import functools import time def logger(level='INFO'): """ 【第一层】工厂函数 接收装饰器的参数 (level)。 返回一个装饰器函数。 """ def decorator(func): """ 【第二层】装饰器函数 接收被装饰的函数 (func)。 此时,它能看到外面的 'level'。 """ @functools.wraps(func) def wrapper(*args, **kwargs): """ 【第三层】包装函数 最终执行逻辑的地方。 它背着两个背包: 1. level (来自第一层) 2. func (来自第二层) """ # --- 模拟日志格式 --- log_msg = f"[{level}] calling {func.__name__}(args={args}, kwargs={kwargs})" print(log_msg) # 执行原函数 start_time = time.time() result = func(*args, **kwargs) end_time = time.time() print(f"[{level}] {func.__name__} returned {result} (cost {end_time - start_time:.4f}s)") return result return wrapper return decorator加强版日志装饰器使用:
import functools import time def logger(level='INFO'): """ 【第一层】工厂函数 接收装饰器的参数 (level)。 返回一个装饰器函数。 """ def decorator(func): """ 【第二层】装饰器函数 接收被装饰的函数 (func)。 此时,它能看到外面的 'level'。 """ @functools.wraps(func) def wrapper(*args, **kwargs): """ 【第三层】包装函数 最终执行逻辑的地方。 它背着两个背包: 1. level (来自第一层) 2. func (来自第二层) """ # --- 模拟日志格式 --- log_msg = f"[{level}] calling {func.__name__}(args={args}, kwargs={kwargs})" print(log_msg) # 执行原函数 start_time = time.time() result = func(*args, **kwargs) end_time = time.time() print(f"[{level}] {func.__name__} returned {result} (cost {end_time - start_time:.4f}s)") return result return wrapper return decorator # 场景 1: 使用 DEBUG 级别 @logger(level="DEBUG") def calculate_sum(a, b): return a + b # 场景 2: 使用 WARNING 级别 (注意:即使是用默认值,也得加括号 @logger()) @logger(level="WARNING") def dangerous_operation(): return "Boom!" # --- 测试 --- print("--- Test 1 ---") calculate_sum(10, 20) print(" --- Test 2 ---") dangerous_operation()可以看到这个带参数的装饰器,我们嵌套了三层,不要害怕,我们逐层解析。首先,我们在装饰的时候是这样使用的@logger(level="DEBUG")。我们其实调用了logger函数,并传入了level参数,logger函数返回了decorator函数,他接收一个函数作为参数,logger完成了接收参数的作用后,现在就相当于@decorator了。而decorator又返回了wrapper函数,我们在这个函数中调用了目标函数,并在前后打了日志,日志级别是根据最外层的logger来的。在logger中,decorator这个闭包,使用了logger的的局部作用域作为闭包作用域,而wrapper这个闭包又引用了decorator的局部作用域作为闭包作用域,所以一层一层传递,最终wrapper可以拿到level。
对装饰器的介绍就到这里,最后,读者可以尝试把我上面加强版的日志装饰器示例写成不要使用@语法糖而是原始闭包的形式。








