# 元组不可变

# 元组不可变

首先明确元组不可变,但是如果元组内元素是可变元素,那么可变元素内部状态(例如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的操作步骤如下:

  1. 弹出栈顶元素,作为value;
  2. 弹出次顶元素,作为key;
  3. 弹出栈底元素,作为对象;
  4. 调用对象的__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.  <-
"""