Pythonで不変オブジェクトを作成する方法は?


181

私はこれを必要としたことは一度もありませんが、Pythonで不変オブジェクトを作成するのは少し注意が必要かもしれないと思いました。をオーバーライドすることはできません。これ__setattr__は、で属性を設定することもできないためです__init__。タプルのサブクラス化はうまくいくトリックです:

class Immutable(tuple):

    def __new__(cls, a, b):
        return tuple.__new__(cls, (a, b))

    @property
    def a(self):
        return self[0]

    @property
    def b(self):
        return self[1]

    def __str__(self):
        return "<Immutable {0}, {1}>".format(self.a, self.b)

    def __setattr__(self, *ignored):
        raise NotImplementedError

    def __delattr__(self, *ignored):
        raise NotImplementedError

しかし、その後、あなたはへのアクセス持っているab変数スルーをself[0]してself[1]迷惑しています、。

これはPure Pythonで可能ですか?そうでない場合は、C拡張機能でどうすればよいですか?

(Python 3でのみ機能する回答は許容されます)。

更新:

したがって、タプルをサブクラス化することは、Pure Pythonでそれを行う方法であり[0][1]などによってデータにアクセスする追加の可能性を除いてうまく機能します。したがって、この質問を完了するには、Cで「適切に」行う方法だけです。geititemor setattributeなどを実装しないだけで、非常に単純になると思います。しかし、自分で行うのではなく、怠惰なので、そのための報奨金を提供します。:)


2
あなたのコードは、経由の属性へのアクセスを容易にしない.a.b?それが結局のところ、プロパティが存在するように見えるものです。
Sven Marnach、2011年

1
@Sven Marnach:はい。ただし、[0]と[1]はまだ機能しますが、なぜ機能するのでしょうか。欲しくない。:)たぶん、属性を持つ不変オブジェクトのアイデアはナンセンスですか?:-)
Lennart Regebro 2011年

2
もう1つの注意:NotImplementedリッチ比較の戻り値としてのみ意図されています。の戻り値__setatt__()は、通常はまったく表示されないため、とにかく意味がありません。のようなコードimmutable.x = 42は黙って何もしません。TypeError代わりにレイズするべきです。
Sven Marnach、2011年

1
@Sven Marnach:OK、驚いた。この状況ではNotImplementedを発生させることができると思っていたが、それは奇妙なエラーを引き起こす。代わりにそれを返しましたが、うまくいったようです。TypeErrorは、あなたがそれを使用しているのを見たときに明白に理解できました。
Lennart Regebro、2011年

1
@Lennart:あなたはレイズすることができますがNotImplementedError、それTypeErrorを変更しようとするとタプルがレイズするものです。
Sven Marnach、2011年

回答:


115

さらに考えた別の解決策:元のコードと同じ動作を実現する最も簡単な方法は

Immutable = collections.namedtuple("Immutable", ["a", "b"])

属性を介して属性にアクセスできる[0]などの問題は解決しませんが、少なくともかなり短く、およびと互換性があるという追加の利点がpickleありcopyます。

namedtupleこの回答で説明したものと同様のタイプ、つまりから派生しtupleて使用するタイプを作成し__slots__ます。Python 2.6以降で使用できます。


7
手書きのアナログと比較したこのバリアントの利点(Python 2.5(コードverbosenamedtupleのパラメーターの使用は簡単に生成されます)でも)は、単一のインターフェース/実装がnamedtuple数十もの非常にわずかに異なる手書きのインターフェース/実装よりも優れていることです。やるほとんど同じことを。
jfs

2
わかりました。これが最も簡単な方法なので、「ベストアンサー」が得られます。Sebastianは、Cythonの短い実装を提供することで賞金を獲得します。乾杯!
Lennart Regebro、2011

1
不変オブジェクトのもう1つの特徴は、関数を介してそれらをパラメーターとして渡すと、別の参照が行われるのではなく、値によってコピーされることです。うnamedtuple機能を通過する際にSが値によってコピーされますか?
hlin117 2015年

4
@ hlin117:すべてのパラメーターは、可変であるか不変であるかに関係なく、Pythonのオブジェクトへの参照として渡されます。不変オブジェクトの場合、コピーを作成しても特に意味がありません。オブジェクトを変更することはできないため、元のオブジェクトへの参照を渡すこともできます。
Sven Marnach、2015年

オブジェクトを外部でインスタンス化する代わりに、クラスの内部でnamedtupleを使用できますか?私はpythonは初めてですが、他の答えの利点は、クラスに詳細を非表示にして、オプションのパラメーターなどの機能を持たせることができることです。この回答だけを見ると、クラスを使用するすべてのものに名前付きタプルをインスタンス化する必要があるようです。両方の回答ありがとうございます。
Asaf 2017年

78

これを行う最も簡単な方法は、以下を使用することです__slots__

class A(object):
    __slots__ = []

のインスタンス A属性を設定できないため、現在は不変です。

クラスインスタンスにデータを含める場合は、これをからの派生と組み合わせることができますtuple

from operator import itemgetter
class Point(tuple):
    __slots__ = []
    def __new__(cls, x, y):
        return tuple.__new__(cls, (x, y))
    x = property(itemgetter(0))
    y = property(itemgetter(1))

p = Point(2, 3)
p.x
# 2
p.y
# 3

編集:インデックスを削除する場合は、次のようにオーバーライドできます__getitem__()

class Point(tuple):
    __slots__ = []
    def __new__(cls, x, y):
        return tuple.__new__(cls, (x, y))
    @property
    def x(self):
        return tuple.__getitem__(self, 0)
    @property
    def y(self):
        return tuple.__getitem__(self, 1)
    def __getitem__(self, item):
        raise TypeError

