Python で mutable と immutable の違い

簡単に言えば...


値を変更できるオブジェクトのことを mutable と呼びます。
Objects whose value can change are said to be mutable;

値を変更できないオブジェクトのことを immutable と呼びます。
objects whose value is unchangeable ... are called immutable.

3. Data model — Python 3.5.5 documentation

正確に言えば...


mutable 属性に直接代入されている
 オブジェクトを取り替えられる
immutable 属性に直接代入されている
 オブジェクトのを取り替えられない


簡単な具体例

◯ mutable なオブジェクト

例えば、list 型、 dict 型、普通にユーザが定義した型は mutable です。

>>> class Person():
...   def __init__(self, name):
...     self.name = name
... 
>>> 
>>> person = Person('yaruo')
>>> 
>>> person.name
'yaruo'
>>> 
>>> # value 値を変更できた -> mutable
>>> person.name = 'yarumi' 
>>> person.name
'yarumi'
>>>
>>> lst = [1, 2, 3]
>>> 
>>> # value 値を変更できた -> mutable
>>> lst[2] = 4
>>> lst
[1, 2, 4]
>>>

◯ immutable なオブジェクト

例えば、int, str, bool と tuple のインスタンスは immutable です。

>>> a = 1
>>>
>>> # 1 の実部
>>> a.real
1
>>> 
>>> # 1 の虚部
>>> a.imag
0
>>> # 1 は immutable なので、値は変更できない。
>>> i.imag = 100
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: attribute 'imag' of 'int' objects is not writable
>>> 


exception AttributeError
属性参照 (属性参照 を参照) や代入が失敗した場合に送出されます (オブジェクトが属性の参照や属性の代入をまったくサポートしていない場合には TypeError が送出されます)。

 

>>> s = 'ランボー/怒りの脱出'
>>>
>>> s[0]
'ラ'
>>> 
>>> # value 値を変更できない -> immutable
>>> s[0] = 'チ'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'str' object does not support item assignment
>>> 


exception TypeError
組み込み演算または関数が適切でない型のオブジェクトに対して適用された際に送出されます。関連値は型の不整合に関して詳細を述べた文字列です。


正確な具体例

tuple は immutable です。しかし、その中の値を取り替えられます。

>>> # tuple は immutable
>>> t = ([1,2],[3,4,5])
>>> t
([1,2],[3,4,5])
>>>
>>> # だけど代入ができる
>>> t[0][0]=100
>>> t
([100, 2], [3, 4, 5])



◯ 値を変更できる immutable なオブジェクト

mutable object への参照を持っている immutable container object は、値が変更できますが immutable です。

mutable object への参照を持っている immutable container object の値は、参照している mutable object の値が変化させられた時に変化すると言えます。しかしながら container (an immutable container object) は immutable であると判断されます、
The value of an immutable container object that contains a reference to a mutable object can change when the latter’s value is changed; however the container (an immutable container object) is still considered immutable,

3. Data model — Python 3.5.5 documentation



なんで?どうして?

なぜなら container が所持しているオブジェクトの集合は変化していないからです。従って immutable であること (immutability) は、厳密に言えば "値が変更できないこと" と同義ではなく、もう少し複雑です。
because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.

3. Data model — Python 3.5.5 documentation







f:id:domodomodomo:20180113154538j:plain






mutable object への参照を持っている
immutable container object って何?

答え: mutable なオブジェクトが属性に代入された immutable なオブジェクト

例えば ([1, 2], [3, 4, 5]) が、そうです。
一つ一つ見ていきたいと思います。

Step1. object

「変数に代入できるもの」は、全てオブジェクトだと理解しています。

a = 1
b = 'Hello, world!'
c = [1, 2, 3, 4]
d = (1, 2, 3, 4)
e = ['a':1, 'b':2, 'c':3]

Step2. immutable object

int, strings, tuples は immutable です。

オブジェクトが mutable かどうかはその型によって決まります。例えば、数値型(int, float などの総称か)、文字列型とタプル型のインスタンスは immutable で、dict や list は mutable です。
An object’s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.

3. Data model — Python 3.5.5 documentation

Step3. container object

ほぼほぼ全てのオブジェクトが複数の属性を持っているので、ほぼほぼ全てのオブジェクトがcontainer オブジェクトだって認識でいいのではないでしょうか... int も複数の値を持ってますしね。

container - Python 言語リファレンス
他のオブジェクトに対する参照をもつオブジェクトもあります; これらは コンテナ (container) と呼ばれます。コンテナオブジェクトの例として、タプル、リスト、および辞書が挙げられます。オブジェクトへの参照自体がコンテナの値の一部です。
— ワイの注記 container について記述されている箇所の抜粋しました。タプル、リスト、および辞書など集合を表現するオブジェクトを container だと言いたい様子。ただ、この定義だと全てのオブジェクトが container に該当してしまうんじゃまいか..

