はじめに
プログラミング初学者です。Pythonで多次元リストを作成した際に学んだことを復習のためまとめました。はじめに私が書いたコードの誤りを訂正し、次になぜそれが起こったのかを説明します。解決に時間がかかってしまったので、同じ境遇の方が早く答えに辿り着く助けになればと思いつつ、詳しい説明はほかの方にお任せしてしまいました。初学者の限界をお許しください。
更新日
(2022/08/28)内容を大幅に変更しました。一番のミスは「参照の値渡し」を「参照渡し」だと誤認していたことです。正直、いただいたコメントが指南書として完成されているので、まずそちらをご覧いただければと思います。@shiracamusさま、@ttatsfさま、ご教示くださりありがとうございます。
環境
- Python 3.10.5
$ python --version
Python 3.10.5
発生した事象
[[0], [0, 1], [0, 1, 2], [0, 1, 2, 3], [0, 1, 2, 3, 4]]
上のようなリストを作成したく、次のようなコードを書きました。
result_list: list[list[int]] = []
num_list: list[int] = []
for num in range(0,5):
num_list.append(num)
result_list.append(num_list)
print(result_list)
しかし結果はこのようになってしまいました。
$ python main.py
[[0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4], [0, 1, 2, 3, 4]]
解決方法
Pythonが関数に引数を渡す際、参照の値渡しという方法が用いられること、そしてリストがミュータブルなオブジェクトであることにより引き起こされたものでした。解決には、.copy()
メソッドを用いるとよいそうです。
result_list: list[list[int]] = []
num_list: list[int] = []
for num in range(0,5):
num_list.append(num)
- result_list.append(num_list)
+ result_list.append(num_list.copy()) # num_listのコピーをappendする
print(result_list)
すると期待した結果が返ってきました!
$ python main.py
[[0], [0, 1], [0, 1, 2], [0, 1, 2, 3], [0, 1, 2, 3, 4]]
参照の値渡しとミュータブルについて
本稿のキーワードである「参照の値渡し」と「ミュータブル」についてご説明します。
x: int = 1
このように変数を定義したとき、Pythonは二つのオブジェクトを生成します。ひとつはx
というstr型の値を持つオブジェクト(仮にオブジェクトφと呼ぶ)、もうひとつは1
というint型の値を持つオブジェクト(仮にオブジェクトψと呼ぶ)です。このとき、それぞれのオブジェクトにはメモリアドレスという、いわゆるデータの(コンピュータ内での)住所も与えられます。ロッカーという例え1も分かりやすいと思いました。そしてオブジェクトφはオブジェクトψを指すようになり、我々がx + 1
とコーディングするだけで1 + 1
を計算してくれます。
Pythonには、id(x)
という組み込み関数があります。この関数の引数に先ほど定義したx
(オブジェクトφ)を渡すと、戻り値としてオブジェクトψのメモリアドレスを取得できます。
そして、参照の値渡しとは、関数に引数として変数を指定した際、関数にアドレスを渡す方法のことです。我々がx
と引数に入れると、オブジェクトψのアドレスが関数に渡されます。
値渡しという方法では、1
そのものを渡します。ですがこの値が1
ではなく、長い文字列や何個も要素があるリストである可能性を考えると、データの大きくないアドレスのほうが扱いやすいというメリットがあります。
また、(私が誤認した)参照渡しという方法もあります。メモリアドレスとは別の話です。
def add(num):
return num + 10:
x = 1
y = add(x)
print(y) // 11
print(x) // 1
Pythonは参照渡しできない言語なのでこのような結果になりますが、このx
も11
になる、つまり渡された変数そのものを変えてしまうのが参照渡しです。2
そして、リスト型がミュータブルであったためにこの問題に遭遇しました。ミュータブルとは「(変数定義後に)変更可」という意味で、「変更不可」なオブジェクトはイミュータブルといいます3。
例えば、リスト型はミュータブルなのでlst = ['H', 'a', 'l', 'l', 'o']
に対してlst[1] = 'e'
と一部を変更することができますが、イミュータブルな文字列型ではstr = 'Hallo'
から一文字だけ変更することはできません。
result_list: list[list[int]] = []
num_list: list[int] = []
for num in range(0,5):
num_list.append(num)
result_list.append(num_list)
print(result_list)
もとのコードではfor
文で5回num_list
を引数に指定していますが、そのすべてが同じアドレスを渡していたため、変更後のオブジェクトを5回追加するという結果になってしまったようでした。
イミュータブルなオブジェクトの場合、値を変えようとstr = 'Hello'
、x = 10
、x += 1
などと再代入すると思いますが、このとき新しくオブジェクトが生成され、str
やx
は新しいほうのオブジェクトを指すようになります。このときアドレスも変わるので、同様の事象は起こらず、今まで勉強しなくても済んでいたようでした。