回到首页

输入关键词,多个关键词用空格分隔。

← 编程语言与英语 · 系列目录

Python 编程思想:名字、对象与资源边界

约 3 分钟
本页目录

Python 中很多看似奇怪的行为,都可以回到三个问题:名字绑定了哪个对象,对象是否可变,谁负责释放外部资源。把这三件事弄清楚,比记住一串“黑魔法”更有用。

本文重写自《Python 编程思想》,保留对象与资源管理主线,示例使用 Python 3,不沿用旧版字节码细节和实现偶然行为。

赋值没有复制对象

a = [1, 2]
b = a
b.append(3)
assert a == [1, 2, 3]
assert a is b

a 和 b 绑定到同一个列表。== 比较值是否相等,is 比较是否为同一对象;不要用字符串或小整数缓存的现象推导通用规则。

对象的身份、类型和值是不同概念。元组不能替换成员引用,但成员指向的对象仍可能可变:

items = ([1], 'label')
items[0].append(2)
assert items == ([1, 2], 'label')

这里没有改变元组保存的引用,改变的是列表内容。定义见 Python 数据模型。

浅拷贝只复制外层

from copy import deepcopy

original = [[1], [2]]
shallow = original.copy()
deep = deepcopy(original)
original[0].append(3)
assert shallow[0] == [1, 3]
assert deep[0] == [1]

浅拷贝产生新容器,成员仍指向原对象;深拷贝递归复制成员。深拷贝也不是万能的资源克隆:文件、套接字等并不会被复制成等价的独立资源,类还可以定制复制行为。见 copy 模块。

选择哪种复制方式,要从是否允许共享成员状态出发,而不是把深拷贝当成默认的“更安全”。有时共享就是设计的一部分。

区分修改对象与重新绑定

对列表,append 增加一个元素,extend 逐个加入可迭代对象中的元素。+ 通常创建新列表,+= 对列表通常原地扩展;对于其他类型,行为由相应协议决定。

a = [1]
b = a
a = a + [2]
assert b == [1]
a = b
a += [3]
assert b == [1, 3]

函数参数同样遵循名字绑定。函数内重新给参数赋值,不会重新绑定调用者的名字;修改共同引用的可变对象,则可能被调用者观察到。

可变默认参数也与此相关。默认值在函数定义时求值,而不是每次调用都重新创建:

def add_item(value, items=None):
    if items is None:
        items = []
    items.append(value)
    return items

assert add_item(1) == [1]
assert add_item(2) == [2]

with 明确资源生命周期

文件、锁和事务有明确的开始与结束。用上下文管理器表达这个边界,能够在正常退出和异常退出时执行约定的清理逻辑。

from contextlib import contextmanager

@contextmanager
def tracked(events):
    events.append('enter')
    try:
        yield
    finally:
        events.append('exit')

events = []
try:
    with tracked(events):
        raise ValueError('example')
except ValueError:
    pass
assert events == ['enter', 'exit']

这个例子不会吞掉异常;外层仍需要决定怎样处理它。实际上下文管理器是否抑制异常、如何提交或回滚,由具体实现决定。contextlib 文档说明了协议与生成器形式的管理器。

语言规则与实现细节分开

CPython 的引用计数、垃圾回收和对象缓存,是理解性能时的重要背景,但不应据此依赖某个对象恰好何时销毁。需要及时关闭的外部资源,使用明确的生命周期管理。

分析复杂行为时,可以先画出名字与对象的引用关系,再写一个最小例子。能清楚解释哪个引用被替换、哪个对象被修改,很多问题就已经解决了一半。

相关文章