コンテナ (データ型) - Wikipedia
コンテナとはオブジェクトの集まりを表現するデータ構造、抽象データ型またはクラスの総称である。

Step4. immutable container object

Step2, 3 を踏まえると...
int, str, tuple は immutable container object と言えそうですね。

Step5. mutable object への参照を持っている immutable container object

答え: mutable なオブジェクトが属性に代入された immutable なオブジェクト

タプル t がそれに該当します。さっそく値を変更できる immutable なオブジェクトを見てみましょう。

#
# a, b, c は mutable
#

class Obj():
  def __init__(self, attr):
    self.attr = attr

a = Obj('nihao')
b = Obj('hello')
c = Obj('hola')


#
# tuple t は immutable
#

#
# mutable なオブジェクト a, b, c への参照を持つ
# immutable なオブジェクト t
# 

t = (a, b, c)


#
# t はタプル immutable なので
# 値を別のオブジェクトに変更できない。
#


t[2] = Obj('konnichiwa')
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
# TypeError: 'tuple' object does not support item assignment



#
# でも タプル t の要素 t[2], c は mutable なので
# 値を別のオブジェクトに変更できる。
#

t[2].attr = "konnichiwa"

◯ まとめ


mutable 属性に直接代入されている
 オブジェクトを取り替えられる
immutable 属性に直接代入されている
 オブジェクトのを取り替えられない


immutable なクラスの一覧

immutable なクラスの一覧です。
copy 関数のコードから抜粋しました。

とりあえず、よく使うものだけ覚えておけば
いいのではないでしょうか。

  • int
  • float
  • str
  • tuple
  • bool
  • range
  • type(None)



その他にもこんなのがあります。

  • bytes
  • complex
  • frozenset
  • slice
  • type
  • type(Ellipsis)
  • type(NotImplemented)
  • types.FunctionType *これは mutable, 後述します。
  • types.BuiltinFunctionType
  • weakref.ref



以下は copy 関数のコードの抜粋です。

# ここに immutable なクラスのオブジェクトが列挙されています。
for t in (type(None), int, float, bool, complex, str, tuple,
          bytes, frozenset, type, range, slice,
          types.BuiltinFunctionType, type(Ellipsis), type(NotImplemented),
          types.FunctionType, weakref.ref):
    # _copy_dispatch[クラス] = オブジェクトをコピーする関数
    # d             [クラス] = オブジェクトをコピーする関数
    # d             [t     ] = _copy_immutable
    d[t] = _copy_immutable

8.10. copy — 浅いコピーおよび深いコピー操作 — Python 3.6.5 ドキュメント
cpython/copy.py at 3.6 · python/cpython · GitHub

◯ copy 関数の中身

抜粋したコードが本当に immutable を列挙しようとしているのかを確認するために copy 関数の中身を、すこし追って見たいと思います。

def copy(x):
    """Shallow copy operation on arbitrary Python objects.
    See the module's __doc__ string for more info.
    """

    cls = type(x)
    # _copy_dispatch は、組込型の copy 関数を返す辞書です。
    copier = _copy_dispatch.get(cls)
    if copier:
        return copier(x)

    ... # 省略



こうやって辞書を dispatch って表現することもあるんですね。
知らなかった... orz
ディスパッチテーブル | 新人プログラマに知ってもらいたい...
Python に switch や case 文がないのはなぜですか?

とはいえ「_copy_dispatch は、組込型のコピー関数を辞書です。」って何?
って感じなので、さらに中身を見てみます。

組込型は int, str, list など最初から Python にはいってるクラスのこと
4. 組み込み型 — Python 3.6.5 ドキュメント

# _copy_dispatch は、組込型のコピー関数を返す辞書です。
# しかし、クラスオブジェクトが hashable だったとは....
_copy_dispatch = d = {}

# 使い方
# _copy_dispatch[クラス] = オブジェクトをコピーする関数
# d             [クラス] = オブジェクトをコピーする関数



#
# 1. immutable な組込型の copy 関数を辞書に代入。
#

# immutable なクラスは、そのままインスタンスオブジェクトをそのまま返す
def _copy_immutable(x):
    return x

# ここに immutable なクラスのオブジェクトが列挙されています。
for t in (type(None), int, float, bool, complex, str, tuple,
          bytes, frozenset, type, range, slice,
          types.BuiltinFunctionType, type(Ellipsis), type(NotImplemented),
          types.FunctionType, weakref.ref):
    # _copy_dispatch[クラス] = オブジェクトをコピーする関数
    # d             [クラス] = オブジェクトをコピーする関数
    # d             [t     ] = _copy_immutable
    d[t] = _copy_immutable

