Pythonタプルの不変性の罠

Pythonの可変性とは、Pythonオブジェクトの作成後に、そのオブジェクトを変更できるかどうかを指す。DictやListは可変、タプルやブールは不変というように。ざっくりまとめると一般的には以下のように習うことが多い。

タイプ 概要 不変?  
bool Boolean value yes  
int 整数 yes  
float 少数 yes  
list 可変オブジェクトの集まり no  
tuple 不変オブジェクトの集まり yes  
str ストリング yes  
set 集合のオブジェクトの集まり no  
frozenset 不変の集合 yes  
dict ディクショナリ no </figure>

例えば不変性のオブジェクトを作成後に変更しようと以下のような事が起こる。

In [11]: my_tuple = (1,2,3)
In [12]: my_tuple[0]
Out[12]: 1
In [13]: my_tuple[0] = 10
TypeError  Traceback (most recent call last) in
----> 1 my_tuple[0] = 10
TypeError: 'tuple' object does not support item assignment

しかし!もしタプルが可変性のオブジェクトを含む場合(リストとか)、注意が必要だ。

In [14]: my_tuple2 = ([1,2,3], [4,5,6])
In [15]: my_tuple2[0][2] = 10
In [16]: my_tuple2
Out[16]: ([1, 2, 10], [4, 5, 6])

この場合、Pythonは何も問題なくリストの変数を更新してしまう。これはタプルはリストの参照のみを含んでいるだけであって、タプルはリストの中身が何かまでは把握していないからだ。上記の例をより分かりやすく書き換えてみると、こうなる。

In [17]: list1 = [1,2,3]
In [18]: list2 = [4,5,6]
In [19]: my_tuple2 = (list1, list2)
In [20]: list1[2] = 10
In [21]: my_tuple2
Out[21]: ([1, 2, 10], [4, 5, 6])

つまり、タプルが不変だ!と言うときは、タプル自身のオブジェクトの参照のみを指していているだけなのだ。実際にタプルのリストの参照を更新しようとすると不変性のルールでエラーが出る。

In [22]: my_tuple2[0] = [2,3,4] TypeError                                 Traceback (most recent call last) in
 ----> 1 my_tuple2[0] = [2,3,4]
 TypeError: 'tuple' object does not support item assignment

不変性の本質についてちょっと詳しくなった!って思えたら、他のPython書く友達は同僚に教えてみてね!意外と知らない人いると思うので。

おまけ

他にもPythonのちょっとビックリするような動作がまとめられたレポジトリがあるので、確認してみてね!https://github.com/satwikkansal/wtfpython#-mutating-the-immutable (wft pythonは訳すると”Python、何だこれ!”的な意味)。

例えば、タプルに関してもう一つびっくりする例を出すなら。。。

In [23]: another_tuple = ([1, 2], [3, 4], [5, 6, 1000])
In [24]: another_tuple[2].append(1000) # エラーなし
In [25]: another_tuple[2] += [99, 999] TypeError                                 Traceback (most recent call last) in
 ----> 1 another_tuple[2] += [99, 999]
 TypeError: 'tuple' object does not support item assignment
In [26]: another_tuple
Out[26]: ([1, 2], [3, 4], [5, 6, 1000, 1000, 99, 999])