Python で mutable と immutable の違い



みなさまへの大切なメッセージ

簡単に言えば...

値を変更できるオブジェクトのことを mutable と呼びます。
値を変更できないオブジェクトのことを 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:20171027103757j: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)



その他にもこんなのが

  • complex
  • bytes
  • frozenset
  • type
  • slice
  • types.BuiltinFunctionType
  • type(Ellipsis)
  • type(NotImplemented)
  • types.FunctionType
  • weakref.ref

◯ isimmutable 関数(適当に読み飛ばしてください。)

存在しません。

覚えるなんて大げさな isimmutable 関数みたいなの使って、都度調べればいいんでしょどうせww と思われるかもしれません。しかし、動的に 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



このことは最初から付属で付いてくる copy モジュールでさえ immutable なクラスのインスタンスであるかどうかを、全て列挙して力技で判定しているところから推測しています。

以下は copy 関数のソースコードの抜粋です。
8.10. copy — 浅いコピーおよび深いコピー操作 — Python 3.6.3 ドキュメント
cpython/copy.py at 3.6 · python/cpython · GitHub

# オブジェクトをコピーする関数を返す辞書
# _copy_dispatch[クラス] = コピーする関数 
_copy_dispatch = d = {}

# immutable なクラスは、そのままインスタンスオブジェクトをそのまま返す
# immutable_obj is copy(immutable_obj)
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):
    # クラスオブジェクトが hashable だったとは....
    # _copy_dispatch[クラス] = オブジェクトをコピーする関数
    # _copy_dispatch[t     ] = _copy_immutable
    # d             [t     ] = _copy_immutable
    d[t] = _copy_immutable



力技で代入して例外が発生したら immutable であるかどうか判断するような関数を作ってはみたのですが... なんか違うような気がします。使いどこもありませんしね。
isimmutable function in Python -

力技で判定しているのを眺めると、よくわかってはいないのですが、なんとなく Python は型で苦しんでいるのかなという感じもします。
Revenge of the Types: 型の復讐 - Qiita


value って何?

答え:
1. 属性 attribute に代入されたインスタンスオブジェクト または
2. シーケンス sequence の要素 element
と思われます。

値って言葉を使ってるけど、まず、そもそも値ってなんやねんって話になります。この辺りの用語的な定義やら何やらを探してますが、それと思しきものを見つけられないでいます。ただ、色々読んでると Python の公式マニュアルが値 value と言った時は、上記の2つを指してるように思います。

オブジェクト、値、および型 の章では、オブジェクトは (型や id のに加えて) 値を持つことを述べています。 オブジェクトの値は Python ではやや抽象的な概念です: 例えば、オブジェクトの値にアクセスする正統な方法はありません。 また、その全てのデータ属性から構成されるなどの特定の方法で、オブジェクトの値を構築する必要性もありません。 比較演算子は、オブジェクトの値とは何かについての特定の概念を実装しています。 この比較の実装によって、間接的にオブジェクトの値を定義している考えることもできます。

6.10.1. 値の比較

1. 属性 attribute に代入されたインスタンスオブジェクト

>>> a = 1
>>>
>>> # 1 という数字もインスタンスオブジェクト
>>> # なので属性を持つ
>>>
>>> # a の属性を表示
>>> for e in dir(a):e
... 
'__abs__'
'... 略 ...'
'__xor__'
'bit_length'
'conjugate'
'denominator'
'from_bytes'
'imag'
'numerator'
'real'
'to_bytes'
>>> # 1 オブジェクト
>>> a = 1
>>>
>>> # 1 オブジェクトの値 
>>> a.real
1
>>>

2. シーケンス sequence の要素 element

例えばリストのように s[0] という記述でインスタンスオブジェクトを参照できるオブジェクトをシーケンスと読んでいる "ようです"。 

s[0] は s の i 番目の要素です。s の属性とは違いそうですが、なんとなくこれが、値と絡んでいそうな気がします。

s[i] s の 0 から数えて i 番目の要素

4.6.1. 共通のシーケンス演算

>>> # Hello, world! オブジェクト
>>> s = 'Hello, world! '
>>> 
>>> # Hello, world! オブジェクトの値
>>> s[0]
'W'
>>>


要素は属性とは違うやろ。って声が聞こえてきそうですが。str には callable でない属性、メソッドでない属性が __doc__ しかないのです。だから、シーケンスの要素も値の一種と考えてもいいのかな、と。

