《深入探索Python中的作用域、闭包与装饰器》

大多数Python教程解释了作用域、闭包和装饰器是什么。

今天,我们不打算进行枯燥的讲座,而是要偷偷溜进后台🎭,窥探Python的引擎室🛠️,以一种能够真正留在你脑海中的方式揭示这些概念背后的魔力。

到最后,你会明白为什么Python感觉像是一种扭曲现实的语言,并且你再也不会以同样的方式看待装饰器。

准备好你的爆米花🍿,让我们开始吧。


🎯 第1部分:作用域,变量的藏宝图

把Python想象成一座巨大的豪宅🏰。每个房间(函数/模块)都有装满贵重物品(变量)的抽屉。

当你请求x时,Python会按照以下顺序搜索抽屉:

  • 局部(L) → 你的房间(当前函数)。
  • 封闭(E) → 包含当前函数的任何外部函数。
  • 全局(G) → 整个豪宅(模块级命名空间)。
  • 内置(B) → 外部世界(Python的内置函数,如lenprint)。

这被称为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 已经运行结束,times3times5 仍然记得它们的 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 有效:它更新现有的容器,而不是创建一个新的。


🧰 现实生活中的闭包魔法

  1. 记住点击次数的计数器
def make_counter():
    count = 0
    def counter(): nonlocal count
        count += 1

return count
return counter

click = make_counter()
print(click())  # 1
print(click())  # 2
  1. 函数工厂(专用计算器)。
  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。没有黑魔法,只是重新赋值。


⏱️ 实用装饰器示例

  1. 计时器
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 "完成"

()
  1. 记录器(记录函数调用)。
  2. 记忆化(缓存昂贵的结果)。
  3. 安全检查(仅在登录后允许)。
  4. 堆叠装饰器(链式多个装饰)。

🔮 高级魔法:装饰器工厂与类装饰器

装饰器也可以接受参数。这就是装饰器工厂

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

更多