首页 › 论坛 › 置顶 › 《深入探索Python中的作用域、闭包与装饰器》
-
作者帖子
-
2025-08-29 10:33 #25804Q QPY课程团队管理员
大多数Python教程解释了作用域、闭包和装饰器是什么。
今天,我们不打算进行枯燥的讲座,而是要偷偷溜进后台🎭,窥探Python的引擎室🛠️,以一种能够真正留在你脑海中的方式揭示这些概念背后的魔力。
到最后,你会明白为什么Python感觉像是一种扭曲现实的语言,并且你再也不会以同样的方式看待装饰器。
准备好你的爆米花🍿,让我们开始吧。
🎯 第1部分:作用域,变量的藏宝图
把Python想象成一座巨大的豪宅🏰。每个房间(函数/模块)都有装满贵重物品(变量)的抽屉。
当你请求
x
时,Python会按照以下顺序搜索抽屉:- 局部(L) → 你的房间(当前函数)。
- 封闭(E) → 包含当前函数的任何外部函数。
- 全局(G) → 整个豪宅(模块级命名空间)。
- 内置(B) → 外部世界(Python的内置函数,如
len
、print
)。
这被称为LEGB规则。
👉 示例:
x = "Global" def outer(): x = "Enclosing" def inner(): x = "Local" print(x) inner() outer()
输出:
本地
在幕后,Python 构建了一个 作用域链:
内部局部变量 → 外部局部变量 → 模块全局变量 → 内置函数
如果在这些抽屉中找不到名字呢?💥
NameError
。所以当你遇到
NameError
时,想象一下 Python 疯狂地在大厦里奔跑,喊道:“我发誓我检查了每一个抽屉……你的袜子不见了!” 🧦
🔄 第二部分:
nonlocal
,变量借用通常,在函数内部赋值会创建一个新的局部抽屉。
但有时你并不想要一个新的,你想借用隔壁房间爸爸的抽屉。这就是
nonlocal
的作用。👉 示例:
def outer(): message = "Hello" def inner():
nonlocal message message = "来自内部的问候" inner() print(message) outer()
输出:
来自内部的问候
如果没有
nonlocal
,Python 会认为你在inner
中定义了一个全新的变量。而使用nonlocal
,你实际上是在说:“不,Python,使用旧的抽屉。我只是借用它。”
🪢 第三部分:闭包,函数的时间胶囊
闭包就像时间胶囊或背包 🎒。即使一个函数结束,它的内部函数仍可以永远携带一些变量。
👉 示例:
def make_multiplier(n):
def multiplier(x): return x * n return multiplier times3 = make_multiplier(3) times5 = make_multiplier(5) print(times3(10)) # 30 print(times5(10)) # 50
即使
make_multiplier
已经运行结束,times3
和times5
仍然记得它们的n
。这是怎么做到的呢?闭包!
🧬 秘密:
__closure__
和cell
对象闭包不仅仅是神奇地“记住”变量,它们实际上是将变量存储在单元格中。
👉 示例:
def outer(): x = 42 def inner(): return x return inner func = outer()
print(func()) # 42
让我们窥探一下幕后:
print(func.__closure__) print(func.__closure__[0].cell_contents)
这意味着什么?
-
__closure__
→ 一个单元对象的元组。
-
- 每个单元就像一个装着变量的玻璃罐 🫙。
-
cell_contents
→ 里面的实际内容(这里是42
)。
闭包字面上是携带变量罐的函数。
多个单元示例
def outer(): a = 10 b = 20 def inner(): return a + b return inner func = outer()
for i, cell in enumerate(func.__closure__): print(f"单元格 {i}: {cell.cell_contents}")
输出:
单元格 0: 10 单元格 1: 20
因此,闭包就像是带着两个罐子 🫙🫙 一个装着
a
,一个装着b
。
闭包不复制值
闭包保持引用,而不是快照。
def outer():
x = [1, 2, 3] def inner(): return x return inner func = outer() print(func.__closure__[0].cell_contents) # [1, 2, 3] func.__closure__[0].cell_contents.append(4) print(func()) # [1, 2, 3, 4]
在上述代码中,首先定义了一个列表 `x`,包含元素 1、2 和 3。接着定义了一个内部函数 `inner`,该函数返回变量 `x`。外部函数 `outer` 返回内部函数 `inner`。随后,调用 `outer` 函数并将其结果赋值给 `func`。通过 `print` 语句输出 `func` 的闭包内容,结果为 `[1, 2, 3]`。接下来,向 `func` 的闭包中的 `cell_contents` 列表添加了元素 4,再次调用 `func`,输出结果为 `[1, 2, 3, 4]`。
看到了吗?这个容器保存了实际的列表,因此当我们更改它时,闭包会注意到。
这也解释了为什么
nonlocal
有效:它更新现有的容器,而不是创建一个新的。
🧰 现实生活中的闭包魔法
- 记住点击次数的计数器
def make_counter(): count = 0 def
counter():nonlocal count count += 1
return count return counter click = make_counter() print(click()) # 1 print(click()) # 2
- 函数工厂(专用计算器)。
- 数据隐藏(模拟私有变量)。
闭包 = 内存 + 函数。
第四部分:装饰器,函数的服装设计师
如果函数是演员,那么装饰器就是服装设计师 🎭。它们为函数增添额外的行为,而不改变它们的脚本。
👉 基本装饰器:
def shout(func): def wrapper(): return func().upper() return wrapper @shout def greet(): return "hello world" print(greet()) # HELLO WORLD
在幕后,
@shout
的意思是:greet = shout(greet)
所以
greet
实际上是wrapper
。没有黑魔法,只是重新赋值。
⏱️ 实用装饰器示例
- 计时器
import time def timer(func): def wrapper(*args, **kwargs): start = time.time()
result = func(*args, **kwargs) end = time.time() print(f"{func.__name__} 耗时 {end-start:.2f}秒") return result return wrapper @timer def slow(): time.sleep(2)
return "完成" 慢()
- 记录器(记录函数调用)。
- 记忆化(缓存昂贵的结果)。
- 安全检查(仅在登录后允许)。
- 堆叠装饰器(链式多个装饰)。
🔮 高级魔法:装饰器工厂与类装饰器
装饰器也可以接受参数。这就是装饰器工厂。
def repeat(times): def decorator(func): def wrapper(*args, **kwargs): for _ in range(times): func(*args, **kwargs) return wrapper return decorator @repeat(3) def say_hi(): print("Hi!")
这段代码定义了一个装饰器 `repeat`,它接受一个参数 `times`,表示要重复调用被装饰函数的次数。装饰器内部定义了一个 `decorator` 函数,该函数又定义了一个 `wrapper` 函数。在 `wrapper` 函数中,使用 `for` 循环根据 `times` 的值多次调用传入的函数 `func`,并将其参数 `args` 和 `kwargs` 传递给 `func`。最后,使用 `@repeat(3)` 装饰器装饰了 `say_hi` 函数,使其在调用时会打印 “Hi!” 三次。
say_hi()
执行为:
say_hi = repeat(3)(say_hi)
我们甚至可以装饰类,在它们创建时进行转换,就像给钢铁侠一套全新的战衣 🦾。
🧠 大局观
-
- 作用域 → Python 寻找变量的藏宝图 🗺️(LEGB 规则)。
-
- 闭包 → 背包 🎒,在父级消失后仍然携带变量。
-
__closure__
&cell
→ 实际存储闭包内容的罐子 🫙。
-
- 装饰器 → 使用闭包重新连接函数的服装设计师 🎭。
真正的超能力:Python 中的函数是一级公民。你可以传递它们、存储它们、返回它们并对它们进行包装。其他所有的闭包和装饰器都建立在此基础上。
🎉 最终篇章
下次你看到类似的内容时:
@something def do_magic():
记住:
- Python 只是将你的函数替换为一个包装版本。
- 那个包装器可能在其闭包中携带了一些变量。
- 而且 Python 静默地检查了作用域链以使一切正常工作。
这就是 Python 的真正秘诀:🪄 简单的规则 + 灵活的函数 = 无尽的魔法。
-
作者帖子
- 哎呀,回复话题必需登录。