operator.itemgetterこの場合、のPoint.__getitem__()代わりに依存するため、プロパティに使用することはできませんtuple.__getitem__()。さらに、これによっての使用が妨げられることはありませんがtuple.__getitem__(p, 0)、これがどのように問題を構成するのかは想像できません。

不変オブジェクトを作成する「正しい」方法は、C拡張機能を作成することではないと思います。Pythonは通常、ライブラリの実装者とライブラリユーザーが大人同意していることに依存しています。実際にインターフェースを強制するのではなく、インターフェースをドキュメントに明確に記載する必要があります。これが、問題を__setattr__()呼び出すobject.__setattr__()ことによってオーバーライドされたものを回避する可能性を考慮しない理由です。誰かがこれをするなら、それは彼女自身の責任です。


1
tupleここで__slots__ = ()はなく、ここを使用する方が良い__slots__ = []でしょうか?(明確化)
user225312

1
@sukhbir:これはまったく問題ではないと思います。なぜタプルを好むのですか?
Sven Marnach、2011年

1
@Sven:私はそれが問題ではないことに同意します(ただし、無視できる速度の部分を除く)、私はこのように考えました:__slots__正しく変更されないのですか?これは、設定できる属性を一度に識別することを目的としています。では、そのような場合にtupleは、自然な選択ではないでしょうか。
user225312 2011年

5
しかし、空の場合は属性を__slots__設定できません。そして私が持っているなら、aとb属性はまだ変更可能です。__slots__ = ('a', 'b')
Lennart Regebro、2011年

しかし、あなたのソリューションはオーバーライドよりも優れている__setattr__ので、私のソリューションよりも優れています。+1 :)
Lennart Regebro 2011年

50

..Cで「適切に」行う方法

Cythonを使用し Pythonの拡張タイプを作成できます。

cdef class Immutable:
    cdef readonly object a, b
    cdef object __weakref__ # enable weak referencing support

    def __init__(self, a, b):
        self.a, self.b = a, b

Python 2.xと3の両方で動作します。

テスト

# compile on-the-fly
import pyximport; pyximport.install() # $ pip install cython
from immutable import Immutable

o = Immutable(1, 2)
assert o.a == 1, str(o.a)
assert o.b == 2

try: o.a = 3
except AttributeError:
    pass
else:
    assert 0, 'attribute must be readonly'

try: o[1]
except TypeError:
    pass
else:
    assert 0, 'indexing must not be supported'

try: o.c = 1
except AttributeError:
    pass
else:
    assert 0, 'no new attributes are allowed'

o = Immutable('a', [])
assert o.a == 'a'
assert o.b == []

o.b.append(3) # attribute may contain mutable object
assert o.b == [3]

try: o.c
except AttributeError:
    pass
else:
    assert 0, 'no c attribute'

o = Immutable(b=3,a=1)
assert o.a == 1 and o.b == 3

try: del o.b
except AttributeError:
    pass
else:
    assert 0, "can't delete attribute"

d = dict(b=3, a=1)
o = Immutable(**d)
assert o.a == d['a'] and o.b == d['b']

o = Immutable(1,b=3)
assert o.a == 1 and o.b == 3

try: object.__setattr__(o, 'a', 1)
except AttributeError:
    pass
else:
    assert 0, 'attributes are readonly'

try: object.__setattr__(o, 'c', 1)
except AttributeError:
    pass
else:
    assert 0, 'no new attributes'

try: Immutable(1,c=3)
except TypeError:
    pass
else:
    assert 0, 'accept only a,b keywords'

for kwd in [dict(a=1), dict(b=2)]:
    try: Immutable(**kwd)
    except TypeError:
        pass
    else:
        assert 0, 'Immutable requires exactly 2 arguments'

インデックス作成のサポートを気にしない場合collections.namedtupleは、@ Sven Marnachの提案が推奨されます

Immutable = collections.namedtuple("Immutable", "a b")

@Lennart:(namedtupleまたはより正確には、関数によって返されるタイプのnamedtuple())インスタンスは不変です。間違いなく。
Sven Marnach、2011年

@Lennart Regebro:namedtupleすべてのテストに合格します(インデックス作成のサポートを除く)。どのような要件を見逃しましたか?
jfs

はい、そうです。私は名前付きタプル型を作成し、インスタンス化してから、インスタンスではなくその型でテストを行いました。へえ。:-)
Lennart Regebro 2011年

なぜここで弱い参照が必要なのでしょうか?
McSinyx

1
@McSinyx:それ以外の場合、オブジェクトはweakrefのコレクションで使用できません。正確には__weakref__Python には何がありますか?
jfs

40

もう1つのアイデアは、完全に禁止してコンストラクタで__setattr__使用object.__setattr__することです。

class Point(object):
    def __init__(self, x, y):
        object.__setattr__(self, "x", x)
        object.__setattr__(self, "y", y)
    def __setattr__(self, *args):
        raise TypeError
    def __delattr__(self, *args):
        raise TypeError

もちろんobject.__setattr__(p, "x", 3)Pointインスタンスの変更に使用することもできますpが、元の実装では同じ問題が発生します(試してみてくださいtuple.__setattr__(i, "x", 42)Immutableインスタンス)。

元の実装で同じトリックを適用できます:を削除して、プロパティ関数で__getitem__()使用tuple.__getitem__()します。


11
誰かが意図的にsuperclassを使用してオブジェクトを変更することについては気にし__setattr__ません。重要なのは、変更してはならないことを明確にし、誤って変更しないようにすることです。
zvone

18

あなたは、作成することができます@immutableどちらかの上書きがあることデコレータ__setattr__ 変更__slots__飾った後、空のリストに__init__それをする方法を。

