Files
coding-mentor-agent/kb/python-course-kb-practical-python/wiki/concepts/闭包.md
T

16 KiB
Raw Blame History

sources, brief
sources brief
summaries/07_Advanced_Topics__00_Overview.md
summaries/04_Function_decorators.md
summaries/03_Returning_functions.md
summaries/02_Anonymous_function.md
summaries/00_Overview.md
闭包是函数携带其所引用外部变量环境并在之后继续使用的机制。

闭包

闭包是一种与高阶函数、函数式编程和装饰器实现密切相关的机制:当一个内部函数引用了外部函数作用域中的变量,并且这个内部函数被返回、保存或传递到外部继续使用时,它仍然能够“记住”并访问那些外部变量。

更直观地说:

闭包 = 函数 + 该函数运行所需的外部变量环境

在 summaries/00_Overview 中,闭包作为第 7 章“高级主题”的一部分出现,与“返回函数”一起被列为 Python 进阶特性之一。summaries/03_Returning_functions 进一步说明:闭包的核心价值在于让一个函数即使在原始定义环境结束后,仍能携带必要上下文,在未来正确执行。summaries/04_Function_decorators 则展示了闭包在 函数装饰器 中的典型用途:装饰器通过内部包装函数保存被装饰的原函数,并在调用时添加日志、计时等额外行为。

基本思想

闭包通常涉及三个要素:

  1. 外部函数:定义局部变量、参数或局部状态。
  2. 内部函数:在函数体内引用外部函数中的变量。
  3. 返回或保存内部函数:即使外部函数已经执行结束,内部函数仍然可以访问当时捕获的变量。

示意代码:

def make_adder(x):
    def add(y):
        return x + y
    return add

add10 = make_adder(10)
print(add10(5))  # 15

这里 add 就形成了闭包,因为它捕获了外部函数 make_adder 中的变量 x。即使 make_adder(10) 已经返回,add10 仍然记住了 x = 10。

summaries/03_Returning_functions 中的例子也展示了同样机制:

def add(x, y):
    def do_add():
        print('Adding', x, y)
        return x + y
    return do_add

调用 add(3, 4) 时,加法并不会立刻发生,而是返回内部函数 do_add:

a = add(3, 4)
a()   # 输出 Adding 3 4,并返回 7

这里 do_add 在 add() 已经执行结束后,仍然能够使用 x = 3 和 y = 4。这正是闭包的本质:函数不仅保存代码,还保存代码所依赖的环境。

与返回函数的关系

闭包经常和“函数作为返回值”一起出现。外部函数返回内部函数时,内部函数并不只是一个可执行对象;如果它引用了外层变量,它还会携带对应的变量绑定。

因此,闭包体现了 Python 中“函数是一等对象”的思想:函数可以被创建、赋值、传递、返回,并保存状态。这也使闭包与 函数式编程 密切相关。

需要注意的是,“返回函数”本身不一定总是闭包。只有当返回的内部函数引用了外部作用域中的变量时,才形成闭包。例如:

def outer():
    def inner():
        return 'hello'
    return inner

这里 inner 没有依赖 outer 的局部变量,因此它只是被返回的函数,不体现典型闭包的状态捕获能力。

局部变量为什么还能存在

普通理解中,函数调用结束后,它的局部变量似乎应该消失。但闭包改变了这个直觉:如果内部函数仍然需要某些外部变量,Python 会保留这些变量,使返回后的函数可以继续访问它们。

因此,下面的问题是理解闭包的关键:

a = add(3, 4)
a()

a() 执行时,3 和 4 从哪里来?答案是:它们来自闭包保存的环境。do_add 携带了对 x 和 y 的引用,使这些值在之后仍可用。

同样,在装饰器中也可以问类似问题:

def logged(func):
    def wrapper(*args, **kwargs):
        print('Calling', func.__name__)
        return func(*args, **kwargs)
    return wrapper

当 logged() 已经返回后,wrapper() 为什么仍然知道要调用哪个 func?答案也是闭包:wrapper 捕获并保存了外层 logged(func) 调用时传入的函数对象。

闭包与装饰器

summaries/04_Function_decorators 说明,装饰器的基本思想是用一个函数包裹另一个函数,为原函数添加额外行为。例如日志装饰器:

def logged(func):
    def wrapper(*args, **kwargs):
        print('Calling', func.__name__)
        return func(*args, **kwargs)
    return wrapper

这里 wrapper 是闭包,因为它引用了外层函数 logged 的参数 func。当执行:

def add(x, y):
    return x + y

logged_add = logged(add)

logged_add 实际上指向返回的 wrapper。调用时:

logged_add(3, 4)

wrapper 会先输出日志,再调用它保存的原始函数 add。即使 logged(add) 的调用已经结束,wrapper 仍然保留着对 add 的引用。

这也是 Python装饰器 或 函数装饰器 常见实现方式的核心:

  • 外层装饰器函数接收原函数 func。
  • 内层包装函数 wrapper 添加额外逻辑。
  • wrapper 通过闭包保存 func。
  • 装饰器返回 wrapper,用它替代原函数名。

