大多数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 的真正秘诀:🪄 简单的规则 + 灵活的函数 = 无尽的魔法。