Python 编程思想:名字、对象与资源边界
本页目录
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 的引用计数、垃圾回收和对象缓存,是理解性能时的重要背景,但不应据此依赖某个对象恰好何时销毁。需要及时关闭的外部资源,使用明确的生命周期管理。
分析复杂行为时,可以先画出名字与对象的引用关系,再写一个最小例子。能清楚解释哪个引用被替换、哪个对象被修改,很多问题就已经解决了一半。
相关文章
- RESTful API 框架 HUG 指南2017-03-09
- Cpp 编码风格指南2017-01-29
- Javascript 编码风格指南2016-11-14
- Go 编码风格指南2016-11-02