装饰器语法:

@logged
def add(x, y):
    return x + y

等价于:

def add(x, y):
    return x + y
add = logged(add)

因此,装饰器并不是脱离闭包的独立魔法。很多装饰器本质上就是“返回包装函数的函数”,而包装函数之所以能继续调用原函数,正是因为闭包保存了原函数对象。

闭包、包装函数与可变参数

装饰器中常见的内部函数也称为 包装函数。包装函数需要尽量像原函数一样工作,因此通常使用 Python函数参数 中的可变参数形式:

def wrapper(*args, **kwargs):
    return func(*args, **kwargs)

这里有两层重要机制:

  • *args 和 **kwargs 让 wrapper 可以接受任意位置参数和关键字参数,从而适配不同函数签名。
  • func 来自外层作用域,由闭包保存,使 wrapper 知道真正要调用的原函数。

因此,装饰器中的闭包通常同时结合了:

  • 函数作为参数传入;
  • 内部函数作为返回值返回;
  • 内部函数捕获外部变量;
  • 可变参数转发调用;
  • 用返回的新函数替换旧函数。

这些特性共同体现了 Python 函数对象的灵活性。

计时装饰器中的闭包

summaries/04_Function_decorators 的练习要求实现一个 timethis(func) 装饰器,用来测量函数执行时间:

import time