t = getattr(types, "CodeType", None)
if t is not None:
    d[t] = _copy_immutable



#
# 2. mutable な組込型の copy 関数を辞書に代入。
#

# _copy_dispatch[クラス] = オブジェクトをコピーする関数
# d             [クラス] = オブジェクトをコピーする関数
d[list] = list.copy
d[dict] = dict.copy
d[set] = set.copy
d[bytearray] = bytearray.copy

if PyStringMap is not None:
    d[PyStringMap] = PyStringMap.copy

del d, t

◯ types.FunctionType, 関数型は mutable

types.FunctionType は、ちゃんと確認してみると mutable でした。PEP 232 で Function Attributes として認められ Python 2.1 から types.FunctionType は mutable になったそうです。

import types
def f ():
    pass


# 1) types.FunctionType ... mutable
assert type(f) is types.FunctionType
f.a = 100  # 代入してもエラーにならない。

# 2) types.BuiltinFunctionType ... immutable
assert type(max) is types.BuiltinFunctionType
max.a = 100  # AttributeError
def f():
    return f.a

f.a = 10
f()  # 10
f.a = 20
f()  # 20



クラスでラップしてしまえばよかったんじゃないんやろか...

class Wrapper(object):
    a = 10
    def f():
        return Wrapper.a

Wrapper.f()  # 10



と思ったら PEP の中にあったメールへのリンクで、ちゃんと説明されている様子。詳細はちゃんとまだ読みきっていない。要約すると、いちいち関数をクラスで wrap するなんて、面倒くさいやろバーローってことらしい。

> クラスインスタンスと比べて何が利点ですか?
もし私が関数には属性を持たせないというあなたの考えに従うなら、関数と関連のあるオブジェクトを扱いたいときは、いつも関数とそのオブジェクトをクラスでラップしないといけなくなる。しかし、そのラップしたことによる結果は、すべての個々の関数がクラスとなるようなプログラムを生み出すことになる。そんなことは信じられないくらい面倒だ、特に Python のスコープのルールにおいては。一般に、おそらく可能でさえない。

> What are the benefits compared to class instances?
If I follow you, you are saying that whenever you need to associate information with a function, you should wrap up the function and object into a class. But the end result of this transformation could be a program in which every single function is a class. That would be incredibly annoying, especially with Python's scoping rules. In general, it may not even be possible.

[Python-Dev] Arbitrary attributes on funcs and methods



となると、copy モジュールで immutable として扱ってるのは、ええのやろかとも思ったけど。singleton として扱ってるなら問題ないんやろな。それなら _copy_immutable じゃなくて _copy_singleton の方が関数の表現としては適切なのではないやろか.. それはそれで、わかりづらいか。

◯ isimmutable
immutable であるかどうかを判定するコードを書く

かなり、難しい... 多分できない。

いちいち immutable なオブジェクトを覚えるなんて面倒ですよね。だから、isimmutable(obj) みたいな感じで、判定できたら便利そうですよね。

Python ではオブジェクトを immutable にする機能があるわけではありません。immutable とはオブジェクトの属性を変更できないという、オブジェクトの性質を表す言葉でしかありません。


f:id:domodomodomo:20180623235138j:plain



オブジェクトの性質を調べるために、単純に属性に力技で代入して例外が発生したら immutable であるかどうか判断するような関数であれば簡単に作れそうです。
isimmutable function in Python -

しかし Python には、属性参照をカスタマイズすることができるディスクリプタという機能があります。これを使われると、そのような実装では正確には判定できなくなります。ディスクリプタについては Effective Python の 4 章を読むとわかりやすいです(理解したとは言っていない)。

結局、mutable であるか immutable であるかは型、クラスごとに決まります。ソースコードを覗いて判断するほかなさそうです。

オブジェクトが mutable かどうかはその型によって決まります。例えば、数値型、文字列型とタプル型のインスタンスは immutable で、dict や list は mutable です。
An object’s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.

3. Data model — Python 3.5.5 documentation

immutable な dict

ちなみに immutable な dict として、frozendict というのものが PEP 416 で提案されたそうですが。reject されたようです。なんで tuple, frozenset は組込型で、namedtuple は組込モジュールで実装されているのに frozendict は完全に不採用なんだろう。

(原文)
Rejection Notice
According to Raymond Hettinger, use of frozendict is low. Those that do use it tend to use it as a hint only, such as declaring global or class-level "constants": they aren't really immutable, since anyone can still assign to the name.

(直訳)
却下通知
Raymond Hettinger によると、frozendict の使用は低い。forzendict を使うのは、ヒントのためだけに使われる傾向がある。例えば global もしくは class レベルの定数を宣言する。これらは実際には immutable ではない、誰でも名前に代入することができるからである。

