0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Pythonの『リストコピーの罠』と『変更できない』オブジェクトという概念の明示に向けて

Last updated at Posted at 2023-08-24

Pythonの『リストコピーの罠』と『変更できない』オブジェクトという概念の明示に向けて

『リストコピーの罠』

pythonの*による『リストコピーの罠』はよく知られている。公式サイトから例を引用する。

Python よくある質問 » プログラミング FAQ » 多次元のリストを作るにはどうしますか?

A = [[None] * 2] * 3
print(A)
# [[None, None], [None, None], [None, None]]

A[0][0] = 5
print(A)
# [[5, None], [5, None], [5, None]]

これは、*が、等価な別オブジェクトを作るのではなく、同一オブジェクトへの参照を複製するからなのだろう。

公式サイトで紹介されている、模範的な回避方法は以下の通りである。

A = [[None] * 2 for _ in range(3)]

*を繰り返し処理(ここではリスト内包表記)に変更するというものだが、ここで、[None] * 2の部分は[None for _ in range(2)]に書き直されること無く維持されていることに気づく。

このことを考えると、『リストコピーの罠』は、単に*のせいだけでないことがわかる。

『変更できない』オブジェクト

少し書き直して、次のように分解してみよう。

containee = [None] * 2
print(type(containee), containee)
# <class 'list'> [None, None]

a = [containee] * 3
print(a)
# [[None, None], [None, None], [None, None]]

説明のために、1行目のcontainee変数が指すオブジェクトのことをcontaineeオブジェクトとよぶことにする。すなわち、こうして作られたリストaは、containeeオブジェクトを参照することになる。

aのすべての要素はcontaineeオブジェクト(への参照)であるため、containeeオブジェクトがなんらかの形で変わると、aのすべての要素が変わることになる。

containee[0] = 5
print(a)
# [[5, None], [5, None], [5, None]]

containee.append("a")
print(a)
# [[5, None, 'a'], [5, None, 'a'], [5, None, 'a']]

続いて、containeeオブジェクトがリストではなくNoneだった場合を考えよう。

containee = None
print(type(containee), containee)
# <class 'NoneType'> None

a = [containee] * 3
print(a)
# [None, None, None]

この場合も、aのすべての要素はcontaineeオブジェクト(への参照)である。要は、containeeオブジェクトがリストであろうとNoneであろうと、intであろうとstrであろうと、containeeオブジェクトがなんらかの形で変わると、aのすべての要素が変わることになる。

しかしこのケースでcontaineeオブジェクトがリストの場合と異なるのは、Noneが『変更できない』オブジェクトであり、すなわち「containeeオブジェクトがなんらかの形で変わる」という条件が満たされることはないということである。

したがって、冒頭で紹介した模範的な回避方法の例において、[None] * 2の部分が[None for _ in range(2)]に書き直されること無く維持されているのは、このリストのcontaineeオブジェクトが『変更できない』オブジェクトであるNoneであるためと言える。

『リストコピーの罠』は、第一に*が同一オブジェクトへの参照をコピーすること、第二に中身が『変更できる』オブジェクトである(containeeオブジェクトがリスト等である)こと、の2つが原因である。

では、『リストコピーの罠』にはまることがないcontaineeオブジェクトである、『変更できない』オブジェクトとは何なのだろうか。

『変更できない』オブジェクトはイミュータブルオブジェクトか

上記の文章では、「containeeオブジェクトがなんらかの形で変わる」という条件が満たされるか満たされないかについて、『変更できる』オブジェクト vs 『変更できない』オブジェクトという言葉を使った。

『変更できる』オブジェクト vs 『変更できない』オブジェクトは、一見すると、ミュータブルオブジェクト vs イミュータブルオブジェクトのようである。ほとんどの場合において、実際にそうである。

しかし詳細に検討すると、今ここで『変更できない』オブジェクトと呼ぶべきものはイミュータブルオブジェクトとは完全に一致しないことがわかる。タプルはイミュータブルであるが、「リストを要素として持つタプル」の例で問題が起きる。

containee = ([None],)
print(type(containee), containee)
# <class 'tuple'> ([None],)

a = [containee] * 3
print(a)
# [([None],), ([None],), ([None],)]

