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.6 documentation

◯ mutable なオブジェクト

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

ユーザ定義クラス
class Person():
    def __init__(self, name):
        self.name = name

person = Person('yaruo')
person.name

# 変更できた -> mutable
person.name = 'yarumi' 
person.name  # 'yarumi'
>>> # 変更できた -> mutable
... person.name = 'yarumi' 
>>> person.name  # 'yarumi'
'yarumi'
>>> 
list 型
lst = [1, 2, 3]

# 変更できた -> mutable
lst[2] = 4
lst  # [1, 2, 4]
>>> # 変更できた -> mutable
... lst[2] = 4
>>> lst  # [1, 2, 4]
[1, 2, 4]
>>> 

◯ immutable なオブジェクト

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

int 型
a = 1

# 1 の実部
a.real  # 1

# 1 の虚部
a.imag  # 0

# 変更できない -> immutable
i.imag = 100  # AttributeError
>>> # 変更できない -> immutable
... i.imag = 100  # AttributeError
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
NameError: name 'i' is not defined
>>> 


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

str 型
s = 'ランボー/怒りの脱出'

s[0]  # 'ラ'

# 変更できない -> immutable
s[0] = 'チ'  # TypeError
>>> # 変更できない -> immutable
... s[0] = 'チ'
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
TypeError: 'str' object does not support item assignment
>>> 


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



こう言うコード見せられると「やめろっ!!」って思いますよね。まっ、エラーになるんですけど笑 あと正直、この時の AttributeErrot と TypeError の違いが、あまりよくわかっていません。

◯ immutable な型の一覧

以下に immutable な型を列挙します。

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

◯ よくある誤解

変数に代入できるから mutable だというのは誤りです。

a = 1
a = 2  # <- 変数 a に代入できたから int 型は mutable だよね!?


何故なら、変数に代入してもオブジェクトは変化しないからです。反対に属性に代入できた場合は、オブジェクトが変化します。変数への代入と属性への代入の違いについては、こちらで説明させていただきました。
Python の変数と属性、代入とコピー - いっきに Python に詳しくなるサイト





正確に言えば...



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





例えば tuple は、オブジェクトを変更することができますが immutable に分類されます。

# tuple は immutable だけど...
t = ([1,2],[3,4,5])