編集:OPが指摘したように、__slots__属性を変更しても、変更ではなく、新しい属性作成のみが防止されます

Edit2:ここに実装があります:

Edit3:__slots__ifはオブジェクトのの作成を停止するため、このコードは使用されません__dict__。別の方法を探しています。

Edit4:まあ、それだけです。これはハックですが、演習として機能します:-)

class immutable(object):
    def __init__(self, immutable_params):
        self.immutable_params = immutable_params

    def __call__(self, new):
        params = self.immutable_params

        def __set_if_unset__(self, name, value):
            if name in self.__dict__:
                raise Exception("Attribute %s has already been set" % name)

            if not name in params:
                raise Exception("Cannot create atribute %s" % name)

            self.__dict__[name] = value;

        def __new__(cls, *args, **kws):
            cls.__setattr__ = __set_if_unset__

            return super(cls.__class__, cls).__new__(cls, *args, **kws)

        return __new__

class Point(object):
    @immutable(['x', 'y'])
    def __new__(): pass

    def __init__(self, x, y):
        self.x = x
        self.y = y

p = Point(1, 2) 
p.x = 3 # Exception: Attribute x has already been set
p.z = 4 # Exception: Cannot create atribute z

1
(クラス?)デコレータまたはメタクラスをソリューションから作成することは確かに良いアイデアですが、問題はソリューションが何であるかです。:)
Lennart Regebro 2011年

3
object.__setattr__()それは休憩stackoverflow.com/questions/4828080/...
JFS

確かに。私はデコレータの練習として続けました。
PaoloVictor 2011年

13

凍結されたデータクラスの使用

Python 3.7以降の場合、データクラスを使用できますfrozen=Trueオプションを指定しをます。これは、た非常にメンテナンスしやすい方法です。

それはそのようなものになるでしょう:

from dataclasses import dataclass

@dataclass(frozen=True)
class Immutable:
    a: Any
    b: Any

データクラスのフィールドには型ヒントが必要なので、私はtypingモジュールのAnyしました

名前付きタプルを使用しない理由

Python 3.7より前のバージョンでは、名前付きタプルが不変オブジェクトとして使用されていることがよくありました。多くの点で注意が必要ですが、その1つは__eq__、名前付きタプル間のメソッドがオブジェクトのクラスを考慮しないことです。例えば:

from collections import namedtuple

ImmutableTuple = namedtuple("ImmutableTuple", ["a", "b"])
ImmutableTuple2 = namedtuple("ImmutableTuple2", ["a", "c"])

obj1 = ImmutableTuple(a=1, b=2)
obj2 = ImmutableTuple2(a=1, c=2)

obj1 == obj2  # will be True

ご覧のとおり、タイプの場合でもobj1とはobj2異なって、それぞれの分野の名前が異なっていても、obj1 == obj2まだ提供しますTrue。これ__eq__は、使用されるメソッドがタプルのメソッドであり、指定された位置にあるフィールドの値のみを比較するためです。特にこれらのクラスをサブクラス化している場合は、エラーの大きな原因になる可能性があります。


10

タプルまたは名前付きタプルのいずれかを使用することを除いて、それが完全に可能であるとは思いません。何があっても、オーバーライド__setattr__()すると、ユーザーはobject.__setattr__()直接呼び出すことで常にバイパスできます。依存するすべてのソリューションが機能し__setattr__ないことが保証されています。

以下は、なんらかのタプルを使用せずに取得できる最も近いものです。

class Immutable:
    __slots__ = ['a', 'b']
    def __init__(self, a, b):
        object.__setattr__(self, 'a', a)
        object.__setattr__(self, 'b', b)
    def __setattr__(self, *ignored):
        raise NotImplementedError
    __delattr__ = __setattr__

しかし、十分に頑張ると壊れます。

>>> t = Immutable(1, 2)
>>> t.a
1
>>> object.__setattr__(t, 'a', 2)
>>> t.a
2

しかし、スヴェンの使用 namedtupleは完全に不変です。

更新

質問がCで適切に実行する方法を尋ねるように更新されたので、Cythonで適切に実行する方法に関する私の答えは次のとおりです。

まずimmutable.pyx

cdef class Immutable:
    cdef object _a, _b

    def __init__(self, a, b):
        self._a = a
        self._b = b

    property a:
        def __get__(self):
            return self._a

    property b:
        def __get__(self):
            return self._b

    def __repr__(self):
        return "<Immutable {0}, {1}>".format(self.a, self.b)