この例では『リストコピーの罠』が発生する

containee[0][0] = 5
print(a)
# [([5],), ([5],), ([5],)]

「リストを要素として持つタプル」はイミュータブルだが、『リストコピーの罠』を誘発するオブジェクトなのである。タプルに限らず、他のオブジェクトへの参照を持つ、いわゆるコンテナオブジェクトはこの可能性を秘めている。

公式サイトの記述は以下の通りである。

Python 言語リファレンス » 3. データモデル » 3.1. オブジェクト、値、および型

mutable なオブジェクトへの参照を格納している immutableなコンテナオブジェクトの値は、その格納しているオブジェクトの値が変化した時に変化しますが、コンテナがどのオブジェクトを格納しているのかが変化しないのであれば immutable だと考えることができます。したがって、immutable かどうかは値が変更可能かどうかと完全に一致するわけではありません

タプル自身はイミュータブルであり、実際参照は変わっていないなので、原理的にはcontaineeオブジェクトが変わったわけではない。変わったのはcontaineeオブジェクト・オブ・containeeオブジェクトである(リストはミュータブル)。しかし、実用的には、このケース(「mutable なオブジェクトへの参照を格納している immutableなコンテナオブジェクト」)は『変更できる』オブジェクトと呼びたい。

結局のところ、『変更できる』オブジェクト vs 『変更できない』オブジェクトは、ミュータブルオブジェクト vs イミュータブルオブジェクトとは異なるようだ。

我々は、『変更できない』オブジェクトの最適な呼称を求めている

『変更できない』オブジェクトは原子オブジェクトか

ある個人サイト、民主主義に乾杯 » リストの初期化 » 変更できないオブジェクト では、我々と同じ問題に取り組み、そして同様に『変更できない』オブジェクトという呼称が用いられている。

このサイトでは、copy.deepcopy()の実装を参照して、_deepcopy_atomicを用いてコピーされるものが『変更できない』オブジェクトであると述べられている。

_deepcopy_dispatch = d = {}

def _deepcopy_atomic(x, memo):
    return x
d[types.NoneType] = _deepcopy_atomic
d[types.EllipsisType] = _deepcopy_atomic
d[types.NotImplementedType] = _deepcopy_atomic
d[int] = _deepcopy_atomic
d[float] = _deepcopy_atomic
d[bool] = _deepcopy_atomic
d[complex] = _deepcopy_atomic
d[bytes] = _deepcopy_atomic
d[str] = _deepcopy_atomic
d[types.CodeType] = _deepcopy_atomic
d[type] = _deepcopy_atomic
d[range] = _deepcopy_atomic
d[types.BuiltinFunctionType] = _deepcopy_atomic
d[types.FunctionType] = _deepcopy_atomic
d[weakref.ref] = _deepcopy_atomic
d[property] = _deepcopy_atomic

このatomicという名称は、これらが他オブジェクトへの参照を持たず不可分であるという点で『原子的』である、ということだろうか(strが「不可分」というのは直感とは異なるが)。これを原子オブジェクトと呼ぼう。上記のサイトでは、『変更できない』オブジェクトとは原子オブジェクトのことであると考えられているようだ。

とはいえ、『変更できない』オブジェクトを型・クラスで定義することには満足できない。すでに見たように、タプルは原子オブジェクトでは無いが、タプル自身はイミュータブルで、『変更できない』オブジェクトとして扱われる可能性がある。

では、タプルのような両性的な特徴を持つものを単に例外扱いすれば良いのだろうか。そうもいかないようだ。我々は、『変更できない』オブジェクトとは原子オブジェクトのことであるという考えに反する決定的な例を発見した。frozensetは(定義的にも直感的にも)原子オブジェクトではないが、常に『変更できない』オブジェクトなのである。

現在地

『リストコピーの罠』にはまらないオブジェクト=『変更できない』オブジェクトに、満足できる既存の定義や名称は無いようである。

『変更できない』オブジェクトの必要十分条件はおそらく、他オブジェクトを参照していない非コンテナオブジェクトか、イミュータブルオブジェクトのみを参照しているコンテナオブジェクトである。

暫定的に『再帰的にイミュータブルなオブジェクト』あるいは『再帰的不変オブジェクト』と呼ぶことにする。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?