12. 可変と不変#
Pythonのオブジェクトには不変な(immutable)ものと、可変な(mutable)なものがある。
不変: 整数、浮動小数点数、文字列、タプルなど
可変: リスト、辞書、集合など
12.1. 不変なオブジェクト#
整数値が不変とはどういうことか? 実際に変数x
に整数値1
を代入して、変数x
が参照しているオブジェクトの識別値をid
関数で調べてみる。なお、オブジェクトの識別値は、(Pythonインタプリタの実装にもよるが)オブジェクトが格納されているメモリ空間上のアドレスに相当する。
x = 1
id(x)
140283206549744
続いて、変数x
に1を加算すると、変数x
の識別値も変化する。
x += 1
x
2
id(x)
140283206549776
2つのオブジェクトに異なる識別値が割り当てられた場合、それらのオブジェクトの実体(メモリ空間上で格納されている場所)は異なることを意味する。先ほどのコードでは、変数x
が参照しているオブジェクトの値(1
)が変更されたのではなく、変数x
の参照先が整数2
を表すオブジェクトに変更されたことを表している。このように、不変なオブジェクトを参照している変数の値を変更する時は、オブジェクトの内容を変更するのではなく、その変数の参照先が更新後の値を表すオブジェクトに変更される。
以上の動作は、単なる代入でも同様である。代入文の実行後に変数x
の識別値が変化する。
x = 0
id(x)
140283206549712
代入文の本質的な動作は「左辺の変数の参照先を右辺のオブジェクトに変更する」ことである。例えば、以下の代入文では変数y
と変数x
は同一のオブジェクトを参照することになる。ゆえに、変数x
とy
の識別値は一致し、変数x
とy
の評価結果は同じになる。
y = x
x
0
id(x)
140283206549712
y
0
id(y)
140283206549712
x
とy
の値は等しい。
x == y
True
is
演算子は2つのオブジェクトの識別値が等しいかどうかを評価する。以下の結果からも、x
とy
は同一のオブジェクトを参照していることが分かる。
x is y
True
ここで、変数y
に値をインクリメントすると、変数x
とy
の参照先が同じなので、変数x
の値も変更されてしまうのではないかと思うかもしれない。ところが、y
が参照しているオブジェクト(=x
が参照しているオブジェクト)が不変であるため、変数y
が参照しているオブジェクトの値を変更するのではなく、変数y
の参照先がインクリメント後の計算結果を格納するオブジェクトに変更される。
y += 1
したがって、y += 1
を実行したことにより、変数x
とy
は異なるオブジェクトを参照するようになる。これにより、変数x
の値は変更されず、変数y
の値が変更される、という(我々が通常期待する通りの)動作になる。
x
0
id(x)
140283206549712
y
1
id(y)
140283206549744
x == y
False
x
とy
の識別値は異なるので、以下の評価結果も偽となる。
x is y
False
12.2. 可変なオブジェクト#
オブジェクトが可変のときはどのようになるのか? 実際に変数x
にリストを代入して、変数x
の識別値を調べてみる。
x = [1]
x
[1]
id(x)
140283101596992
続いて、変数x
に要素を追加してみる。先ほどの不変のオブジェクトに対する操作とは異なり、変数x
の識別値は変化しない。
x += [1]
x
[1, 1]
id(x)
140283101596992
これは、変数x
が参照しているオブジェクトの内容が直接変更されたことを意味している。このように、可変なオブジェクトを参照している変数の値を変更する操作を行うと、その変数の参照先は変更されず、オブジェクトの中身が変更される。
先ほども説明したとおり、代入文の本質的な動作は「左辺の変数の参照先を右辺のオブジェクトに変更すること」である。例えば、以下の代入文では変数y
と変数x
は同一のオブジェクトを参照することになる。ゆえに、変数x
とy
の識別値は一致し、変数x
とy
の評価結果は同じになる。
y = x
x
[1, 1]
y
[1, 1]
x == y
True
x is y
True
ここで、変数y
に要素を追加すると、変数x
とy
の参照先が同じなので、変数x
の値も変更されたように見える。実際には、変数x
とy
は全く同一のオブジェクトを指しており、そのオブジェクトの内容を更新したので、当然の結果と言える。同じオブジェクトの内容を変数x
やy
を通して評価しているだけである。
y += [1]
x
[1, 1, 1]
id(x)
140283101596992
y
[1, 1, 1]
id(y)
140283101596992
x == y
True
x is y
True
変数x
が参照しているリストのコピーを作成し、変数z
に代入する。x
とz
の内容は等しいが、x
とz
の識別値は異なる。
z = x[:]
x
[1, 1, 1]
id(x)
140283101596992
z
[1, 1, 1]
id(z)
140283101495936
x
とz
の内容(要素)は等しいが、x
とz
は同一のオブジェクトを指しているわけではない。
x == z
True
x is z
False
12.3. 関数への参照渡しと不変・可変#
Pythonでは、関数の引数はすべて参照渡しとなる。関数にオブジェクトの参照を渡すと、原理上はその関数内でオブジェクトへの内容を変更できる。
ところが、数値や文字列などの不変なオブジェクトを関数の引数として変数で渡した場合、関数内で引数として渡された変数を変更しても、呼び出し元には影響が波及しない。
def add(x):
x += 1
x = 1
add(x)
x
1
比較のため、以上のコードをC言語風に書くと、次のようになる。これを読む限りは、呼び出し元の変数x
の値が変更されそうに思える。
void add(int *x)
{
(*x) += 1;
}
int main()
{
int x = 1;
add(&x);
printf("%d\n", x);
return 0;
}
しかしながら、変数x
のオブジェクトは不変であるため、add
関数内でx
の値を更新する際に、変数x
の識別子が変化する。これは、先ほど説明した不変なオブジェクトへの値の変更の動作と一貫している。この動作のため、関数に不変なオブジェクトを参照渡ししても、関数内でそのオブジェクトの値が変更されることなく、値の変更の影響が関数内に留まる。
def add(x):
print('更新前の識別値', id(x))
x += 1
print('更新後の識別値', id(x))
a = 1
add(a)
print('呼び出し元の識別値', id(a))
a
更新前の識別値 140283206549744
更新後の識別値 140283206549776
呼び出し元の識別値 140283206549744
1
これに対し、リストを参照渡しで関数に与えると、その関数内でそのリストの内容を変更できるし、その変更結果は関数の呼び出し元にも波及する。
def add(x):
x += [1]
x = [1]
add(x)
x
[1, 1]
これは、変数x
のオブジェクト(リスト)は可変であるため、add
関数内でx
に要素を追加する際に、変数x
の識別子が変化しない。これも、先ほど説明した可変なオブジェクトへの変更の動作と一貫している。この動作のため、関数に可変なオブジェクトを参照渡しをした場合、関数内でそのオブジェクトの内容が変更されると、その影響が関数の呼び出し元にも波及する。
def add(x):
print('更新前の識別値', id(x))
x += [1]
print('更新後の識別値', id(x))
a = [1]
add(a)
print('呼び出し元の識別値', id(a))
a
更新前の識別値 140283101599232
更新後の識別値 140283101599232
呼び出し元の識別値 140283101599232
[1, 1]