# オブジェクトを変更できる -> だけど immutable
t[0][0]=100
t  # ([100, 2], [3, 4, 5])
>>> # オブジェクトを変更できる -> だけど immutable
... t[0][0]=100
>>> t  # ([100, 2], [3, 4, 5])
([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.6 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.6 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.6 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 と名前までつけて、一体なんの意味があるのでしょうか?

Python では、そこまで重要ではないかなと思います。なぜなら immutable なクラスを自分で作っても Python では immutable の恩恵に預かることができないからです(後述します)

しかし我々 Pythonista は見落としがちですが、プログラミング言語という視点で考えた時 immutable は、とてもとても重要な概念です。それは、可読性と実装の2点からです。

1. 可読性においての重要性 - 副作用ってなに?

「副作用」という言葉を考えた時に、無いよりは、あった方がいいかなと思いました。「副作用」とは、関数またはメソッドを実行した時に、オブジェクトの属性が変化することを指しています。

副作用 - Wikipedia
プログラミングにおける副作用(ふくさよう)とは、ある機能がコンピュータの(論理的な)状態を変化させ、それ以降で得られる結果に影響を与えることをいう。代表的な例は変数への値の代入である。

堅牢なアプリケーションを実現する上で「副作用を最小限に抑える」という設計思想は、非常に重要な示唆を含んでいます。
副作用を最小限に抑えるために必要なこと



例えば list.sort は、副作用のあるメソッドです。そして sorted は、副作用のない関数です。

# 副作用あり
lst1 = [1, 0, 3, 2]
lst1.sort()
lst1
# [0, 1, 2, 3] -> オブジェクトが変化したので副作用がある
# 副作用なし
lst2 = [1, 0, 3, 2]
lst3 = sorted(lst2)

lst2
# [1, 0, 3, 2] -> オブジェクトは変化していないので副作用はない

lst3
# [0, 1, 2, 3]
1.1. 副作用が無ければ、影響範囲を絞ることができる。

正直自分は、ひとりで Python を書いていると、基本的に変数も属性も mutable なので、これのどこが嬉しいのか、全くわかりません。副作用を最小に抑えるのがなぜ重要なのか、理解していません。

副作用がある関数だと、システムに影響を及ぼす範囲が無限に広がります。反対に副作用がない関数だと、システムに影響を及ぼす範囲は関数の返り値の中にに限定されます。副作用がある関数は、どこまでその関数がシステムに影響するのか、把握するか、あるいは覚えておかなければなりません。副作用のあるコードは、影響範囲の広い、コードを書く人の脳への負担、ワーキングメモリ への負担が大きいコードだと言えます。

C# の記事で List (Python で言えば mutable な list) ではなく IReadOnlyList (Python で言えば immutable な tuple) を使えと怒っている記事を紹介します。僕も理解していないのですが、記事の文体から温度感だけ、怒ってる感じだけ伝わればと思います。ひとりの時は問題ないけど、やはりいろんな人と一緒にコードを組む環境では immutable であることはとても重要であるように感じます。
引数の型を何でも List にしちゃう奴にそろそろ一言いっておくか - Qiita

1.2. 副作用が有ったとしても、アクションとクエリをわける。

CQS: 「あらゆるメソッドは、アクションを実行するコマンドか、呼び出し元にデータを返すクエリかのいずれかであって、両方を行ってはならない。これは、質問をすることで回答を変化させてはならないということだ。」
副作用を最小限に抑えるために必要なこと



もう一度、ソートの処理を見ます。list.sort は、何も返しません。正確には None を返していますが。「あらゆるメソッドは、アクションを実行するコマンドか、呼び出し元にデータを返すクエリかのいずれかであって、両方を行ってはならない。

これは、質問をすることで回答を変化させてはならないということだ。」とは、もし乱暴に言うなら "副作用のある関数に返り値を持たせるな、副作用のない関数には返り値を持たせろ" ということです。

副作用のあるコマンドには返り値を持たせないことで、副作用の有無を明示できます。もし実行結果が知りたければアクションコマンドとは別にクエリコマンドを投げろと言うことです。あるいは返り値を返さず例外機構を使い失敗したことを通知するべきです。

# 副作用あり
lst1 = [1, 0, 3, 2]
lst1.sort()
lst1
# [0, 1, 2, 3] -> オブジェクトが変化したので副作用がある
# 副作用なし
lst2 = [1, 0, 3, 2]
lst3 = sorted(lst2)

lst2
# [1, 0, 3, 2] -> オブジェクトは変化していないので副作用はない

lst3
# [0, 1, 2, 3]



(体験談)
そして副作用のあるコードなのか、ないコードなのかを明確にすることは、とても重要だと思います。

これは私の実体験なのですが、ベンダーさんの作業を立ち会いしていた時に、大量の障害報がビービー鳴り響きました。最初はどこかで作業しているんだろうくらいに思っていたのですが...

どうやら大規模障害が発生したらしいことがわかりました。そんな影響の大きい範囲の作業は、うちくらいしかしていなかったので、ついにこの時が来てしまったか、という気持ちでいっぱいになりました。

でも、どうやら、ある別のベンダーさんの装置に限定された障害であることがわかり、自分たちの作業ではないことがわかりました。後日わかった原因は、メモリのある特定の箇所を確認するクエリコマンドを打つと、設定が変わってしまうアクションコマンドも混じっていたという仕様が原因らしく。

書き込まれたデータを読み出す時に、内容を破壊してしまうものが破壊読み出しである。
破壊読み出しと非破壊読み出し - Wikipedia



その作業手順書には、該当箇所のメモリは参照するなと書かれていたけど、作業をしていた方が親切心で事前確認のために、当初指定されていた範囲よりも広い範囲の、クエリコマンドをかけてはいけないと手順書に記載されていた箇所も含めて、全国にある数千の装置に同時にクエリコマンドを打ち込んで大規模障害が発生したようでした。


f:id:domodomodomo:20181208193407j:plain

僕、個人は、その人が悪いとは一切思いません。むしろ、日々限られた時間の中で作業手順を組み、作業に望むことは何があるかわからない恐怖心との戦いで、エクストリームスポーツそのものでした。本当に毎日お先真っ暗感いっぱいの中で仕事をしていました。

大量の機器、あるいは設定を取り扱わないといけない中で、作業前にクエリコマンドを打ちまくって機器の状態を確認することは、機器を自分の体の一部のようにすることは、褒められることではあっても貶されることではないと思います。これは私のポジショントークかもしれませんが。

実際に設定が正しいことを確認するには、手順書を作成した時とは別の視点で確認することが大切です。同じ視点で見ても誤りに気づくことができないからです。例えば、エクセルの管理表で手順書を作り、その手順書が正しいかどうかの確認は、現地の示名条片で確認します。

また確認するときも、全てを確認することはできないので、絶対に間違えてはいけないところだけを、優先的に確認します。間違えてはいけないところとは、事前に申請していたサービス断以上の箇所に影響が及ぼす過ちです。事前に申請した範囲内のサービス断の中での誤りであれば、初めてみるエラーを、黙ってその場で修正してました。

本当に毎日泣きそうでした。と言うか、トイレに駆け込んで、あまりに惨めすぎてよく泣いてました。いま考えると大げさですが、精神的に追い詰められて、睡眠時間が不足すると、涙もろくなります。あとで本を読んで知りました。

もちろん手順書を熟読しなかったことは悪いことですが、絶対に起こり得るヒューマンエラーだと思います。その人がやらなかったとしても、別の人がやることを私は確信しています。もし手順書を読まなかったことが悪いというなら、その人の勤務体系がどのようなものであったかを明らかにするべきです。

ちゃんと読まなかったことが悪いなんていうことにはならないはずです。これはマネジメントの問題です。このような態度、指針には私も本当に苦しめられました。ちなみにこの時の改善策は、検証していないコマンド、手順書に書かれていないコマンドは打たない。が改善策になり、監督機関に報告していました。何も対処できていません。負担だけが増えました。数年に経って垂れてきた時に、ワイルドカードを記載した手順書が仕上がってくるのが目に見えてます。

# 必要に応じ任意のクエリコマンドを打ち込む
クエリコマンド ワイルドカード



ここでの正しい改善策は、クエリコマンドとアクションコマンドを明確に分けることです。以下のコメントは動的型付けの言語に対するものですが、とても示唆的です。完全に同意できるわけではないのですが、ブラック企業という表現は、決して誇張ではなく的確な表現だと感じます。

自分の未熟さをカバーするために、型システムがあるんですよ > id: yug1224 。未熟だからミスが起きた、根性でテスト書けばなんとかなる、という一部の動的型付け言語ユーザーの精神論はまさにブラック企業の体質と同じ。
ttp://b.hatena.ne.jp/entry/365243110/comment/megumin1



大抵、どんな改善策ももっと頑張りますが、改善策なのです。もう十分に頑張っているのに。それは残業時間から端的に現れているにも関わらず。もし、その改善策がもっと頑張るなら、他のどこを頑張らないのかを明確にしないといけません。どこのリソースを割き、問題の箇所に割り当てるのか。絶対にそういう話はしませんし、意識もしません。

私たち日本人は根本的に、人員や補給にかかる足し算、引き算ができない民族なのです。この画像はネタ画像のようで決してネタ画像ではなく、私たちは日々全く同じ過ちを犯しています。


f:id:domodomodomo:20181211095439j:plain



1.3. 他言語での副作用への対応, Rust, Haskell, JavaScript

1.3.1. Rust での対応
Rust は、とても厳密に mutable を管理します。この記事は、後半は厳しいですが、前半は、わかりやすいです。
Rustの所有権に親しむ - Qiita

1.3.2. Haskell での対応
関数型言語Haskell では、副作用がありません。平たく言えば、変数や属性への再代入を許しません。一切、再代入を許さないと、都度オブジェクトを生成しないといけならず、特にメモリの消費量の面で苦しくなりやすいのですが、Haskell 自体は色々なところでプログラミング言語として、好意的に評価されているのを見かけます。

1.3.3. JavaScript での対応
ES6 で const が導入されいました。


2. 実装面での重要性

2点目は、実装面です。immutable であると実装が容易になりますし、実行速度も速くなります。Python は、オブジェクトの属性を自由に付け足しできたりするのですが、そういった柔軟さのために属性参照が鬼のように重くなっています。

2.1. str 型の処理

基本的に型が immutable であれば実装も簡単になり、処理性能も上がります。なぜ Python の str が immutable なのかという記事を見かけたので紹介します。
Why are Python strings immutable?


2.2. 並列処理

イミュータブルの利点は、挙動が予測可能なところです。物事が単純になり、見通しが良くなります。また、JavaScriptではあまり恩恵を受けられませんが、イミュータブルにすると自動的にスレッドセーフが実現できます。
JavaScriptでイミュータブルなプログラミングをする

2.3. ガベレージコレクション

Rust は本来、ガベレージコレクションでのゼロコストを狙って、mutable をしている気配があります。あまり詳しいことは知りませんが。副次的ば効果として副作用の範囲も限定的にしているという、なかなか素敵な実装になっているわけです。






Python も immutable な機能を実装するべきなのか?

わからない。

近年生まれた Rust は mutable を厳密に管理し、JavaScript は ES6 で const を導入しました。Python も、このビックウェーブに乗るべきなのでしょうか?


f:id:domodomodomo:20181207130022j:plain



規模の大きなもの、多人数で組む時は immutability はとても重要だと感じます。ワイみたいなのが10人寄せ集まって何か組まないといけないとなった時には、immutability のある言語を選びたいなと思ったりもします。

もちろん Python でも Guido のいる DropBox など大規模なものが組まれたりしていますが。そういうことが許されるのは、優秀な人に限定されてしまうのではないかなと思ったり、思わなかったり。

ただ、必ず immutable にする言語を選ぶ必要があるのかというと、書く時の面倒くささとかもあるかなと思ったりもします。なので、仲の良い少人数でガリガリ書いたりする時は Python のような言語でサクッと組み上げる方が、きっと楽しいと思います。

もし導入するにしても末尾にアンダーバーをつけたら val_ mutable のような形で、命名規則で判別するくらいが Python らしくていいかなと感じます。




immutable なオブジェクトを自作したい。

◯ immutable なオブジェクトを自作する意味はあるの?

答え: Python では、そもそも immutable にすること自体、あまり意味がありません。なぜなら Python は、可読性においても、実装面においても、あまり immutable の恩恵に預かれないからです。

可読性

まず、可読性、副作用の面で言えば、例えば Rust のようにほとんど immutable で部分的に mutable ですよ、とするなら、コードを書く人の脳への負担が少ないコードがかけると思います。mutable な箇所は限定されているので、そこにだけ注意してコーディングすればいい訳です。

反対に Python では、ほとんどすべてのオブジェクトが mutable ですし、定数を宣言することさえできません。そのような中で mutable な箇所はどこかを意識するのは無理がありますし, 逆にこれが immutable だから安心というのも無きにしもあらずですが、効果は限定的な気がします。

mutable,immutableがいい加減なC#で、userlandで「Listだけ」この辺りいくら頑張ったことろで焼け石に水なんですよね。所詮局所解でありファイクなimmutableでいくらでも抜け道がありますね。この点、Rust等は圧倒的に優れている。
ttp://b.hatena.ne.jp/entry/374617525/comment/megumin1



例えば classmethod, staticmethod は、そういった限定的な効果を提供してくれます。(以下のリンク先の記事を読む必要は一切ありません)。
Python の classmethod と staticmethod ってなに? - いっきに Python に詳しくなるサイト

ただ、部分的にでも副作用を抑えることは、効果は制限されるにせよ、まったく意味が無いという訳ではないのかなと思ったりもします。まず第一に classmethod, staticmethod は、標準ライブラリではなく組み込み型として定義されて、標準ライブラリのコードの中で頻繁に見かけます。また第二に Guido は classmethod や staticmethod を比較的好印象に捉えています。

実装面

さらに悪いことに、実装面で言えば、ユーザ定義クラスを頑張って immutable にしても、属性参照はもれなく遅くなります。namedtuple, NamedTuple は tuple を使っているのでメモリの消費量は改善されるかと思います。__slots__ を使った時は速くなるのですが、__slots__ 正確には immutable なわけではありません。

__slots__ とメタクラスを組み合わせて、属性参照も改善できる immutable なクラスの定義の仕方を考えはしましたが、今度はインスタンス化にかかる処理が遅くなり過ぎて失敗しました(以下のリンク先の記事を読む必要は一切ありません)。
Python のメタクラスとクラスデコレータってなに?

結論

したがって、Python では、可読性の点でも、実装面でもあまり immutable の恩恵に預かれないのです。ただ、immutable にするやり方は、いくつかやり方はあるので、ご紹介させていただきます。こんなのもあるんだなーくらいに眺めていただければと存じます。


1. collections.namedtuple 関数

標準ライブラリ collections の中にある 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


2. typing.NamedTuple クラス

文字列で属性を定義するなんて、面倒ですよね。標準ライブラリ 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.dataclasses クラス

Python 3.7 から標準ライブラリに dataclasses が追加されました。typing.NamedTuple と同じような形で immutable なオブジェクトが作れます。immutable にする場合は frozen=True を指定してください。Python 3.6 以前でも pip install dataclasses をすれば使えます。

import dataclasses

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

Employee('domodomodomo', 4649)


dataclasses の本来の使い方、説明については、ここの説明が一番わかりやすいです。
ttp://cocodrips.hateblo.jp/entry/2018/08/26/172938
最近追加されたPythonの便利機能とこれからのPython in #ll2018jp

どうやって実装しているかも覗いてみたのですが、よくわかりませんでした。

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__

◯ namedtuple, NamedTuple, dataclasses の書き分け方は?

わからない...

namedtuple 関数は型アノテーションを使わないとき。NamedTuple は型アノテーションを使うとき。dataclasses は型アノテーションを使うときでかつメソッドを追加しないとき。かなと感じたりもするのですが、おそらく違います。




immutable な型の一覧は、公式ドキュメントのどこに書いてあるの?

公式ドキュメントで immutable なクラスの一覧というのを探したのですが、見当たりませんでした。

代わりにオブジェクトをコピーする機能を提供してくれる標準ライブラリの copy モジュールのソースコードの中にimmutable なクラスを列挙したと思われるものがありました。
8.10. copy 浅いコピーおよび深いコピー操作 - Python 標準ライブラリ

ただ、ここで列挙されたものが全て immutable である保証もなく、ほかにも組み込み型で immutable であるものがあるかもしれません。これも、こんなものがあるんだなーくらいに眺めておいていただければ幸いです。



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

  • int
  • float
  • str
  • tuple
  • bool
  • range
  • type(None)
  • type
  • types.BuiltinFunctionType
  • types.FunctionType *これは mutable, 後述します。



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

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

◯ なんか、あまりよく知らないのも入ってない?

見慣れないものも入っていますが type はユーザ定義クラスと組み込み型の型、types.BuiltinFunctionType はユーザ定義関数の型、types.FunctionType は組み込み関数の型になります。

import types

class Cls:
    pass

def f():
    pass


# 1. 組み込み型
isinstance(int, type)  # True

# 2. ユーザ定義クラス
isinstance(Cls, type)  # True

# 3. 組み込み関数
isinstance(max, types.BuiltinFunctionType)  # True

# 4. ユーザ定義関数
isinstance(f, types.FunctionType)  # True

types
このモジュールは(types は)Python インタプリタを実装するために必要な多くの型に対して名前を提供します。

isinstance
object 引数が classinfo 引数のインスタンスであるか、 (直接、間接、または 仮想) サブクラスのインスタンスの場合に真を返します。 object が与えられた型のオブジェクトでない場合、この関数は常に偽を返します。

◯ copy モジュールの中身

以下は copy 関数のコードの抜粋です。
cpython/copy.py at 3.6 · python/cpython · GitHub

# ここに 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

抜粋したコードが本当に 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 - def を使って定義した関数は mutable

types.FunctionType は def を使って定義した関数のクラスです。types.FunctionType は、ちゃんと確認してみると mutable でした。PEP 232 で Function Attributes として認められ Python 2.1 から types.FunctionType は mutable になったそうです。

def f():
    return f.a

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



クラスでラップしてしまえばよかったんじゃないんやろか。こんなときこそ classmethod の使いどころかな、と思ったのですが...

class C:
    @classmethod
    def f(cls):
        return cls.a
    
    def g():
        return C.a

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



classmethod を使って名前空間 cls を明示した方が Python らしい書き方かなと思ったりします。

名前空間ってのは、すんげーアイデアなんだなぁ。これ、もっと使っていこうよ!
Namespaces are one honking great idea -- let's do more of those!

Python にまつわるアイデア: PEP 20 - Life with Python



と思ったら 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



読めてもいないけど、個人的には新しい機能は導入して欲しくなかったかな。そんな特別なルール、実装が必要だったのだろうかと疑問に感じたりもします。

ルールを破ってまで作るべき特例なんてない
Special cases aren't special enough to break the rules.

Python にまつわるアイデア: PEP 20 - Life with Python



クラスは、スコープを与えてくれます。彼らが欲しがっている機能はまさにこれです。この関数に属性を持たせると言う機能が善しとされるなら、関数がクロージャでもないのに状態、簡単に言えば属性を持ってしまうことになります。

属性を持ってしまうということは、すなわち副作用を持ってしまうということです。関数をクラスに属させれば、属性を持つ特別な関数であることを明示することができます。副作用を持つことを明示するために、この2行追加することは妥当じゃないかなと感じたりもします。

「暗黙」よりも「明示」
Explicit is better than implicit.

Python にまつわるアイデア: PEP 20 - Life with Python



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


◯ frozendicit - 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 であるかどうかを判定するコードを書きたい

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

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

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


f:id:domodomodomo:20180623235138j:plain



オブジェクトの性質を調べるために、単純に属性に力技で代入して例外が発生したら immutable であるかどうか判断するような関数であれば簡単に作れそうです(リンク先のコードを読む必要は一切ありません)。
isimmutable.py - GitHubGist

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

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

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

◯ 定数

None, Flase, True は Python は定数です。定数とは代入できない変数ということです。定数を作る機能があるならオブジェクトを immutable にしたり mutable に切り替えるのも簡単に実装できそう気がします。しかし、これをどうやってこれを実装しているのでしょうか。

None = 1
>>> None = 1
  File "<stdin>", line 1
SyntaxError: can't assign to keyword
>>> 



class, def, if, for などと同じ 予約語 として定義することによって定数を実装しています。ざっくり言えば、特例的に None, False, True だけ定数にしていて、簡単には他の変数には適用できないと言うことです。オブジェクトを immutable にしたり mutable に切り替える機能は、簡単に実装できそうにもないと言うわけです。

# 構文エラー SyntaxError なので関数も実行する前に
# 定義した段階でエラーで弾かれる
def f():
    None = 1
>>> def f():
...   None = 1
... 
  File "<stdin>", line 2
SyntaxError: can't assign to keyword
>>> 

◯ まとめ

mutable であるか immutable であるかは型、クラスごとに決まります。ソースコードを覗いて個別に判断するほかなさそうです。結局 Python は 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.6 documentation

おわりに

実は、いちばん最初に「値」という言葉を使っていました。この「値」という言葉は何者でしょうか?

を変更できるオブジェクトのことを 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.6 documentation