>>> for e in dir(s):
...   if not callable(getattr(s, e)):
...     e
... 
'__doc__'

◯ 属性と値の関係

でも、そう考えると、属性と値の関係も、再帰的に定義されてそうな気配がありますね。

object には複数の attribute がついていて
attribute には 1 つの value(object) が結びついてる的な...
object -*-> attribute -1-> value(object)

リテラルはどこにあるんだろう?

答え: immutable なオブジェクトがリテラルだと思われます

再帰的に定義されてるなら、 1 とか 'a' のような実際のデータ、リテラルは、どこに保存されてるのかな?と思いました。

何故なら、属性と値の関係は再帰的に定義されてるなら、どんなに属性を参照し続けても実際のデータ, リテラルが保存されているところに行き着くことができないからです。自己参照してるところを終端として捉えて、リテラルを保存している場所として取り扱っているのかなと思ったりもします。

例えば int の 1 は自分自身 1 が属性に代入されています。

>>> a = 1
>>> 
>>> a.real
1
>>> a.real.real
1
>>>
>>> id(a.real)
4300950496
>>> id(a.real.real)
4300950496
>>> 




str の 'W' には自分自身 'W' が 0 番目の要素に代入されています。

>>> s = 'Welcome to ようこそジャパリパーク! 今日もドッタンバッタン大騒ぎ うー!がぉー! 高らかに笑い笑えば フレンズ喧嘩して すっちゃかめっちゃかしても仲良し けものは居ても のけものは居ない本当の愛はここにあるほら 君も手をつないで大冒険 '
>>> 
>>> s[0]
'W'
>>> s[0][0]
'W'
>>> 
>>> id(s[0])
4473881520
>>> id(s[0][0])
4473881520
>>>




そして、なぜ immutable なオブジェクトが実際のデータを表現していると思ったのかと言うと、immutable なオブジェクトには __dict__ 属性がないからです。通常 Python は x.a と参照された時、内部で x.__dict__['a'] に変換して値を参照しています。

>>> # 同じことをしている。
>>> x.a
1
>>>x.__dict__['a']
1

Python のクラスオブジェクトとインスタンスオブジェクトって何? -


しかし、immutable なオブジェクトには __dict__ 属性が存在しません。おそらく __dict__ を使わずに属性を固定して(immutable にして)、リテラルを表現しているものと思われます。

>>> a = 100
>>> a.__dict__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'int' object has no attribute '__dict__'
>>>
>>> b = range(10)
>>> b.__dict__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'range' object has no attribute '__dict__'
>>>

◯ str クラスのオブジェクトの構造

Python のテキストデータは str オブジェクト、
すなわち 文字列 として扱われます。
文字列は Unicode コードポイント
イミュータブルなシーケンスです。
4.7. テキストシーケンス型 — str

文字集合では、個々の文字に対して、
文字集合内での符号位置が決められている。
これをコードポイントという。
文字コード考え方から理解する Unicode と UTF-8 の違い

immutable なオブジェクトを生成するクラスを自作

1. __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__ は、クラス生成の時にメモリ消費を抑えたい時に使います。

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

3.3.2.3. __slots__

2. CPython 拡張

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

3. namedtuple 関数

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


インスタンスオブジェクトの動作確認

>>> import collections
>>>
>>> # immutable なクラスの定義
>>> Point = collections.namedtuple('Point', ['x', 'y'])
>>>
>>> p = Point(11, y=22)
>>> 
Point(x=11, y=22)
>>> 
>>> # 1) 属性の参照ができる
>>> p.x
11
>>> 
>>> # 2) 属性の変更はできない
>>> p.x = 33
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can t set attribute
>>>
>>>
>>> # 3) 属性の追加はできない
>>> p.z = 44
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Point' object has no attribute 'z'
>>>



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

>>> import inspect 
>>> 
>>> # 4) Point はクラスオブジェクト
>>> inspect.isclass(Point)
True
>>>
>>> # 5) tuple を継承したクラスで実現されている 
>>> Point.__bases__[0]
<class 'tuple'>
>>>
>>> 
>>> # 6) クラスオブジェクトへの関数の追加ができる
>>> #    区別 クラスオブジェクト Point <-> インスタンスオブジェクト p
>>>
>>> def add(self):
...   return self.x + self.y
... 
>>>
>>> Point.add = add
>>>
>>> # 7) メソッドとして利用できる。
>>> p.add()
33
>>>
__new__ を使って値を初期化する。

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