Python のリストの扱いで注意すること

  • 58
    いいね
  • 10
    コメント

こんなコードがあったとさ(1)

a = 1
b = a

# 後からaの値を変更したくなった
a = 2
print(a, b)

実行結果

2 1

普通ですね。

こんなコードがあったとさ(2)

a = [1,2,3,4,5]
# aというリストの中身をコピーしたbというリストを作った(つもり)
b = a

# 後からa[2]の値を変更した(つもり)
a[2] = 5
print(a)
print(b)

実行結果

[1, 2, 5, 4, 5]
[1, 2, 5, 4, 5]

意図した通りに動いてないですね。

こんなコードがあったとさ(3)

# aという5x5の多次元配列的なリストを生成し、0で初期化
a = [[0]*5]*5

# 3行3列目の要素を1にした(つもり)
a[2][2] = 1
print(a)

実行結果

[[0, 0, 1, 0, 0], [0, 0, 1, 0, 0], [0, 0, 1, 0, 0], [0, 0, 1, 0, 0], [0, 0, 1, 0, 0]]

めちゃくちゃですね。

どうしてこうなった?

その前に、「識別値」「immutable/mutableなオブジェクトの違い」について理解しておくと、原因を理解しやすいので、まずはそれらについて解説をします1

「識別値」とは?

Pythonの全てのオブジェクトは、それぞれ固有の「識別値」(以下、IDと表記します)を持っています。
このIDは、組み込み関数id()を用いて、次のようにして確認することができます2

print(id(1)) # => 10408064
print(id('ほげふが')) # => 140040318148080

Pythonでは、同じIDを持つオブジェクトは、同じオブジェクトです。
当然、違うIDを持つオブジェクトは、違うオブジェクトです。
1'ほげふが'のIDが同じだったら、とんでもないことになりますね。

immutable/mutableって何? mutableなオブジェクトってどういうこと?

immutableなオブジェクトとは

immutableなオブジェクトとは、固有のIDを持ち、IDを変更せずに値を変更することができないオブジェクトのことです。
例えば、数値・文字列・タプルはimmutableなオブジェクトです。

どういうことやねん

これは、変数に値を代入するときに重要です。
たとえば、次のようなコードがあったとします。

a = 2
print(id(a))
a = 3
print(id(a))

この実行結果を見ると、aのIDが変化していることが分かります。
これがimmutableなオブジェクトの特性です。
immutableなオブジェクトを変数に代入するという行為は、変数にオブジェクトのクローンを生成するという行為と等しいという認識でいいかと思われます3

mutableなオブジェクトとは

mutableなオブジェクトとは、IDを変更せずに値を変更することができるオブジェクトのことです。
例えば、リストはmutableなオブジェクトです。

どういうことやねん

たとえば、次のようなコードがあったとします。

a = [1]
print(id(a))
a.append(2)
print(id(a))
del a[0]
print(id(a))

この実行結果を見ると、aのIDが変化していないことが分かります。
これがmutableなオブジェクトの特性です。
また、mutableなオブジェクトは、同じオブジェクトであっても変数に代入されるたびに違うIDを持つという特性があります。
つまり、次のコードで示されるabのIDは、異なります。

a = [1]
b = [1]
print(id(a))
print(id(b))

ややこしすぎ。

ここで

オブジェクトの値とIDを区別することで、次のようなことが言えます。
==演算子は、2つのオブジェクトの持つが等しければTrue、そうでなければFalseを返す演算子です。
is演算子は、2つのオブジェクトの持つIDが等しければTrue、そうでなければFalseを返す演算子です。
たとえば、次のようなコードがあったとします。

a = [1]
b = [1]
if a == b:
  print("ここは表示されます")
if a is b:
  print("ここは表示されません")

この実行結果は、次のようになります。

ここは表示されます

この場合、abは、値は等しいがIDが異なるオブジェクトということになります。

本題に戻ります

なぜ(2)(3)で示したようなバグが起こるのか?

a = [1]
b = a

というコードがあったとき、baと全く同一のオブジェクトをコピーすることになります。
abのIDは、当然ながら等しいです。
ここで、この次に、

a.append(2)

というコードを書くと、変数aが指すIDのリストの末尾に、IDを変更することなく2という要素を加えるという動作が行われます。
そして、Pythonの実行環境は、aのIDは変化していないから、abの指すオブジェクトは同一のものであるという解釈をします。
そのため、

print(a)
print(b)

の実行結果は、

[1, 2]
[1, 2]

になる、ということが起こるのです。ややこしい。

対策

リストのコピーを作りたい場合

スライスを使うか、copyモジュールのdeepcopy()関数を使うとよいです。

import copy
a = [1]
b = a[:]
c = copy.deepcopy(a)
print(id(a), id(b), id(c)) # 全て異なるIDを持つ。

多次元配列的なリストの要素を特定の値で初期化したい場合

for内包表記を使いましょう。

a = [[0 for i in range(5)] for j in range(5)]
a[4][4] = 1
print(a)
# => [[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 1]]

ちなみに、一次元のリストなら、次のように初期化しても大丈夫です。

a = [0]*5
a[4] = 1
print(a) # => [0, 0, 0, 0, 1]

まとめ

  • Python の世界には immutable(変更不可能) なオブジェクトと、 mutable(変更可能) なオブジェクトがある
  • オブジェクトはそれぞれ固有の ID を持っていて、id() 関数で確認できる
  • 変数の代入に見えるものは、オブジェクトの束縛と捉えたほうがよさそう
  • 多次元配列を一度に初期化したかったら for 内包表記を使うか、素直に for 文を回しましょう

参考にしたサイト


  1. 解説できるかなあ。 

  2. 下のコードのコメントに示したIDは、環境によって異なるかもしれません(ほんまか)(未検証)。 

  3. この辺から怪しいので、コメント欄につよい人が現れてほしいなあーと思っています。