List, Tuple, += 问题记录

Posted April 26, 2021 by clannadzsy  ‐  1 min read

运行环境:以下示例使用标准库,在 CPython 3.12.14 验证。每个代码块可独立保存为 demo.py 后执行 python demo.py;其中 TypeError 是要观察的预期行为,示例会捕获它以继续打印对象状态。

Building Lists of Lists

test_1 = [['1'] * 3 for i in range(3)]
test_2 = [['1'] * 3] * 3

test_1[1][1] = 0
print(test_1)
# [['1', '1', '1'], ['1', 0, '1'], ['1', '1', '1']]

test_2[1][1] = 0
print(test_2)
# [['1', 0, '1'], ['1', 0, '1'], ['1', 0, '1']]

print(test_1[0] is test_1[1], test_2[0] is test_2[1])
# False True

列表推导式每次创建一个新的内层列表;[inner] * 3 重复的是对同一个内层列表的引用。这里用 print 观察结果,而不是把预期结果重新赋值给变量。

Tuple, +=

a = (1, 2, [3, 4])

try:
    a[2] += [5, 6]
except TypeError as error:
    print(f'TypeError: {error}')
# TypeError: 'tuple' object does not support item assignment

print(a)
# (1, 2, [3, 4, 5, 6])

+= 对这里的列表先做原地扩展,再尝试把结果写回 a[2]。后一步因元组不支持下标赋值而失败,但前一步已修改的列表不会自动恢复;元组不可变也不表示其中引用的列表不可变。这个现象见 Python 官方 FAQ。

直接调用列表方法不需要给元组的槽位重新赋值,不过 append 和 extend 的结果不同:

a = (1, 2, [3, 4])
a[2].append([5, 6])
print(a)
# (1, 2, [3, 4, [5, 6]])

a = (1, 2, [3, 4])
a[2].extend([5, 6])
print(a)
# (1, 2, [3, 4, 5, 6])

append 将整个参数作为一个元素加入,extend 逐个加入参数中的元素,参见可变序列操作。

可以用下面的独立代码查看这两种写法的字节码:

import dis

dis.dis('a[2] += [5, 6]')
print()
dis.dis('a[2].append([5, 6])')

在 CPython 3.12 中,第一个反汇编的 BINARY_OP (+=) 在 STORE_SUBSCR 之前;第二个通过 CALL 调用 append,没有对元组执行 STORE_SUBSCR。dis.dis 只反汇编传入的代码,不会执行这两条语句。字节码是 CPython 实现细节,不同版本的指令和偏移量会变化,应以本机 dis 输出为准。