【Python】 リストを変数に代入する際に気をつけること(参照渡し、copyによるコピー)

Python モジュール リスト 組込み型

リストの要素をひとつだけ変更して新しいリストを作る際に、元のリストも値が変わってしまって困ったことはありませんか?
今回は、このような悩みを解決するために、

  • リスト(に限らずPythonのオブジェクト)の代入のからくり(参照渡し)
  • copy.copy()、copy.deepcopy()を使ってデータをコピーする方法

についてまとめます。

確認した環境

  • OS: Ubuntu 16.04LTS
  • Python3.7.2@Anaconda

リストを変数に代入したけどうまく行かない!

リストaはそのままにして、
要素をひとつだけ変更して新しいリストbをつくりたい!

と思い、下記のようなコードを書きました。

>>> a = [1, 2, 3, 4]
>>> b = a
>>> b[0] = 'X'

結果は下記の通り、、

>>> a
['X', 2, 3, 4]
>>> b
['X', 2, 3, 4]

リストbのみを変更したかったのですが、元のリストaも書き換わってしまいました

数値の場合は以下の通り変更できたのですが、リストの場合は異なるようです。

# 数値の場合
>>> a = 1
>>> b = a
>>> b =2
>>> a
1
>>> b
2

代入処理のからくり

Pythonでは、リスト(に限らずオブジェクト)を変数に代入する処理は、値そのものを変数に代入するのではなく、リストの「参照」を変数に代入します。

  • 「参照(reference)」とは、データの存在する箇所を示す値であり、それぞれID(=識別子)が付与されます。
  • IDは、id()関数で調べることができます。

そして、実際の値はその参照先のメモリに格納されます。

リストaを’=’で変数bに代入すると、Pythonはリストの「参照」を新しい変数bにコピーします。
その結果、下記の例の様にリストaとリストbは同じID(識別子)、つまり同じ「参照」を示します。

>>> a = [1, 2, 3, 4]

#リストaの参照をbにコピー
>>> b = a

#リストaの識別子
>>> id(a)
140099874343240

#リストbの識別子
>>> id(b)
140099874343240

ここでリストbの要素を変更すると、リストbの参照先の値が変更されます。
これに伴い、下記の様に同じIDを参照しているリストaの値も変更されたように見えます。
これが今回の事象のからくりです。

# リストbの要素を変更
>>> b[0] = 99
>>> b
[99, 2, 3, 4]

# 同じIDを参照しているリストaの値も変わったように見える
>>> a
[99, 2, 3, 4]

ここで、リストbに新たなリスト型データを代入してみます。

# 新たなリスト型データを代入
>>> b = [5, 6, 7, 8]

# 今度はリストaは変わらず
>>> a
[99, 2, 3, 4]

この場合はリストaの値は変わりません。
下記に示すように、変数bに新しいリスト型データを代入したことにより新しいオブジェクトが生成され、別のIDを参照するようになった為です。

#リストbの識別子(リストaと異なっている)
>>> id(b)
140099895867784

#リストaの識別子
>>> id(a)
140099874343240

ちなみに、先に数値は今回の様な現象は置きないと記載しました
これは、変数bに新しい数値を代入した時点で新しいオブジェクトが生成された(=参照先が変わった)為です。

>>> a = 1
>>> b = a
>>> b =2 ★ここで新しいオブジェクトが生成

copyモジュールによるリストのコピー

元の値を変更せずにリストをコピーするには、Pythonの標準ライブラリのcopyモジュールを使います。このモジュールは、

を提供しています。これらを使ってコピーすることで、別のIDを参照するようになります。
具体例を以下に示します。

#copyモジュールをインポートするのを忘れずに
>>> import copy 

# リストaのデータを変数bにコピー
>>> a = [1, 2, 3, 4]
>>> b = copy.copy(a)
>>> b[0] = 'X'

#リストaの要素は変更されない
>>> a
[1, 2, 3, 4]

#リストbのみ変更される
>>> b
['X', 2, 3, 4] 

#リストa、リストbそれぞれのIDを確認
>>> id(a)
140355191620040
>>> id(b)
140355191620104 

copy()関数を使うことで、リストa、リストbにそれぞれ異なるIDがアサインされたことがわかります。

<参考>copy.copy()はリストだけでなく他のシーケンス型(タブル、文字列)や辞書もコピーすることができます。

辞書のようなミュータブルなオブジェクトは、リストと同様に参照が変わります。

# 辞書の場合
>> d = {'a':1, 'b':2}
>>> e = d
>>> e
{'a': 1, 'b': 2}

# 参照が同じ
>>> id(d)
139912673832320
>>> id(e)
139912673832320


# copy.copy()でコピー後は参照が変わる
>>> f = copy.copy(d)
>>> f
{'a': 1, 'b': 2}
>>> id(f)
139912673910288

タブル、文字列のようなイミュータブルなオブジェクトは参照が変わりません。

# タプルの場合
>>> a = (1, 2, 3)
>>> b = a
>>> b
(1, 2, 3)

# 参照が同じ
>>> id(a)
139912673989616
>>> id(b)
139912673989616

# copy.copy()でコピー後も参照が同じ
>>> c = copy.copy(a)
>>> c
(1, 2, 3)
>>> id(c)
139912673989616

また、入れ子になったリストなど、他のオブジェクトを含む複合オブジェクトをコピーする際は、copy.deepcopy()を使います。

リストをコピーする他の方法

  • s.copy()を使う
    リストはcopy()メソッドをサポートしています。下記に使いかたを示します。

    >>> l = [1, 2, 3]
    
    # s.copy()を使ってコピー
    >>> l.copy()
    [1, 2, 3]
    >>> m = l.copy()
    
    # コピー前後でIDが変更
    >>> id(l)
    139912673826120
    >>> id(m)
    139912673911560
  • スライス表記を使う
    スライス表記を使っても上記同じ結果が得られます。
  • >>> l = [1, 2, 3]
    
    # スライス表記
    >>> m = l[:]
    >>> m
    [1, 2, 3]
    
    # コピー前後でIDが変更
    >>> id(m)
    139912695268744
    >>> id(l)
    139912673827208

辞書をコピーする他の方法

辞書もcopy()メソッドをサポートしていますので、こちらのほうが簡単に書けると思います。

>>> d = {'a':1, 'b':2}

# dict.copy()を使ってコピー
>>> e = d.copy()
>>> e
{'a': 1, 'b': 2}

# コピー前後でIDが変更
>>> id(d)
139912673910360
>>> id(e)
139912673910432

まとめ

リストを別の変数にコピーして新しいリストと使い分けるには以下に注意する必要があります。

  • リストの代入処理の仕組み → リストの値そのものでなく、「参照」を変数に代入
  • リストのコピー → copy.copy()、copy.deepcopy()

Learn more...

書籍でもう少し詳しく学びたい場合はこちらもどうぞ。筆者もかなり参考にさせてもらっています!

シェアする
ひびきをフォローする
Hbk project