12. 可変と不変#

Pythonのオブジェクトには不変な(immutable)ものと、可変な(mutable)なものがある。

  • 不変: 整数、浮動小数点数、文字列、タプルなど

  • 可変: リスト、辞書、集合など

12.1. 不変なオブジェクト#

整数値が不変とはどういうことか? 実際に変数xに整数値1を代入して、変数xが参照しているオブジェクトの識別値をid関数で調べてみる。なお、オブジェクトの識別値は、(Pythonインタプリタの実装にもよるが)オブジェクトが格納されているメモリ空間上のアドレスに相当する。

x = 1
id(x)
9788608

続いて、変数xに1を加算すると、変数xの識別値も変化する。

x += 1
x
2
id(x)
9788640

2つのオブジェクトに異なる識別値が割り当てられた場合、それらのオブジェクトの実体(メモリ空間上で格納されている場所)は異なることを意味する。先ほどのコードでは、変数xが参照しているオブジェクトの値(1)が変更されたのではなく、変数xの参照先が整数2を表すオブジェクトに変更されたことを表している。このように、不変なオブジェクトを参照している変数の値を変更する時は、オブジェクトの内容を変更するのではなく、その変数の参照先が更新後の値を表すオブジェクトに変更される。

以上の動作は、単なる代入でも同様である。代入文の実行後に変数xの識別値が変化する。

x = 0
id(x)
9788576

代入文の本質的な動作は「左辺の変数の参照先を右辺のオブジェクトに変更する」ことである。例えば、以下の代入文では変数yと変数xは同一のオブジェクトを参照することになる。ゆえに、変数xyの識別値は一致し、変数xyの評価結果は同じになる。

y = x
x
0
id(x)
9788576
y
0
id(y)
9788576

xyの値は等しい。

x == y
True

is演算子は2つのオブジェクトの識別値が等しいかどうかを評価する。以下の結果からも、xyは同一のオブジェクトを参照していることが分かる。

x is y
True

ここで、変数yに値をインクリメントすると、変数xyの参照先が同じなので、変数xの値も変更されてしまうのではないかと思うかもしれない。ところが、yが参照しているオブジェクト(=xが参照しているオブジェクト)が不変であるため、変数yが参照しているオブジェクトの値を変更するのではなく、変数yの参照先がインクリメント後の計算結果を格納するオブジェクトに変更される。

y += 1

したがって、y += 1を実行したことにより、変数xyは異なるオブジェクトを参照するようになる。これにより、変数xの値は変更されず、変数yの値が変更される、という(我々が通常期待する通りの)動作になる。

x
0
id(x)
9788576
y
1
id(y)
9788608
x == y
False

xyの識別値は異なるので、以下の評価結果も偽となる。

x is y
False

12.2. 可変なオブジェクト#

オブジェクトが可変のときはどのようになるのか? 実際に変数xにリストを代入して、変数xの識別値を調べてみる。

x = [1]
x
[1]
id(x)
140212299190208

続いて、変数xに要素を追加してみる。先ほどの不変のオブジェクトに対する操作とは異なり、変数xの識別値は変化しない。

x += [1]
x
[1, 1]
id(x)
140212299190208

これは、変数xが参照しているオブジェクトの内容が直接変更されたことを意味している。このように、可変なオブジェクトを参照している変数の値を変更する操作を行うと、その変数の参照先は変更されず、オブジェクトの中身が変更される。

先ほども説明したとおり、代入文の本質的な動作は「左辺の変数の参照先を右辺のオブジェクトに変更すること」である。例えば、以下の代入文では変数yと変数xは同一のオブジェクトを参照することになる。ゆえに、変数xyの識別値は一致し、変数xyの評価結果は同じになる。

y = x
x
[1, 1]
y
[1, 1]
x == y
True
x is y
True

ここで、変数yに要素を追加すると、変数xyの参照先が同じなので、変数xの値も変更されたように見える。実際には、変数xyは全く同一のオブジェクトを指しており、そのオブジェクトの内容を更新したので、当然の結果と言える。同じオブジェクトの内容を変数xyを通して評価しているだけである。

y += [1]
x
[1, 1, 1]
id(x)
140212299190208
y
[1, 1, 1]
id(y)
140212299190208
x == y
True
x is y
True

変数xが参照しているリストのコピーを作成し、変数zに代入する。xzの内容は等しいが、xzの識別値は異なる。

z = x[:]
x
[1, 1, 1]
id(x)
140212299190208
z
[1, 1, 1]
id(z)
140212299141248

xzの内容(要素)は等しいが、xzは同一のオブジェクトを指しているわけではない。

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
更新前の識別値 9788608
更新後の識別値 9788640
呼び出し元の識別値 9788608
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
更新前の識別値 140212299280576
更新後の識別値 140212299280576
呼び出し元の識別値 140212299280576
[1, 1]