元组的两副面孔
元组有两个身份,入门教程通常只讲后一个:
| 用法 | 元素个数 | 顺序 | 元素类型 | 能排序吗 |
|---|---|---|---|---|
| 记录(record) | 固定 | 有意义,位置就是含义 | 通常各不相同 | 不能,排序会毁掉含义 |
| 不可变列表 | 任意 | 可能不重要 | 通常一致 | 能 |
把元组只说成「不可变的列表」是低估了它。
lax_coordinates = (33.9425, -118.408056) # 纬度, 经度city, year, pop, chg, area = ('Tokyo', 2003, 32_450, 0.66, 8014)traveler_ids = [('USA', '31195855'), ('BRA', 'CE342567'), ('ESP', 'XDA205856')]判断有没有当记录用,看一件事:把元素重排之后信息还在不在。
(33.9425, -118.408056) 换个顺序就变成另一个地方了,所以它是记录。
[3, 1, 2] 排一下还是那三个数,所以它是列表。
记录用法下有两个技巧很省事:
# % 格式化认识元组,会把它拆成一个个字段>>> '%s/%s' % ('USA', '31195855')'USA/31195855'
# for 循环里直接拆,不用下标>>> for country, _ in traveler_ids:... print(country)第二个用法叫解包。_ 只是个普通的变量名,约定俗成表示「这个位置我不关心」。
当不可变列表用
Section titled “当不可变列表用”好处有两个:
- 清晰:代码里看到元组,你就知道长度不会变。
- 性能:同样长度的元组比列表省内存,还能让 CPython 做优化。
具体快在哪,书里引了 Raymond Hettinger 的解释,我归纳成四条:
| 方面 | tuple | list |
|---|---|---|
| 字面量求值 | 编译器一次生成一个元组常量 | 逐个压栈再构造列表 |
tuple(t) |
直接返回同一个对象 | list(l) 必须复制一份 |
| 内存分配 | 长度固定,刚好够用 | 多留余量,摊薄后续 append 成本 |
| 元素引用存放 | 存在元组自身结构体的数组里 | 存指针指向别处的数组,多一层间接 |
最后一条的「多一层间接」不是随便设计的:列表增长时得重新分配那块引用数组, 所以引用数组必须放在外面。
陷阱:不可变的是引用,不是引用的对象
Section titled “陷阱:不可变的是引用,不是引用的对象”这条是最容易踩的。看这个:
>>> a = (10, 'alpha', [1, 2])>>> b = (10, 'alpha', [1, 2])>>> a == bTrue>>> b[-1].append(99)>>> a == bFalse>>> b(10, 'alpha', [1, 2, 99])元组本身没变——b 的三个引用还是指向原来那三个对象。但第三个对象是个 list,
它的内容变了,于是 b 的值变了。
后果很实际:这样的元组不可哈希,不能当 dict 的键,也不能放进 set。
>>> {b: 'value'}TypeError: unhashable type: 'list'怎么判断一个元组的值是不是真的固定
Section titled “怎么判断一个元组的值是不是真的固定”用 hash()。能算出哈希,说明值永远不会变:
def is_fixed_value(obj): try: hash(obj) except TypeError: return False return True
>>> is_fixed_value((10, 'alpha', (1, 2))) # 元素全是不可变的True>>> is_fixed_value((10, 'alpha', [1, 2])) # 里面有个 listFalse我把它写成 chapters/ch02/records.py 里的 is_fixed_value,测试里连 [1, 2] 本身
也验证了(返回 False)。
一条实践建议
Section titled “一条实践建议”书里的原话是「避免把可变元素放进元组」。这条建议的成本很低,收益是省掉一类很难查的 bug: 值莫名其妙变了,但你找不到哪行代码改的它。
test_hashability_means_fixed_value—— 哈希判断三种情况test_tuple_immutability_stops_at_the_reference—— 改内层 list 之后两元组不等test_unhashable_tuple_cannot_be_a_dict_key—— 塞进 dict 抛TypeError