そして、それsetup.pyをコンパイルするには(コマンドを使用してsetup.py build_ext --inplace

from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext

ext_modules = [Extension("immutable", ["immutable.pyx"])]

setup(
  name = 'Immutable object',
  cmdclass = {'build_ext': build_ext},
  ext_modules = ext_modules
)

次に、それを試してみる:

>>> from immutable import Immutable
>>> p = Immutable(2, 3)
>>> p
<Immutable 2, 3>
>>> p.a = 1
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: attribute 'a' of 'immutable.Immutable' objects is not writable
>>> object.__setattr__(p, 'a', 1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: attribute 'a' of 'immutable.Immutable' objects is not writable
>>> p.a, p.b
(2, 3)
>>>      

Cythonコードをありがとう、Cythonは素晴らしいです。JF Sebastiansのreadonlyの実装はすっきりしていて最初に到着したので、彼は賞金を獲得しました。
Lennart Regebro

5

をオーバーライドして不変クラスを作成し__setattr__、呼び出し元が__init__次の場合にセットを許可します。

import inspect
class Immutable(object):
    def __setattr__(self, name, value):
        if inspect.stack()[2][3] != "__init__":
            raise Exception("Can't mutate an Immutable: self.%s = %r" % (name, value))
        object.__setattr__(self, name, value)

だれでも___init__オブジェクトを変更できるため、これではまだ十分ではありませんが、アイデアはわかります。


object.__setattr__()それは休憩stackoverflow.com/questions/4828080/...
JFS

3
スタックインスペクションを使用して呼び出し元__init__が十分であることを確認することは、あまり満足のいくものではありません。
gb。

5

他の優れた答えに加えて、Python 3.4(またはおそらく3.3)のメソッドを追加したいと思います。この回答は、この質問に対するいくつかの以前の回答に基づいています。

Python 3.4では、セッターなしのプロパティ使用して、変更できないクラスメンバーを作成できます。(以前のバージョンでは、セッターなしでプロパティに割り当てることが可能でした。)

class A:
    __slots__=['_A__a']
    def __init__(self, aValue):
      self.__a=aValue
    @property
    def a(self):
        return self.__a

次のように使用できます。

instance=A("constant")
print (instance.a)

印刷されます "constant"

しかし、呼び出しinstance.a=10は以下を引き起こします:

AttributeError: can't set attribute

説明:セッターなしのプロパティは、Python 3.4のごく最近の機能です(そして3.3と思います)。このようなプロパティに割り当てようとすると、エラーが発生します。スロットを使用して、メンバー変数を__A_a(つまり__a)に制限しています。

問題:への割り当て_A__aはまだ可能です(instance._A__a=2)。しかし、プライベート変数に割り当てる場合、それはあなた自身の責任です...

ただし、特にこの回答は、の使用を推奨しません__slots__。属性の作成を防ぐために他の方法を使用することをお勧めします。


propertyPython 2でも利用できます(質問自体のコードを見てください)。それは不変オブジェクトを作成しません、私の答えからテストを試してください例えば、instance.b = 1新しいb属性を作成します。
jfs 2015

そうです、問題は実際にどのようにしてそれを防ぐか、A().b = "foo"つまり新しい属性の設定を許可しないことです。
Lennart Regebro、2015

プロパティに割り当てようとすると、セッターのないプロパティはpython 3.4でエラーを発生させます。以前のバージョンでは、セッターは暗黙的に生成されていました。
ベルンハルト

@Lennart:私のソリューションは、不変オブジェクトのユースケースのサブセットへの回答であり、以前の回答への追加です。不変オブジェクトが必要になる理由の1つは、ハッシュ可能にすることができるためです。この場合、私のソリューションが機能する可能性があります。しかし、あなたは正しい、これは不変のオブジェクトではありません。
Bernhard

@ jf-sebastian:属性の作成を防ぐためにスロットを使用するように私の回答を変更しました。他の回答と比較した私の回答の新しい点は、存在する属性の変更を回避するためにpython3.4のプロパティを使用することです。以前の回答でも同じことが達成されましたが、プロパティの動作が変更されたため、私のコードはより短くなっています。
Bernhard

5

ここにエレガントなソリューションがあります:

class Immutable(object):
    def __setattr__(self, key, value):
        if not hasattr(self, key):
            super().__setattr__(key, value)
        else:
            raise RuntimeError("Can't modify immutable object's attribute: {}".format(key))

このクラスから継承し、コンストラクターでフィールドを初期化すれば、準備は完了です。


1
しかし、このロジックを使用すると、オブジェクトに新しい属性を割り当てることができます
javed

3

動作のあるオブジェクトに興味がある場合は、namedtupleがほとんどの解決策です。

namedtuple ドキュメントの下部に記載されているように、namedtuple から独自のクラスを派生させることができます。次に、必要な動作を追加できます。

たとえば(ドキュメントから直接取得したコード):

class Point(namedtuple('Point', 'x y')):
    __slots__ = ()
    @property
    def hypot(self):
        return (self.x ** 2 + self.y ** 2) ** 0.5
    def __str__(self):
        return 'Point: x=%6.3f  y=%6.3f  hypot=%6.3f' % (self.x, self.y, self.hypot)

for p in Point(3, 4), Point(14, 5/7):
    print(p)

これは次の結果になります:

Point: x= 3.000  y= 4.000  hypot= 5.000
Point: x=14.000  y= 0.714  hypot=14.018

このアプローチは、Python 3とPython 2.7の両方で機能します(IronPythonでもテスト済み)。
唯一の欠点は、継承ツリーが少し変わっていることです。しかし、これはあなたが普段遊んでいるものではありません。


1
Python 3.6+はこれを直接サポートしています。使用するのはclass Point(typing.NamedTuple):
Elazar

3

次のImmutableクラスから継承するクラスは、__init__メソッドの実行が終了した後、インスタンスと同様に不変です。他の人が指摘したように、それは、純粋なPythonのので、そこの何もベースから変異特別なメソッドを使用してから誰かを停止しないobjecttypeが、これは誰かがクラス/インスタンスを誤って変更するのを防ぐのに十分です。

メタクラスを使用してクラス作成プロセスをハイジャックすることで機能します。

"""Subclasses of class Immutable are immutable after their __init__ has run, in
the sense that all special methods with mutation semantics (in-place operators,
setattr, etc.) are forbidden.

"""  

# Enumerate the mutating special methods
mutation_methods = set()
# Arithmetic methods with in-place operations
iarithmetic = '''add sub mul div mod divmod pow neg pos abs bool invert lshift
                 rshift and xor or floordiv truediv matmul'''.split()
for op in iarithmetic:
    mutation_methods.add('__i%s__' % op)
# Operations on instance components (attributes, items, slices)
for verb in ['set', 'del']:
    for component in '''attr item slice'''.split():
        mutation_methods.add('__%s%s__' % (verb, component))
# Operations on properties
mutation_methods.update(['__set__', '__delete__'])


def checked_call(_self, name, method, *args, **kwargs):
    """Calls special method method(*args, **kw) on self if mutable."""
    self = args[0] if isinstance(_self, object) else _self
    if not getattr(self, '__mutable__', True):
        # self told us it's immutable, so raise an error
        cname= (self if isinstance(self, type) else self.__class__).__name__
        raise TypeError('%s is immutable, %s disallowed' % (cname, name))
    return method(*args, **kwargs)


def method_wrapper(_self, name):
    "Wrap a special method to check for mutability."
    method = getattr(_self, name)
    def wrapper(*args, **kwargs):
        return checked_call(_self, name, method, *args, **kwargs)
    wrapper.__name__ = name
    wrapper.__doc__ = method.__doc__
    return wrapper


def wrap_mutating_methods(_self):
    "Place the wrapper methods on mutative special methods of _self"
    for name in mutation_methods:
        if hasattr(_self, name):
            method = method_wrapper(_self, name)
            type.__setattr__(_self, name, method)


def set_mutability(self, ismutable):
    "Set __mutable__ by using the unprotected __setattr__"
    b = _MetaImmutable if isinstance(self, type) else Immutable
    super(b, self).__setattr__('__mutable__', ismutable)


class _MetaImmutable(type):

    '''The metaclass of Immutable. Wraps __init__ methods via __call__.'''

    def __init__(cls, *args, **kwargs):
        # Make class mutable for wrapping special methods
        set_mutability(cls, True)
        wrap_mutating_methods(cls)
        # Disable mutability
        set_mutability(cls, False)

    def __call__(cls, *args, **kwargs):
        '''Make an immutable instance of cls'''
        self = cls.__new__(cls)
        # Make the instance mutable for initialization
        set_mutability(self, True)
        # Execute cls's custom initialization on this instance
        self.__init__(*args, **kwargs)
        # Disable mutability
        set_mutability(self, False)
        return self

    # Given a class T(metaclass=_MetaImmutable), mutative special methods which
    # already exist on _MetaImmutable (a basic type) cannot be over-ridden
    # programmatically during _MetaImmutable's instantiation of T, because the
    # first place python looks for a method on an object is on the object's
    # __class__, and T.__class__ is _MetaImmutable. The two extant special
    # methods on a basic type are __setattr__ and __delattr__, so those have to
    # be explicitly overridden here.

    def __setattr__(cls, name, value):
        checked_call(cls, '__setattr__', type.__setattr__, cls, name, value)

    def __delattr__(cls, name, value):
        checked_call(cls, '__delattr__', type.__delattr__, cls, name, value)


class Immutable(object):

    """Inherit from this class to make an immutable object.

    __init__ methods of subclasses are executed by _MetaImmutable.__call__,
    which enables mutability for the duration.

    """

    __metaclass__ = _MetaImmutable


class T(int, Immutable):  # Checks it works with multiple inheritance, too.

    "Class for testing immutability semantics"

    def __init__(self, b):
        self.b = b

    @classmethod
    def class_mutation(cls):
        cls.a = 5

    def instance_mutation(self):
        self.c = 1

    def __iadd__(self, o):
        pass

    def not_so_special_mutation(self):
        self +=1

def immutabilityTest(f, name):
    "Call f, which should try to mutate class T or T instance."
    try:
        f()
    except TypeError, e:
        assert 'T is immutable, %s disallowed' % name in e.args
    else:
        raise RuntimeError('Immutability failed!')

immutabilityTest(T.class_mutation, '__setattr__')
immutabilityTest(T(6).instance_mutation, '__setattr__')
immutabilityTest(T(6).not_so_special_mutation, '__iadd__')

2

私は少し前にこれを必要としていて、そのためのPythonパッケージを作成することにしました。初期バージョンは現在PyPIにあります:

$ pip install immutable

使用するには:

>>> from immutable import ImmutableFactory
>>> MyImmutable = ImmitableFactory.create(prop1=1, prop2=2, prop3=3)
>>> MyImmutable.prop1
1

ここに完全なドキュメント: https //github.com/theengineear/immutable

それが役に立てば幸い、説明したように名前付きタプルをラップしますが、インスタンス化をはるかに簡単にします。


2

この方法は機能を停止object.__setattr__しませんが、私はまだそれが便利であることに気づきました:

class A(object):

    def __new__(cls, children, *args, **kwargs):
        self = super(A, cls).__new__(cls)
        self._frozen = False  # allow mutation from here to end of  __init__
        # other stuff you need to do in __new__ goes here
        return self

    def __init__(self, *args, **kwargs):
        super(A, self).__init__()
        self._frozen = True  # prevent future mutation

    def __setattr__(self, name, value):
        # need to special case setting _frozen.
        if name != '_frozen' and self._frozen:
            raise TypeError('Instances are immutable.')
        else:
            super(A, self).__setattr__(name, value)

    def __delattr__(self, name):
        if self._frozen:
            raise TypeError('Instances are immutable.')
        else:
            super(A, self).__delattr__(name)

__setitem__ユースケースによっては、より多くのもの(など)をオーバーライドする必要がある場合があります。


これを見る前に似たようなものを思いつきましたが、これを使用しgetattrてのデフォルト値を提供できるようにしましたfrozen。それは少し物事を簡素化しました。stackoverflow.com/a/22545808/5987
Mark Ransom、

私はこのアプローチが一番好きですが、__new__オーバーライドは必要ありません。内部__setattr__では、条件付きのものを次のものに置き換えますif name != '_frozen' and getattr(self, "_frozen", False)
ピートカチョッピ

また、構築時にクラスを凍結する必要はありません。freeze()関数を提供すれば、いつでもフリーズできます。その後、オブジェクトは「1回フリーズ」されます。最後に、object.__setattr__「私たちはすべてここにいる」ので、心配するのはばかげています。
ピートカチョッピ2015年

2

Python 3.7以降では、クラスで@dataclassデコレータを使用でき、構造体のように不変になります。ただし、__hash__()クラスにメソッドを追加する場合と追加しない場合があります。見積もり:

hash()は組み込みのhash()によって使用され、オブジェクトが辞書やセットなどのハッシュ化されたコレクションに追加されます。持つハッシュは、()クラスのインスタンスは不変であることを意味します。可変性は、プログラマーの意図、eq()の存在と動作、およびdataclass()デコレーターのeqフラグと凍結フラグの値に依存する複雑なプロパティです。

デフォルトでは、安全である場合を除き、dataclass()は暗黙的にハッシュ()メソッドを追加しません。また、明示的に定義された既存のハッシュ()メソッドを追加または変更することもありません。hash()のドキュメントで説明されているように、クラス属性hash = Noneを設定すると、Pythonに特定の意味があります。

もしハッシュ定義され、明示的ではない、またはそれをNoneに設定されている場合、データクラス()の暗黙的な追加することができます)(ハッシュ()メソッドを。推奨されていませんが、dataclass()にunsafe_hash = Trueを指定してhash()メソッドを作成させることができます。これは、クラスが論理的に不変であるにもかかわらず、変更できる場合に当てはまります。これは特殊な使用例であり、慎重に検討する必要があります。

上記のリンクからのドキュメントの例:

@dataclass
class InventoryItem:
    '''Class for keeping track of an item in inventory.'''
    name: str
    unit_price: float
    quantity_on_hand: int = 0

    def total_cost(self) -> float:
        return self.unit_price * self.quantity_on_hand

1
あなたが使用する必要があるfrozen、すなわち、@dataclass(frozen=True)が、それは基本的にブロックはの使用__setattr____delattr__、ここで他の回答のほとんどのように。データクラスの他のオプションと互換性のある方法でそれを行います。
CS

2

setattrをオーバーライドし、それでもinitを使用して変数を設定できます。スーパークラスsetattrを使用します。これがコードです。

不変クラス:
    __slots__ =( 'a'、 'b')
    def __init __(self、a、b):
        super().__ setattr __( 'a'、a)
        super().__ setattr __( 'b'、b)

    def __str __(self):
        "" .format(self.a、self.b)を返す

    def __setattr __(self、* ignored):
        NotImplementedErrorを発生させる

    def __delattr __(self、* ignored):
        NotImplementedErrorを発生させる

またはpass代わりにraise NotImplementedError
jonathan.scholbach

この場合、__ setattr__と__delattr__で「渡す」ことはまったくお勧めできません。単純な理由は、誰かがフィールド/プロパティに値を割り当てた場合、当然、フィールドが変更されることを期待するからです。「最小の驚き」のパスを(必要に応じて)追跡したい場合は、エラーを発生させる必要があります。しかし、NotImplementedErrorが発生するのが適切かどうかはわかりません。「フィールド/プロパティは不変」のようなものを提起します。エラー...カスタム例外がスローされるべきだと思います。
darlove

1

サードパーティのattrモジュールがこの機能を提供します

編集:Python 3.7は、このアイデアをstdlibに採用しました@dataclass

$ pip install attrs
$ python
>>> @attr.s(frozen=True)
... class C(object):
...     x = attr.ib()
>>> i = C(1)
>>> i.x = 2
Traceback (most recent call last):
   ...
attr.exceptions.FrozenInstanceError: can't set attribute

attr__setattr__ドキュメントによると、凍結クラスをオーバーライドして実装し、インスタンス化のたびにパフォーマンスに若干の影響を与えます。

クラスをデータ型として使用する習慣があるattr場合は、ボイラープレートの面倒を見ることができるので特に便利です(ただし、魔法はありません)。特に、repr、init、hash、およびすべての比較関数を含む9つのdunder(__X__)メソッドを(それらをオフにしない限り)書き込みます。

attrヘルパー__slots__も提供します。


1

だから、私はそれぞれのpython 3を書いています:

I)データクラスデコレータを使用して、frozen = Trueを設定します。Pythonで不変オブジェクトを作成できます。

これには、データクラスlibからデータクラスをインポートする必要があり、frozen = Trueを設定する必要があります。

例。

データクラスからデータクラスをインポートする

@dataclass(frozen=True)
class Location:
    name: str
    longitude: float = 0.0
    latitude: float = 0.0

o / p:

l = Location( "Delhi"、112.345、234.788)l.name 'Delhi' l.longitude 112.345 l.latitude 234.788 l.name = "Kolkata" dataclasses.FrozenInstanceError:フィールド 'name'に割り当てられません

ソース:https : //realpython.com/python-data-classes/


0

別の方法は、インスタンスを不変にするラッパーを作成することです。

class Immutable(object):

    def __init__(self, wrapped):
        super(Immutable, self).__init__()
        object.__setattr__(self, '_wrapped', wrapped)

    def __getattribute__(self, item):
        return object.__getattribute__(self, '_wrapped').__getattribute__(item)

    def __setattr__(self, key, value):
        raise ImmutableError('Object {0} is immutable.'.format(self._wrapped))

    __delattr__ = __setattr__

    def __iter__(self):
        return object.__getattribute__(self, '_wrapped').__iter__()

    def next(self):
        return object.__getattribute__(self, '_wrapped').next()

    def __getitem__(self, item):
        return object.__getattribute__(self, '_wrapped').__getitem__(item)

immutable_instance = Immutable(my_instance)

これは、一部のインスタンスのみが不変である必要がある状況で役立ちます(関数呼び出しのデフォルト引数など)。

次のような不変のファクトリでも使用できます。

@classmethod
def immutable_factory(cls, *args, **kwargs):
    return Immutable(cls.__init__(*args, **kwargs))

からも保護しますがobject.__setattr__、Pythonの動的な性質により、他のトリックには当てはまりません。


0

私はAlexと同じアイデアを使用しました。メタクラスと「初期マーカー」ですが、上書き__setattr__と組み合わせて使用​​します。

>>> from abc import ABCMeta
>>> _INIT_MARKER = '_@_in_init_@_'
>>> class _ImmutableMeta(ABCMeta):
... 
...     """Meta class to construct Immutable."""
... 
...     def __call__(cls, *args, **kwds):
...         obj = cls.__new__(cls, *args, **kwds)
...         object.__setattr__(obj, _INIT_MARKER, True)
...         cls.__init__(obj, *args, **kwds)
...         object.__delattr__(obj, _INIT_MARKER)
...         return obj
...
>>> def _setattr(self, name, value):
...     if hasattr(self, _INIT_MARKER):
...         object.__setattr__(self, name, value)
...     else:
...         raise AttributeError("Instance of '%s' is immutable."
...                              % self.__class__.__name__)
...
>>> def _delattr(self, name):
...     raise AttributeError("Instance of '%s' is immutable."
...                          % self.__class__.__name__)
...
>>> _im_dict = {
...     '__doc__': "Mix-in class for immutable objects.",
...     '__copy__': lambda self: self,   # self is immutable, so just return it
...     '__setattr__': _setattr,
...     '__delattr__': _delattr}
...
>>> Immutable = _ImmutableMeta('Immutable', (), _im_dict)

注:Python 2.xと3.xの両方で動作するように、メタクラスを直接呼び出しています。

>>> class T1(Immutable):
... 
...     def __init__(self, x=1, y=2):
...         self.x = x
...         self.y = y
...
>>> t1 = T1(y=8)
>>> t1.x, t1.y
(1, 8)
>>> t1.x = 7
AttributeError: Instance of 'T1' is immutable.

スロットでも動作します...:

>>> class T2(Immutable):
... 
...     __slots__ = 's1', 's2'
... 
...     def __init__(self, s1, s2):
...         self.s1 = s1
...         self.s2 = s2
...
>>> t2 = T2('abc', 'xyz')
>>> t2.s1, t2.s2
('abc', 'xyz')
>>> t2.s1 += 'd'
AttributeError: Instance of 'T2' is immutable.

...そして多重継承:

>>> class T3(T1, T2):
... 
...     def __init__(self, x, y, s1, s2):
...         T1.__init__(self, x, y)
...         T2.__init__(self, s1, s2)
...
>>> t3 = T3(12, 4, 'a', 'b')
>>> t3.x, t3.y, t3.s1, t3.s2
(12, 4, 'a', 'b')
>>> t3.y -= 3
AttributeError: Instance of 'T3' is immutable.

ただし、変更可能な属性は変更可能であることに注意してください。

>>> t3 = T3(12, [4, 7], 'a', 'b')
>>> t3.y.append(5)
>>> t3.y
[4, 7, 5]

0

ここに実際に含まれていないことの1つは、完全な不変性です。親オブジェクトだけでなく、すべての子も同様です。例えば、タプル/フロゼンセットは不変かもしれませんが、それが含まれるオブジェクトはそうではないかもしれません。以下は、完全に不変性を強制する適切な作業を行う小さな(不完全な)バージョンです。

# Initialize lists
a = [1,2,3]
b = [4,5,6]
c = [7,8,9]

l = [a,b]

# We can reassign in a list 
l[0] = c

# But not a tuple
t = (a,b)
#t[0] = c -> Throws exception
# But elements can be modified
t[0][1] = 4
t
([1, 4, 3], [4, 5, 6])
# Fix it back
t[0][1] = 2

li = ImmutableObject(l)
li
[[1, 2, 3], [4, 5, 6]]
# Can't assign
#li[0] = c will fail
# Can reference
li[0]
[1, 2, 3]
# But immutability conferred on returned object too
#li[0][1] = 4 will throw an exception

# Full solution should wrap all the comparison e.g. decorators.
# Also, you'd usually want to add a hash function, i didn't put
# an interface for that.

class ImmutableObject(object):
    def __init__(self, inobj):
        self._inited = False
        self._inobj = inobj
        self._inited = True

    def __repr__(self):
        return self._inobj.__repr__()

    def __str__(self):
        return self._inobj.__str__()

    def __getitem__(self, key):
        return ImmutableObject(self._inobj.__getitem__(key))

    def __iter__(self):
        return self._inobj.__iter__()

    def __setitem__(self, key, value):
        raise AttributeError, 'Object is read-only'

    def __getattr__(self, key):
        x = getattr(self._inobj, key)
        if callable(x):
              return x
        else:
              return ImmutableObject(x)

    def __hash__(self):
        return self._inobj.__hash__()

    def __eq__(self, second):
        return self._inobj.__eq__(second)

    def __setattr__(self, attr, value):
        if attr not in  ['_inobj', '_inited'] and self._inited == True:
            raise AttributeError, 'Object is read-only'
        object.__setattr__(self, attr, value)

0

initの最後のステートメントでsetAttrをオーバーライドできます。構築できるが変更できない場合。もちろん、usintオブジェクトによってオーバーライドすることもできます。setAttrですが、実際にはほとんどの言語には何らかの形のリフレクションがあるため、不変性は常に漏れやすい抽象化です。不変性とは、クライアントが誤ってオブジェクトのコントラクトに違反しないようにすることです。私が使う:

=============================

提供された元のソリューションは正しくありませんでした。これは、ここからのソリューションを使用したコメントに基づいて更新されました

元の解決策は興味深い方法で間違っているため、下部に含まれています。

===============================

class ImmutablePair(object):

    __initialised = False # a class level variable that should always stay false.
    def __init__(self, a, b):
        try :
            self.a = a
            self.b = b
        finally:
            self.__initialised = True #an instance level variable

    def __setattr__(self, key, value):
        if self.__initialised:
            self._raise_error()
        else :
            super(ImmutablePair, self).__setattr__(key, value)

    def _raise_error(self, *args, **kw):
        raise NotImplementedError("Attempted To Modify Immutable Object")

if __name__ == "__main__":

    immutable_object = ImmutablePair(1,2)

    print immutable_object.a
    print immutable_object.b

    try :
        immutable_object.a = 3
    except Exception as e:
        print e

    print immutable_object.a
    print immutable_object.b

出力:

1
2
Attempted To Modify Immutable Object
1
2

======================================

元の実装:

コメントで正しく指摘されましたが、これは実際には機能しません。これは、クラスのsetattrメソッドをオーバーライドしているときに複数のオブジェクトの作成を防ぐためです。つまり、2番目のオブジェクトはself.a = willとして作成できません。 2番目の初期化に失敗します。

class ImmutablePair(object):

    def __init__(self, a, b):
        self.a = a
        self.b = b
        ImmutablePair.__setattr__ = self._raise_error

    def _raise_error(self, *args, **kw):
        raise NotImplementedError("Attempted To Modify Immutable Object")

1
これは機能しません。クラスのメソッドオーバーライドしているため、2番目のインスタンスを作成しようとするとすぐにNotImplementedErrorが返されます。
slinkp 2017年

1
このアプローチを追求したい場合は、実行時に特別なメソッドをオーバーライドすることが難しいことに注意してください。これに対するいくつかの回避策については、stackoverflow.com / a / 16426447/137635を参照してください。
slinkp 2017年

0

以下の基本的な解決策は、次のシナリオに対処します。

  • __init__() 通常どおり属性にアクセスして書き込むことができます。
  • OBJECTが属性の変更に対してのみ凍結された後:

アイデアは、__setattr__メソッドをオーバーライドし、オブジェクトの凍結ステータスが変更されるたびにその実装を置き換えることです。

したがって、_freezeこれら2つの実装を保存し、要求されたときにそれらを切り替えるメソッド()が必要です。

このメカニズムは、Freezer以下に示すように、ユーザークラス内に実装するか、特別なクラスから継承することができます。

class Freezer:
    def _freeze(self, do_freeze=True):
        def raise_sa(*args):            
            raise AttributeError("Attributes are frozen and can not be changed!")
        super().__setattr__('_active_setattr', (super().__setattr__, raise_sa)[do_freeze])

    def __setattr__(self, key, value):        
        return self._active_setattr(key, value)

class A(Freezer):    
    def __init__(self):
        self._freeze(False)
        self.x = 10
        self._freeze()

0

ちょうどのように dict

私は機能的な方法で物事を行っているオープンソースライブラリを持っているので、不変オブジェクト内でデータを移動することは役に立ちます。ただし、クライアントがデータオブジェクトと対話するためにデータオブジェクトを変換する必要はありません。だから、私はこれを思いつきました- それはあなたに不変なオブジェクトのようなdict + +いくつかのヘルパーメソッドを与えます

プロパティの更新と削除を制限する基本的な実装に対する彼の回答におけるSven Marnachの功績。

import json 
# ^^ optional - If you don't care if it prints like a dict
# then rip this and __str__ and __repr__ out

class Immutable(object):

    def __init__(self, **kwargs):
        """Sets all values once given
        whatever is passed in kwargs
        """
        for k,v in kwargs.items():
            object.__setattr__(self, k, v)

    def __setattr__(self, *args):
        """Disables setting attributes via
        item.prop = val or item['prop'] = val
        """
        raise TypeError('Immutable objects cannot have properties set after init')

    def __delattr__(self, *args):
        """Disables deleting properties"""
        raise TypeError('Immutable objects cannot have properties deleted')

    def __getitem__(self, item):
        """Allows for dict like access of properties
        val = item['prop']
        """
        return self.__dict__[item]

    def __repr__(self):
        """Print to repl in a dict like fashion"""
        return self.pprint()

    def __str__(self):
        """Convert to a str in a dict like fashion"""
        return self.pprint()

    def __eq__(self, other):
        """Supports equality operator
        immutable({'a': 2}) == immutable({'a': 2})"""
        if other is None:
            return False
        return self.dict() == other.dict()

    def keys(self):
        """Paired with __getitem__ supports **unpacking
        new = { **item, **other }
        """
        return self.__dict__.keys()

    def get(self, *args, **kwargs):
        """Allows for dict like property access
        item.get('prop')
        """
        return self.__dict__.get(*args, **kwargs)

    def pprint(self):
        """Helper method used for printing that
        formats in a dict like way
        """
        return json.dumps(self,
            default=lambda o: o.__dict__,
            sort_keys=True,
            indent=4)

    def dict(self):
        """Helper method for getting the raw dict value
        of the immutable object"""
        return self.__dict__

ヘルパーメソッド

def update(obj, **kwargs):
    """Returns a new instance of the given object with
    all key/val in kwargs set on it
    """
    return immutable({
        **obj,
        **kwargs
    })

def immutable(obj):
    return Immutable(**obj)

obj = immutable({
    'alpha': 1,
    'beta': 2,
    'dalet': 4
})

obj.alpha # 1
obj['alpha'] # 1
obj.get('beta') # 2

del obj['alpha'] # TypeError
obj.alpha = 2 # TypeError

new_obj = update(obj, alpha=10)

new_obj is not obj # True
new_obj.get('alpha') == 10 # True
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.