読者です 読者をやめる 読者になる 読者になる

amacbee's blog

Pythonの話とかデータの話とか酒の話とか

Pythonの値渡しと参照渡し

これはPython Advent Calendar 2016 7日目の記事です.
アドベントカレンダーの日程を勘違いしてしまっていたので,社内向けに作ったPython資料を使いまわすことでお茶を濁します。。。

Pythonの値渡しと参照渡しについてまとめてみます.

Pythonでは,変数や関数に値を渡す場合すべて参照渡しで渡されています.例えば挙動だけ見れば値渡しに見えるint型でも,値が変更されるまでは元の値と同一のオブジェクトを参照しています.
例を見てみましょう.

>>> def foo(a):
...   print(a, id(a))
...   a += 1
...   print(a, id(a))
...
>>> b = 0
>>> print(b, id(b))
0 4297514880  # 'b' 用の領域が確保されている
>>> foo(b)
0 4297514880  # ここまで 'b' と同じ領域を参照している
1 4297514912  # ここで 'a' 用に別領域が確保される
>>> print(b, id(b))
0 4297514880  # 'b' の値は変更されていない

渡された値を変更した際に元の値自体も変更されてしまうかどうかは,オブジェクトの型に依存します.

  • 変更不可(Immutable)な型
    • int, float, str, tuple, bytes, frozenset 等
  • 変更可能(Mutable)な型
    • list, dict, set, bytearray 等

変更可能な型では,渡された値が変更されると元の値も変更されてしまいます.(新しく領域が確保されません)

>>> def foo(a):
...   print(a, id(a))
...   a.append(1)
...   print(a, id(a))
...
>>> b = [0]
>>> print(b, id(b))
[0] 4332159560
>>> foo(b)
[0] 4332159560     # 'b' と同じ領域を参照している
[0, 1] 4332159560  # 'a'に変更を加えても参照先が変更されない
>>> b
[0, 1]  # 'a'が変更された影響で'b'の値も変更されている

変更可能な型のオブジェクトをコピーして別のオブジェクトとして利用したい場合は,copy.deepcopy()関数を利用します.

>>> import copy
>>> a = [1, 2]
>>> print(a, id(a))
[1, 2] 4447440008
>>> b = copy.deepcopy(a)  # deepcopy()関数を利用して,値が同じだが参照先の違うオブジェクトを作成する
>>> b.append(3)
>>> print(b, id(b))  # 'a' とは参照先の違うオブジェクトが生成されている
[1, 2, 3] 4447398408
>>> print(a, id(a))  # 'b' の変更の影響を受けない
[1, 2], 4447440008

Pythonの参照渡しではまりがちなケースを2つほど紹介しておきます.

  • 変数の初期化時
  • 変更可能な値をデフォルト引数として使う時

まず変数の初期化のケースについて紹介します.例えば以下のような例です.

>>> a = [[]] * 2
>>> a
[[], []]
>>> a[0].append(1)  # aの0番目のリストに1を追加したい
>>> a
[[1], [1]]          # a中の全てのリストに1が追加されてしまっている

a = [[]] * 2の操作で,参照先が同じオブジェクトを2つ生成してしまっています.

# a[0]およびa[1]がそれぞれ同じ領域を参照している
>>> id(a[0])
4447397000
>>> id(a[1])
4447397000

上記のような初期化を行いたいケースでは,リスト内包表記を活用しましょう.

# リスト内包表記を使うと問題ない
>>> a = [[] for _ in range(2)]
>>> a
[[], []]
>>> a[0].append(1)
>>> a
[[1], []]
>>> id(a[0])
4447397512
>>> id(a[1])
4447396936

次に,変更可能な値をデフォルト引数として使うケースについて説明します.デフォルト引数とは,Pythonのキーワード引数に対しデフォルト値として設定する引数のことです.

>>> class A:
...   def __init__(self, hikisuuu=[]):
...     self.hikisuuu = hikisuuu

上記のクラスでは,hikisuuuのデフォルト引数として[]が定義されています.
このデフォルト引数ですが, クラスや関数が作成されたときに一度だけ作成される という性質をもっているため,度々以下のような現象が発生します.

>>> x = A()             # Aクラスのインスタンス'x'を作成
>>> x.hikisuuu.append(1)
>>> x.hikisuuu
[1]
>>> y = A()             # Aクラスのインスタンス'y'を作成
>>> y.hikisuuu.append(2)
>>> print(y.hikisuuu, id(y.hikisuuu))  # 'x'と'y'のインスタンス変数'hikisuuu'の参照先が同じになってしまっている
[1, 2] 4447397896
>>> print(x.hikisuuu, id(x.hikisuuu))
[1, 2] 4447397896

デフォルト引数はクラスが作成された際に1度しか初期化されません.そのため,xとyは全く違うインスタンスなのにも関わらず,両者のインスタンス変数hikisuuuが共有化されてしまっています.上記のようなケースを避けるために,Pythonでデフォルト引数を定義する場合は変更不可な値を利用することをおすすめします.
あるいはAクラスは以下の通り書き換えられます.

>>> class A:
...   def __init__(self, hikisuuu=None):
...     if hikisuuu is None:
...       self.hikisuuu = []

参考