一覧PG-07-2 · スコープと参照

オブジェクトの共有とコピー

入れ子の辞書とリストを三方式で扱い、内部の変更と参照先の置換を比較します。デフォルト引数の共有と、タプルの中にあるリストの変更も確認します。

説明

代入はオブジェクトへ別の名前を結び付ける操作で、自動的なコピーではありません。外側をコピーしても、内側の可変オブジェクトが共有される場合があります。

入れ子の辞書とリストを三方式で扱い、内部の変更と参照先の置換を比較します。デフォルト引数の共有と、タプルの中にあるリストの変更も確認します。

1共有する別名と二種類のコピー

scoresにリストを持つ辞書を用意します。aliasは同じ辞書、shallowは浅いコピー、deepは深いコピーです。

import copy; original = {"scores": [10, 20], "label": "A"}入れ子の初期データ; alias = original同じ辞書を参照; shallow = original.copy()外側をコピー; deep = copy.deepcopy(original)内側もコピー; print(alias is original, shallow is original, shallow["scores"] is original["scores"], deep["scores"] is original["scores"])外側と内側の同一性

外側の辞書はaliasだけが同一ですが、内側scoresはshallowでも共有します。deepでは内側のリストも別になっています。

2共有する内側を変更

originalのscoresへ30を追加します。

original["scores"].append(30)内側のリストを更新; print(original, alias, shallow, deep)変更が見える範囲

original・alias・shallowには30が現れ、deepは10と20のままです。浅いコピーの外側が別でも、共有している内側を変更すると両方から見えます。

3内側の参照を別のリストへ置き換える

今度はリスト内容を変更せず、originalのscoresを新しい[99]へ置き換えます。

original["scores"] = [99]辞書の参照先を置換; print(original, alias, shallow, deep)以前のリストが残る場所

aliasは同じ辞書なので99へ変わります。shallowは以前のリスト[10, 20, 30]への参照を保持し、deepは[10, 20]です。内容の変更と参照先の置換は別の操作です。

4深いコピーも共有関係を保つ場合がある

同じ子リストを二箇所で参照する構造を深くコピーします。

child = [1]; source = [child, child]同じ子を二度参照; cloned = copy.deepcopy(source)構造を深くコピー; cloned[0].append(2)コピー側の子を変更; print(source, cloned, cloned[0] is cloned[1], cloned[0] is child)コピー内の共有と元からの分離

コピー内の二箇所は同じ新しい子を参照し、どちらも[1, 2]に見えます。元のchildは[1]です。deepcopyは既にコピーした対象を記録し、元の共有関係もコピー内で保持します。

5デフォルトのリストへ追加する関数

引数を省略したときに使うリストを、デフォルト値として定義します。

def collect(value, items=[]):定義時に作るデフォルト items.append(value)受け取ったリストを変更 return items同じリストを返す

デフォルト式[]は関数定義時に一度評価されます。省略した呼出しが毎回新しい空リストを作るわけではありません。

6引数を省略した呼び出しの間でリストが共有される

二回呼び、その結果を後から表示します。

first = collect("A"); second = collect("B")デフォルトを二度使用; print(first, second, first is second)結果同士の共有

どちらも[A, B]です。firstも同じリストを参照しているため、二回目の追加が見えます。最初に戻された結果が独立した記録として保存されたわけではありません。

7呼出しごとに新しく作る

省略時の印にNoneを使い、関数内で空リストを作ります。

def collect_new(value, items=None省略時の印): if items is None: items = []省略時だけ新規作成 items.append(value)受取先を更新 return items更新したリスト

Noneの場合だけ新しく作るため、明示的に渡した空リストはそのまま使えます。items or []では、渡した空リストも別のリストへ置き換わるので、ここではis Noneを使います。

8省略と明示的な共有を比較

省略した二呼出しと、外側のリストを渡した呼出しを確認します。

a = collect_new("A"); b = collect_new("B")別々に新規作成; external = []; returned = collect_new("C", external)明示的にリストを共有; print(a, b, a is b)省略時の独立性; print(external, returned is external)明示した共有

省略時は[A]と[B]で別のリストです。externalを渡した呼出しは同じリストへCを追加し、明示的な共有は維持します。

9タプルの中のリストは変更できる

タプルは位置の参照を差し替えられませんが、含まれるリストまで不変にするものではありません。

record = ("A", [10])リストを含むタプル; record[1].append(20)参照先のリストを変更; print(record)タプルから見える変化

タプルの二番目は同じリストのまま、その内容が[10, 20]へ変わっています。不変という性質がどの階層のどの操作に適用されるか確認します。

10タプルの位置そのものは差し替えられない

二番目を別のリスト[99]へ代入してみます。

record[1] = [99]タプルの位置への再代入

TypeErrorになります。直前のappendは内側のリストの操作、今回はタプルの位置への代入です。同じrecord[1]が出ても、変更対象が異なります。

まとめ

コピーの外側だけでなく、内側の共有関係まで確認します。

デフォルト値や不変コンテナーでも、内部の可変値が共有される場合があります。

操作履歴と解説を振り返れます。

Python 3

1
import copy; original = {"scores": [10, 20], "label": "A"}; alias = original; shallow = original.copy(); deep = copy.deepcopy(original); print(alias is original, shallow is original, shallow["scores"] is original["scores"], deep["scores"] is original["scores"])
True False True False
2
original["scores"].append(30); print(original, alias, shallow, deep)
{'scores': [10, 20, 30], 'label': 'A'} {'scores': [10, 20, 30], 'label': 'A'} {'scores': [10, 20, 30], 'label': 'A'} {'scores': [10, 20], 'label': 'A'}
3
original["scores"] = [99]; print(original, alias, shallow, deep)
{'scores': [99], 'label': 'A'} {'scores': [99], 'label': 'A'} {'scores': [10, 20, 30], 'label': 'A'} {'scores': [10, 20], 'label': 'A'}
4
child = [1]; source = [child, child]; cloned = copy.deepcopy(source); cloned[0].append(2); print(source, cloned, cloned[0] is cloned[1], cloned[0] is child)
[[1], [1]] [[1, 2], [1, 2]] True False
5
def collect(value, items=[]): items.append(value) return items
6
first = collect("A"); second = collect("B"); print(first, second, first is second)
['A', 'B'] ['A', 'B'] True
7
def collect_new(value, items=None): if items is None: items = [] items.append(value) return items
8
a = collect_new("A"); b = collect_new("B"); external = []; returned = collect_new("C", external); print(a, b, a is b); print(external, returned is external)
['A'] ['B'] False
['C'] True
9
record = ("A", [10]); record[1].append(20); print(record)
('A', [10, 20])
10
record[1] = [99]
Traceback (most recent call last):
  File "<console>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment

演習完了