# 元组不可变
# 元组不可变
首先明确元组不可变,但是如果元组内元素是可变元素,那么可变元素内部状态(例如list内的元素增删改)可以发生变化,并且这和元组不变是不冲突的。
观察如下代码:
t = (1, 2, [30, 40])
try:
t[2] += [50, 60]
except Exception as e:
print("CATCH: ",e)
print(t)
# CATCH: 'tuple' object does not support item assignment
# (1, 2, [30, 40, 50, 60])
或者放入交互式环境下运行效果更明显:
>>> t = (1, 2, [30, 40])
>>> t[2] += [50, 60]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment
>>> t
(1, 2, [30, 40, 50, 60])
注意到列表成功增加了元素,但是报了元组不可变的错误。
查看其字节码:
>>> dis.dis('t[2]+=[100]')
1 0 LOAD_NAME 0 (t)
2 LOAD_CONST 0 (2)
4 DUP_TOP_TWO
6 BINARY_SUBSCR
8 LOAD_CONST 1 (100)
10 BUILD_LIST 1
12 INPLACE_ADD
14 ROT_THREE
16 STORE_SUBSCR
18 LOAD_CONST 2 (None)
20 RETURN_VALUE
其中关键之处在于STORE_SUBSCR字节码,其作用是将栈顶的三个元素弹出,并将最上面的元素作为value,中间的元素作为key,最下面的元素作为对象。然后它会使用对象的__setitem__方法,将key和value关联起来。
具体来说,STORE_SUBSCR的操作步骤如下:
- 弹出栈顶元素,作为value;
- 弹出次顶元素,作为key;
- 弹出栈底元素,作为对象;
- 调用对象的
__setitem__方法,将key和value关联起来。
因此此时实际上是触发了元组的__setitem__方法,导致了报错。
注意,使用append则不会触发报错:
>>> t[2].append(100)
>>> t
(1, 2, [30, 40, 50, 60, 100])
>>> dis.dis('t[2].append(100)')
1 0 LOAD_NAME 0 (t)
2 LOAD_CONST 0 (2)
4 BINARY_SUBSCR
6 LOAD_METHOD 1 (append)
8 LOAD_CONST 1 (100)
10 CALL_METHOD 1
12 RETURN_VALUE
# 元组的内存优化
由于元组本身定义是不可变的,因此有的时候元组是会复用的,这就和int的小整数池以及str的内存优化是一样的。
有如下代码,注意help(tuple)中特别指出的一行,其说明元组有的时候并不会创建副本,而是返回同一个对象的引用。
t1 = (1,2,3)
t2 = tuple(t1)
t3 = t1[:]
print(t1 == t2)
print(t1 is t2)
print(id(t1),' ',id(t2))
"""
True
True
2207179699928 2207179699928
"""
print(t1 == t3)
print(t1 is t3)
print(id(t1),' ',id(t3))
"""
True
True
2207179699928 2207179699928
"""
# print(help(tuple))
"""
class tuple(object)
| tuple(iterable=(), /)
|
| Built-in immutable sequence.
|
| If no argument is given, the constructor returns an empty tuple.
| If iterable is specified the tuple is initialized from iterable's items.
|
| If the argument is a tuple, the return value is the same object. <-
"""