Last Updated: 2/6/2024, 5:44:57 AM

# immutable の一覧

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

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

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

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

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

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

# 1. よく知らないのも入ってない?

見慣れないものも入っていますが 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 (opens new window)
このモジュールは(types は)、 Python インタプリタを実装するために必要な多くの型に対して名前を提供します。

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

weakref.ref は、また別の機会にご紹介させてください。 これはガベレージコレクションとか説明しないといけないので、すこし重いので。

# 2. 根拠は、どこにあるの?

公式ドキュメント内では見つからず、 標準ライブラリ copy の中に immutable な型を列挙していると思われる箇所がありました。 そこから引張ています。

_copy_dispatch = d = {}

def _copy_immutable(x):
    return x
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):
    d[t] = _copy_immutable
t = getattr(types, "CodeType", None)
if t is not None:
    d[t] = _copy_immutable

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

本当にそうなの?って感じですが、 copy.py のコードの詳細については、この先の以下の記事でご紹介させていただきたいと考えております。

# 3. 2つの PEP

immutable なクラスに関連した2つの PEP をご紹介します。 ここは特に適当に流し読みしてください。

# 3.1. 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 (opens new window) の使いどころかな、と思ったのですが...

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!
The Zen of Python - PEP 20 (opens new window)

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

関数とメソッドへの任意属性 - Python Dev (opens new window)
Arbitrary attributes on funcs and methods - Python-Dev

クラスインスタンスと比べて何が利点ですか?
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.

しかし、そのラップしたことによる結果は、すべての個々の関数がクラスとなるようなプログラムを生み出すことになる。 そんなことは信じられないくらい面倒だ、特に Python のスコープのルールにおいては。一般に、おそらく可能でさえない。
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.

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

ルールを破ってまで作るべき特例なんてない
Special cases aren't special enough to break the rules.
The Zen of Python - PEP 20 (opens new window)

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

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

「暗黙」よりも「明示」
Explicit is better than implicit.
The Zen of Python - PEP 20 (opens new window)

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

# 3.2. frozendicit - immutable な dict は採用されなかった

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

PEP 416 - 組み込み型への forzendict の追加 (opens new window)

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

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

(原文) 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.

# 4. immutable を判定したい

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

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

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

オブジェクトの性質を調べるために、単純に属性に力技で代入して例外が発生したら immutable であるかどうか判断するような関数であれば それらしいものは作れそうな気もするのですが... ほとんど immutable で、どこか一箇所の属性だけ mutable な場合は力技では判定できません。

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

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

# 5. 定数

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

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

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

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

# 6. まとめ

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.1. Objects, values and types - The Python Language Reference (opens new window)