Webエンジニア向けプログラミング解説動画をYouTubeで配信中!
▶ チャンネル登録はこちら

【ITニュース解説】Beyond the Label: How Python Variables Really Work with Memory

2025年09月13日に「Dev.to」が公開したITニュース「Beyond the Label: How Python Variables Really Work with Memory」について初心者にもわかりやすく解説しています。

作成日: 更新日:

ITニュース概要

Pythonの変数はメモリ上のオブジェクトを示す「ラベル」であり、全てIDを持つ。代入は同じオブジェクトを指し、シャローコピーは内部オブジェクトを共有。ディープコピーのみ完全な複製となる。関数への引数はオブジェクト参照渡しで、オブジェクトの変更は影響するが、引数の再代入は関数内のみとなる。

ITニュース解説

Pythonプログラミングにおいて、変数がメモリ上でどのように扱われるかを理解することは、堅牢でバグの少ないコードを書く上で非常に重要である。変数は単なるラベルであり、データそのものではないという基本的な概念を超えて、より複雑なデータ構造を扱う際に起こる内部的な挙動を深く掘り下げていく。この知識は、初心者が陥りやすい一般的なバグを未然に防ぐための鍵となる。

Pythonでは、整数、文字列、リスト、関数、さらにはクラスそのものに至るまで、プログラムが扱う「すべて」がメモリ上に存在する「オブジェクト」である。各オブジェクトは、その生涯にわたって一意で不変な「同一性(ID)」、それがどのような種類のオブジェクトであるかを示す「型」、そして実際に保持するデータである「値」の三つの要素を持つ。オブジェクトの同一性(ID)は、メモリ上のアドレスのようなもので、id()関数を使って確認できる。このIDはオブジェクトがメモリ上に存在している間は変更されず、一意性が保証されている。isキーワードは、二つの変数がまったく同じオブジェクト(つまり同じIDを持つオブジェクト)を指しているかどうかを比較する。これに対し、==演算子は、二つのオブジェクトが持つ「値」が等しいかどうかを比較する。例えば、a = [1, 2, 3]と記述すると、Pythonはメモリ上に[1, 2, 3]というリストオブジェクトを作成し、これに固有のIDを割り当て、aというラベルを付けてそのオブジェクトを指し示す。次にb = aと記述した場合、新しいリストオブジェクトは作成されず、既存の[1, 2, 3]というリストオブジェクトに対し、bという新しいラベルが追加される。この結果、abは全く同じIDを持つオブジェクトを指すことになり、a is bTrueとなる。このように、複数の変数が同じオブジェクトを指し示す状況は、特に複雑なデータ構造を扱う際にその後の挙動に大きな影響を及ぼす。

