Python
 Computer >> コンピューター >  >> プログラミング >> Python

Pythonのタプルで起きる「+=」代入の謎をバイトコードで解き明かす

この記事では、本来変更不可(イミュータブル)であるはずのタプルの値を更新しようとしたときに何が起こるのかを解説します。新しい値とのマージは一見成功したように見えるのに、実際にはエラーがスローされる——この一見矛盾した挙動を、エラー発生時のバイトコードを詳しく調べることで、タプルの動作ルールへの理解を深めていきます。

タプルの要素を更新してみる

まずタプルを定義し、その最後の要素(リスト)を更新するコマンドを実行してみましょう。

>>> tupl = (5,7,9,[1,4])
>>> tupl[3] += [6,8]

出力結果

上記のコードを実行すると、次のような結果になります。

Traceback (most recent call last):
File "", line 1, in
TypeError: 'tuple' object does not support item assignment
>>> tupl
(5, 7, 9, [1, 4, 6, 8])

ここで注目すべきは、TypeError が発生したにもかかわらず、タプル内のリストには [6, 8] が追加されているという点です。つまり、処理は「途中まで成功し、その後に失敗している」のです。これが今回のなぞなぞの核心です。

関数にまとめてエラーを詳細に確認する

この挙動をより詳しく調べるため、コードを関数にまとめます。同じエラーが発生しますが、トレースバックにより詳細な情報が得られます。

def immut_function():
    tupl = (5,7,9,[1,4])
    tupl[3] += [6,8]
immut_function()

出力結果

TypeError Traceback (most recent call last)
in ()
      2 tupl = (5,7,9,[1,4])
      3 tupl[3] += [6,8]
----> 4 immut_function()
in immut_function()
      1 def immut_function():
      2 tupl = (5,7,9,[1,4])
----> 3 tupl[3] += [6,8]
4 immut_function()
TypeError: 'tuple' object does not support item assignment

バイトコードで内部動作を覗く

次に、dis モジュールを使ってこの関数のバイトコードを表示し、タプルへの代入とエラーの詳細を確認します。

import dis
dis.dis(immut_function)

出力結果

2   0 LOAD_CONST      1 (5)
    3 LOAD_CONST      2 (7)
    6 LOAD_CONST      3 (9)
    9 LOAD_CONST      4 (1)
   12 LOAD_CONST      5 (4)
   15 BUILD_LIST      2
   18 BUILD_TUPLE     4
   21 STORE_FAST   0 (tupl)
    3 24 LOAD_FAST   0 (tupl)
   27 LOAD_CONST 6 (3)
   30 DUP_TOPX 2
   33 BINARY_SUBSCR
   34 LOAD_CONST 7 (6)
   37 LOAD_CONST 8 (8)
   40 BUILD_LIST 2
   43 INPLACE_ADD
   44 ROT_THREE
   45 STORE_SUBSCR
   46 LOAD_CONST 0 (None)
   49 RETURN_VALUE

各ステップの意味

  • 24〜33行目: tupl[3] の値、つまりリストへの参照をスタックから取り出します。
  • 34〜40行目: 加算対象となる新しいリスト [6, 8] を生成します。
  • 43行目(INPLACE_ADD): 取り出したリストに対してインプレース加算を実行します。この時点で元のリスト自体は書き換えられています。リストはミュータブルだからです。
  • 45行目(STORE_SUBSCR): 更新後のリストを tupl[3] に書き戻そうとしますが、タプルはイミュータブルなオブジェクトのため、ここで初めて TypeError が発生します

まとめ:学ぶべき教訓

この現象から得られる結論はシンプルです。ミュータブルな要素(リストなど)をタプルの中に入れないということです。

一見すると要素への代入が可能に思えても、内部的には「リストの更新」と「タプルへの再代入」という2段階の処理が行われており、後者の段階で必ず例外がスローされます。その結果、データは部分的に書き換えられた中途半端な状態になってしまい、プログラムの予測可能性を損ないます。

可変なデータをグループ化したい場合は、タプルではなくリストを使用するか、そもそも設計を見直してイミュータブルな構造に収まるようにデータを整理することをおすすめします。

  1. 【初心者向け】Pythonのissuperset()メソッドの使い方をわかりやすく解説

    はじめにこの記事では、Pythonのissuperset()メソッドについて、基本的な仕組みから実際のコード例まで詳しく解説します。issuperset()は、セット(集合)に対して使用できるメソッドで、引数として渡されたセットのすべての要素が、呼び出し元のセットに含まれているかどうかを判定します。呼び出し元のセットBが、引数のセットAのすべての要素を含んでいる場合 → True を返すセットAの要素がすべてBに含まれていない場合 → False を返すつまり、「BがAの上位集合(スーパーセット)であるかどうか」を判定するためのメソッドです。基本構文B.issuperset(A)この式は、Bが

  2. Pythonの代入演算子とは?種類と使い方を一覧表でわかりやすく解説

    Pythonには、変数に値を格納するための代入演算子が複数用意されています。基本となる「=」のほかに、算術演算と代入を一度に行える複合代入演算子があり、これらを使いこなすことでコードをより簡潔に記述できます。 Pythonの代入演算子一覧 以下の表は、Pythonで使用できるすべての代入演算子をまとめたものです。 演算子 説明 使用例 = 右側の値を左側の変数に代入します。 c = a + b … a + b の計算結果を c に代入 += 左辺に右辺を加算し、その結果を左辺に代入します。 c += a … c に a を加算(c = c + a と同じ) -= 左辺から右辺を