前言:本文不展示装饰器和闭包的基础语法,默认读者懂得装饰器和闭包的基础使用。文中的装饰器指的都是函数装饰器,本文不涉及类装饰器
函数是一等对象
要真正理解装饰器和闭包,你首先要明白,在python中函数并不只是一堆操作的集合,它首先是一个对象(Object),一个一等对象!
什么是一等对象:
-
在运行时创建(函数是在代码运行到 def 时才被创建出来的)。
-
能赋值给变量(f = my_func)。
-
能作为参数传给函数(sorted(data, key=len))。
-
能作为函数的返回值(闭包和装饰器的基础)。
闭包
变量作用域规则
要想深刻理解闭包,必须得搞清楚Python中的变量作用域规则。
-
L (Local) – 局部作用域: 当前函数内部。
-
(比如函数里定义的变量,或者是函数的参数)
-
-
E (Enclosing) – 闭包作用域: 外层嵌套函数的内部。
-
(这就是闭包发生的地方,闭包函数的外层函数定义的变量)
-
-
G (Global) – 全局作用域: 当前模块(文件)的顶层。
-
(比如你在 .py 文件最开头定义的变量)
-
-
B (Built-in) – 内置作用域: Python 自带的预定义变量。
-
(比如 len, print, 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("\\n— Test 2 —")
dangerous_operation()
可以看到这个带参数的装饰器,我们嵌套了三层,不要害怕,我们逐层解析。首先,我们在装饰的时候是这样使用的@logger(level="DEBUG")。我们其实调用了logger函数,并传入了level参数,logger函数返回了decorator函数,他接收一个函数作为参数,logger完成了接收参数的作用后,现在就相当于@decorator了。而decorator又返回了wrapper函数,我们在这个函数中调用了目标函数,并在前后打了日志,日志级别是根据最外层的logger来的。在logger中,decorator这个闭包,使用了logger的的局部作用域作为闭包作用域,而wrapper这个闭包又引用了decorator的局部作用域作为闭包作用域,所以一层一层传递,最终wrapper可以拿到level。
对装饰器的介绍就到这里,最后,读者可以尝试把我上面加强版的日志装饰器示例写成不要使用@语法糖而是原始闭包的形式。