def timethis(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        r = func(*args, **kwargs)
        end = time.time()
        print('%s.%s: %f' % (func.__module__, func.__name__, end-start))
        return r
    return wrapper

这个例子进一步展示了闭包在诊断工具中的作用:

  • wrapper 捕获原函数 func。
  • 调用 wrapper 时,会在执行原函数前后记录时间。
  • func.__module__ 和 func.__name__ 用于输出被测函数的模块名和函数名。
  • 原函数的返回值 r 被保留并返回,使装饰器尽量不改变原函数的语义。

使用方式:

@timethis
def countdown(n):
    while n > 0:
        n -= 1

这里 countdown 被重新绑定为 timethis(countdown) 返回的 wrapper。但 wrapper 仍通过闭包记住原始 countdown 函数,因此可以在添加计时逻辑后继续调用它。

这个模式说明,闭包不仅能保存简单数值,也能保存函数对象、配置参数、统计状态等运行上下文。

闭包的常见用途

闭包是 Python 中非常重要但有时较隐蔽的特性。常见用途包括:

  • 保存配置或状态:在不使用类的情况下,为函数绑定参数或上下文。
  • 创建函数工厂:根据不同输入生成具有不同行为的新函数。
  • 回调函数:把带有上下文的函数传给其他代码稍后调用。
  • 延迟执行:先构造函数,之后再执行。
  • 装饰器实现:许多 Python装饰器 和 函数装饰器 依赖闭包保存被包装函数及附加状态。
  • 包装横切逻辑:日志、计时、调试、权限检查等 横切关注点 可以通过闭包集中封装。
  • 减少重复代码:用函数生成重复结构,例如属性、检查逻辑或包装函数。

延迟执行

summaries/03_Returning_functions 用 after(seconds, func) 展示了闭包在延迟执行中的作用:

def after(seconds, func):
    import time
    time.sleep(seconds)
    func()

如果直接传入普通函数:

def greeting():
    print('Hello Guido')

after(30, greeting)

after 会在等待后调用 greeting。

闭包可以让这个被延迟执行的函数携带额外信息:

def add(x, y):
    def do_add():
        print(f'Adding {x} + {y} -> {x+y}')
    return do_add

after(30, add(2, 3))

这里 add(2, 3) 返回的 do_add 是闭包,它保留了 x = 2 和 y = 3。即使真正执行发生在 30 秒后,它仍然知道要计算什么。

这类模式与 concepts/回调函数 和 延迟求值 密切相关:函数被提前构造,执行被推迟,但上下文不会丢失。

函数工厂

闭包常用于创建“函数工厂”:一个函数根据输入生成另一个函数。

例如:

def power_factory(n):
    def power(x):
        return x ** n
    return power

square = power_factory(2)
cube = power_factory(3)

square 和 cube 都是闭包,它们分别保存了不同的 n。调用时:

square(5)  # 25
cube(5)    # 125

这里不需要定义两个不同类,也不需要反复传入指数参数;闭包已经把配置保存到了函数内部。

装饰器也可以看作一种特殊的函数工厂:它接收一个函数,生成另一个包装函数。若装饰器再带参数,则通常会出现更多层函数嵌套,每一层都可能通过闭包保存不同的配置或状态。

用闭包减少重复代码

闭包还可以用于“生成代码式结构”,避免大量重复样板代码。summaries/03_Returning_functions 中的核心示例是创建带类型检查的属性。

原始写法中,每个属性都需要重复编写 getter、setter 和类型检查逻辑:

@property
def shares(self):
    return self._shares

@shares.setter
def shares(self, value):
    if not isinstance(value, int):
        raise TypeError('Expected int')
    self._shares = value

如果 name、shares、price 都要做类似检查,就会出现大量重复。可以改用闭包生成属性:

def typedproperty(name, expected_type):
    private_name = '_' + name

    @property
    def prop(self):
        return getattr(self, private_name)

    @prop.setter
    def prop(self, value):
        if not isinstance(value, expected_type):
            raise TypeError(f'Expected {expected_type}')
        setattr(self, private_name, value)

    return prop

这里 prop 是闭包,因为它捕获了:

  • name
  • private_name
  • expected_type

每次调用 typedproperty(),都会生成一个新的属性对象,并且该属性对象记住自己的私有字段名和期望类型。

于是可以这样定义类:

class Stock:
    name = typedproperty('name', str)
    shares = typedproperty('shares', int)
    price = typedproperty('price', float)

    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

当执行:

s = Stock('IBM', 50, 91.1)
s.shares = '100'

赋值会触发 setter,并因类型不匹配抛出 TypeError。

这个例子说明,闭包可以与 Python属性、Python描述符 和类定义机制配合,用来构造可复用的行为模板。

装饰器中的日志和计时示例也体现了同一个原则:如果某段辅助逻辑反复出现在很多函数中,可以把它移到闭包和包装函数中统一管理,从而减少重复并提升可维护性。

与 lambda 的配合

闭包也可以和 lambda表达式 配合,用来进一步简化接口。

例如在 typedproperty 的基础上定义:

String = lambda name: typedproperty(name, str)
Integer = lambda name: typedproperty(name, int)
Float = lambda name: typedproperty(name, float)

这样类定义可以写得更简洁:

class Stock:
    name = String('name')
    shares = Integer('shares')
    price = Float('price')

这里 lambda 本身也是函数对象,也可以捕获或绑定上下文。它常被用于创建短小的函数工厂包装层,从而减少重复参数。

与类和对象状态的关系

闭包和类都可以保存状态,但表达方式不同:

  • 类通常把状态保存在实例属性中,通过方法访问。
  • 闭包把状态保存在函数的外部变量环境中,通过返回的内部函数访问。

例如:

def make_counter():
    count = 0
    def counter():
        return count
    return counter

这类结构类似一个轻量对象:函数 counter 携带了状态 count。当状态和行为较简单时,闭包可以替代小型类;当状态复杂、行为较多或需要继承时,类通常更清晰。

装饰器中的 wrapper 也类似一个轻量对象:它把“原函数”作为状态保存起来,并在每次调用时围绕这个状态执行额外逻辑。不过,如果装饰逻辑需要维护复杂状态,类装饰器或描述符有时会更适合。

与相关概念的联系

  • 函数式编程:闭包是函数式编程中的重要机制,支持函数组合、函数工厂和高阶函数。
  • Python装饰器:装饰器通常通过闭包保存原函数,并返回包装后的新函数。
  • 函数装饰器:函数装饰器中的 wrapper 往往就是捕获 func 的闭包。
  • 包装函数:包装函数通过闭包记住被包装对象,并在调用前后添加额外行为。
  • 横切关注点:日志、计时等横切逻辑可由闭包和装饰器统一封装。
  • lambda表达式:lambda 也可以捕获外部变量,因此在某些情况下也会形成闭包。
  • Python函数参数:闭包有时可替代反复传参,通过捕获外部变量保存上下文;装饰器中也常用 *args、**kwargs 转发参数。
  • concepts/回调函数:闭包可以把回调逻辑和回调所需上下文打包在一起。
  • 延迟求值:闭包允许函数稍后执行,同时保留构造时的变量环境。
  • Python属性:闭包可以生成 property 对象,减少 getter/setter 的重复代码。
  • Python函数对象:闭包依赖函数可被传递、返回和保存的特性,也可访问函数的 __name__、__module__ 等属性。

学习提示

理解闭包时,重点应放在以下问题上:

  • 内部函数引用了哪些外部变量?
  • 外部函数返回后,这些变量为什么仍然可用?
  • 闭包是在保存代码,还是在保存代码加环境?
  • 返回函数是否真的引用了外部变量,还是只是普通地返回了一个函数?
  • 在装饰器中,包装函数如何记住被装饰的原函数?
  • *args 和 **kwargs 如何帮助包装函数透明地转发调用?
  • 这个场景更适合闭包,还是更适合类?
  • 闭包是否能减少重复代码,或让延迟执行更方便?
  • 闭包与装饰器、回调函数、属性工厂之间有什么关系?

summaries/00_Overview 强调,闭包属于 Python 日常编程中可能遇到的进阶主题。summaries/03_Returning_functions 通过返回函数、延迟执行和类型化属性示例说明:闭包不仅是语法技巧,更是一种保存上下文、生成函数和消除重复代码的重要工具。summaries/04_Function_decorators 进一步展示了闭包在装饰器中的实际价值:它让包装函数能够保存原函数,并在不修改原函数主体的情况下添加日志、计时等可复用行为。

See also: summaries/02_Anonymous_function, summaries/03_Returning_functions, summaries/04_Function_decorators

See also: summaries/07_Advanced_Topics__00_Overview