(かなり意訳)
却下通知
Raymond Hettinger によると、frozendict を実装する必要性は低い。ここにいる人たちは、モジュールもしくはクラスの変数が定数であることを示唆するためだけに、frozendict を導入したいと考えているようだ。しかし、frozendict が代入された変数は、実際には定数ではない。Python では、変数に別のオブジェクトを代入することができるからである。

PEP 416 - 組込型に forzendict を追加する

immutable なオブジェクトを自作する。

1. collections.namedtuple 関数

namedtuple 関数を用いて immutable なオブジェクトを生成するクラスを作ることができます。

import collections
import inspect

def msg(err):
    return err.__class__.__name__ + ': ' + str(err)


#
# immutable なクラスの定義
#
Point = collections.namedtuple('Point', ['x', 'y'])

#
# インスタンスオブジェクトの動作確認
#
point = Point(11, y=22)
print(point)

# 1) 属性の参照ができる
assert point.x  == 11

# 2) 属性の変更はできない
try:
    point.x = 33
except AttributeError as err:
    print(msg(err))

# 3) 属性の追加はできない
try:
    point.z = 44
except AttributeError as err:
    print(msg(err))


#
# クラスオブジェクトの動作確認
#

# 4) Point はクラスオブジェクト
assert inspect.isclass(Point)

# 5) tuple を継承したクラスです。
assert Point.__bases__ == (tuple, )
assert point.x is point[0]
assert point.y is point[1] 

# 6) メソッドを追加する。
Point.add = lambda self: self.x + self.y
assert point.add() == 33



namedtuple は point.x が参照されると point[0] を返すように property 関数を使って実装されています。
collections.py - GitHub

話が少し戻りますが、このように Python では属性参照をカスタマイズする機能があるため immutable であるかどうかを判定する isimmutable のようなコードを書くことが困難です。

以下のメソッドを定義して、クラスインスタンスへの属性値アクセス ( 属性値の使用、属性値への代入、 x.name の削除) の意味をカスタマイズすることができます。

3.3.2. 属性値アクセスをカスタマイズする - Python 言語リファレンス

2. typing.NamedTuple クラス

文字列で属性を定義するなんて、面倒ですよね。

# 1
import collections
Employee = collections.namedtuple('Employee', ['name', 'id'])



typing.NamedTuple を使えば、こんな風に書けます。

# 2
from typing import NamedTuple

class Employee(NamedTuple):
    name: str
    id: int



どうやって実装しているかというとメタクラスを使っているようです。メタクラスを使うとクラスの定義をカスタマイズできます。メタクラスについても Effective Python の 4 章に説明が書かれています。
26.1. typing - 型ヒントのサポート
typing.py - GitHub


3. dataclasses

Python 3.7 から組込モジュールに追加されました。Python 3.6 以前でも pip install dataclasses をすれば使えます。immutable にする場合は frozen=True を指定してください。

import dataclasses

@dataclasses.dataclass(frozen=True)
class Employee(object):
    name: str
    id: int

Employee('domodomodomo', 4649)

Python3.7からは「Data Classes」がクラス定義のスタンダードになるかもしれない - Qiita


4. CPython 拡張

Cython を使って CPython(Python) そのものを拡張して immutable なオブジェクトのクラスを生成するクラスを作ることもできる様です。よくわからないけど、すごそう...。
How to make an immutable object in Python? - Stack Overflow


5. __slots__ を使う。

正確には immutable ではないですが __slots__ を使うと属性の追加ができないようになります。属性の変更はできます。使い方は簡単で __slots__ にオブジェクトが使用する変数名を list などのシーケンスで渡すだけです。

class Point(object):
    __slots__ = ['x', 'y']
    def __init__(self, x, y):
        self.x, self.y = x, y


p = Point(1, 2)

# これはできる
p.x = 2
p.y = 4

# これはできない
# AttributeError: 'Point' object has no attribute 'z'
p.z = 3



__slots__ は、クラス生成の時にメモリ消費を抑えたい時に使います。あと属性参照の速度が、20%くらい速くなります。インスタンス化の速度は変化はありませんでした。

デフォルトでは、クラスのインスタンスは属性を保存するための辞書を持っています。これは、ほとんどインスタンス変数を持たないオブジェクトでは領域の無駄です。大量のインスタンスを生成するとき、この記憶領域の消費量は深刻になり得ます。

3.3.2.3. __slots__

__new__ を使って値を初期化する。

immutable なオブジェクトは __init__ で初期化できません。それはオブジェクト self が immutable で属性に値を代入できないためです。これを回避するために __new__ 関数を使う方法があります。次のページでその解説をしています。