この概念が特に重要になるのが、「代入」「シャローコピー」「ディープコピー」の三つの操作である。これらの違いを理解することは、Pythonプログラマにとって避けて通れない課題と言える。 まず「代入(=)」は、前述の例のように、既存のオブジェクトに対して単に新しいラベルを付ける操作であり、新しいオブジェクトは一切作成されない。結果として、複数のラベルが同じ一つのデータオブジェクトを指すことになるため、どちらかのラベルを使ってそのデータオブジェクトの内容を変更すると、もう一方のラベルからもその変更が反映されて見える。 次に「シャローコピー」は、新しい外側のオブジェクトは作成するが、その中に含まれる内側のオブジェクト(例えばリストの中の別のリストなど)は、新しいコピーを作成せず、元のオブジェクトへの「参照」をコピーする。これは例えるなら、新しいバインダーを用意し、元のバインダーの目次だけをコピーして入れたような状態である。章そのもの(内側のオブジェクト)は元のものと共有されている。list.copy()メソッドやlist()関数、あるいはスライス記法[:]を使ってシャローコピーを作成できる。この振る舞いは特にネストされた(入れ子になった)データ構造で問題を引き起こしやすい。 例として、original = [1, 2, [3, 4]]というリストを考える。これは、整数1、整数2、そして[3, 4]という別のリストオブジェクトの三つの要素を持つリストである。このoriginalに対してシャローコピーを作成し、shallow_copied = original.copy()とする。このとき、shallow_copiedという新しいリストオブジェクトが作成されるが、その三番目の要素はoriginalの三番目の要素である[3, 4]というリストオブジェクトと全く同じものを参照している。したがって、もしoriginal[2].append(5)のように、元のリストの内側のリストに要素を追加すると、original自体は[1, 2, [3, 4, 5]]となり、assignedも同様に[1, 2, [3, 4, 5]]となる。さらに驚くべきことに、shallow_copied[1, 2, [3, 4, 5]]と変更されてしまうのである。これは、shallow_copiedが新しい外側のリストオブジェクトであるにもかかわらず、その内側のリストオブジェクトはoriginalと共有されているために起こる現象である。 最後に「ディープコピー」は、copyモジュールのdeepcopy()関数を用いて作成される。これは、外側のオブジェクトだけでなく、その中に含まれるすべてのオブジェクトを再帰的に(つまり内側の内側のオブジェクトまでも)新しいものとしてコピーする。結果として、元のオブジェクトとは完全に独立した複製が作成されるため、どちらかのオブジェクトを変更しても、もう一方に影響を与えることはない。上記の例でdeep_copied = copy.deepcopy(original)とした場合、original[2].append(5)を実行してもdeep_copied[1, 2, [3, 4]]のまま変更されない。これは、ディープコピーが元のオブジェクトから完全に独立した新しいオブジェクト群を作成したためである。

これらの概念は、関数の引数がどのように渡されるかという点にも直接的に関連する。Pythonの関数引数は、「値渡し」でも「参照渡し」でもなく、「オブジェクト参照渡し」と理解することが最も正確である。関数が呼び出される際、引数として渡されたオブジェクトに対し、関数の仮引数名という新しいラベルが割り当てられる。つまり、関数内の仮引数も、関数外の変数と同じオブジェクトを指すことになる。 この挙動には二つのパターンが存在する。一つは「ミュータブルなオブジェクトのインプレース変更」である。リストや辞書のように内容を変更可能な(ミュータブルな)オブジェクトを関数に渡し、関数内でappend()update()のようなメソッドを使ってそのオブジェクトの内容を直接変更した場合、その変更は関数外の元のオブジェクトにも反映される。これは、関数内外の変数が同じオブジェクトを指しているため、片方からオブジェクトを変更すれば、もう片方からもその変更が見えるからである。例えば、my_list = ["hello"]というリストを定義し、def append_to_list(some_list): some_list.append("oops")という関数を呼び出すと、関数実行後のmy_list['hello', 'oops']となる。 もう一つは「ローカル変数の再代入」である。関数内で仮引数に対し、some_list = ["a", "new", "list"]のように代入演算子(=)を使って「新しいオブジェクト」を割り当てた場合、これは仮引数という「ローカルなラベル」が、新しい別のオブジェクトを指すように変更されただけである。この再代入は関数内でのみ有効であり、関数外の元の変数は依然として最初に渡されたオブジェクトを指したままであり、影響は受けない。例えば、def reassign_list(some_list): some_list = ["a", "new", "list"]という関数を呼び出した後も、my_list['hello']のままである。

これらの概念を理解することで、コードの挙動を予測し、意図しない副作用を回避できるようになる。コードを書く前には、そのデータ型がリストや辞書、セットのように内容が変更可能なミュータブルなものか、それとも整数、文字列、タプルのように内容が変更できないイミュータブルなものかを意識することが重要である。また、代入、シャローコピー、ディープコピーのどの操作を行っているのか、そして関数内でオブジェクトを直接変更するのか、それともローカルなラベルを再代入するだけなのかを明確に区別することが求められる。このような意識を持つことで、偶然動作するコードから、設計意図に基づいて確実に動作するコードへとレベルアップできるだろう。

関連コンテンツ