如何在Python中制作一个不变的对象?

问题:如何在Python中制作一个不变的对象?

尽管我从不需要它,但让我感到惊讶的是,在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]

在纯Python中这可能吗?如果没有,我将如何使用C扩展名呢?

(仅在Python 3中有效的答案是可以接受的)。

更新:

因此,将元组子类化是在纯Python 中完成此操作的方法[0],除了通过[1]等访问数据的附加可能性外,该方法行之有效。要解决此问题,所有遗漏的是如何在C中“正确”进行操作,这我怀疑这很简单,只是不执行任何geititemor setattribute等等。但是我没有为此做,而是为此提供了赏金,因为我很懒。:)

Although I have never needed this, it just struck me that making an immutable object in Python could be slightly tricky. You can’t just override __setattr__, because then you can’t even set attributes in the __init__. Subclassing a tuple is a trick that works:

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

But then you have access to the a and b variables through self[0] and self[1], which is annoying.

Is this possible in Pure Python? If not, how would I do it with a C extension?

(Answers that work only in Python 3 are acceptable).

Update:

So subclassing tuple is the way to do it in Pure Python, which works well except for the additional possibility of accessing the data by [0], [1] etc. So, to complete this question all that is missing is howto do it “properly” in C, which I suspect would be quite simple, by just not implementing any geititem or setattribute, etc. But instead of doing it myself, I offer a bounty for that, because I’m lazy. :)


回答 0

我刚刚想到的另一种解决方案:获得与原始代码相同行为的最简单方法是

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

它不能解决可以通过[0]等访问属性的问题,但至少它要短得多,并具有与pickle和兼容的附加优点。copy

namedtuple创建与我在此答案中描述的类型相似的类型,即派生自tuple并使用__slots__。它在Python 2.6或更高版本中可用。

Yet another solution I just thought of: The simplest way to get the same behaviour as your original code is

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

It does not solve the problem that attributes can be accessed via [0] etc., but at least it’s considerably shorter and provides the additional advantage of being compatible with pickle and copy.

namedtuple creates a type similar to what I described in this answer, i.e. derived from tuple and using __slots__. It is available in Python 2.6 or above.


回答 1

最简单的方法是使用__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__()问题来规避被覆盖的可能性的原因。如果有人这样做,则后果自负。

The easiest way to do this is using __slots__:

class A(object):
    __slots__ = []

Instances of A are immutable now, since you can’t set any attributes on them.

If you want the class instances to contain data, you can combine this with deriving from 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

Edit: If you want to get rid of indexing either, you can override __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

Note that you can’t use operator.itemgetter for the properties in thise case, since this would rely on Point.__getitem__() instead of tuple.__getitem__(). Fuerthermore this won’t prevent the use of tuple.__getitem__(p, 0), but I can hardly imagine how this should constitute a problem.

I don’t think the “right” way of creating an immutable object is writing a C extension. Python usually relies on library implementers and library users being consenting adults, and instead of really enforcing an interface, the interface should be clearly stated in the documentation. This is why I don’t consider the possibility of circumventing an overridden __setattr__() by calling object.__setattr__() a problem. If someone does this, it’s on her own risk.


回答 2

..如何在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'

如果您不介意索引支持,那么@Sven Marnach的collections.namedtuple建议是可取的:

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

..howto do it “properly” in C..

You could use Cython to create an extension type for 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

It works both Python 2.x and 3.

Tests

# 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'

If you don’t mind indexing support then collections.namedtuple suggested by @Sven Marnach is preferrable:

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

回答 3

另一个想法是完全禁止在构造函数中__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__()在属性函数中使用。

Another idea would be to completely disallow __setattr__ and use object.__setattr__ in the constructor:

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

Of course you could use object.__setattr__(p, "x", 3) to modify a Point instance p, but your original implementation suffers from the same problem (try tuple.__setattr__(i, "x", 42) on an Immutable instance).

You can apply the same trick in your original implementation: get rid of __getitem__(), and use tuple.__getitem__() in your property functions.


回答 4

您可以创建一个@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

You could create a @immutable decorator that either overrides the __setattr__ and change the __slots__ to an empty list, then decorate the __init__ method with it.

Edit: As the OP noted, changing the __slots__ attribute only prevents the creation of new attributes, not the modification.

Edit2: Here’s an implementation:

Edit3: Using __slots__ breaks this code, because if stops the creation of the object’s __dict__. I’m looking for an alternative.

Edit4: Well, that’s it. It’s a but hackish, but works as an exercise :-)

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

回答 5

使用冻结的数据类

对于Python 3.7+,您可以使用带有option数据类,这是一种非常Python化且可维护的方式来完成您想要的事情。frozen=True

它看起来像这样:

from dataclasses import dataclass

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

由于数据类的字段需要类型提示,因此我typing模块中使用了Any

不使用命名元组的原因

在Python 3.7之前,经常会看到namedtuple被用作不可变对象。在许多方面可能很棘手,其中之一是__eq__namedtuple之间的方法不考虑对象的类。例如:

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__使用的方法是元组的方法,该方法仅比较给定位置的字段的值。这可能是错误的巨大来源,特别是如果您将这些类作为子类。

Using a Frozen Dataclass

For Python 3.7+ you can use a Data Class with a frozen=True option, which is a very pythonic and maintainable way to do what you want.

It would look something like that:

from dataclasses import dataclass

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

As type hinting is required for dataclasses’ fields, I have used Any from the typing module.

Reasons NOT to use a Namedtuple

Before Python 3.7 it was frequent to see namedtuples being used as immutable objects. It can be tricky in many ways, one of them is that the __eq__ method between namedtuples does not consider the objects’ classes. For example:

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

As you see, even if the types of obj1 and obj2 are different, even if their fields’ names are different, obj1 == obj2 still gives True. That’s because the __eq__ method used is the tuple’s one, which compares only the values of the fields given their positions. That can be a huge source of errors, specially if you are subclassing these classes.


回答 6

除了使用元组或namedtuple,我认为这是完全不可能的。无论如何,如果您覆盖__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

但是Sven的使用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)
>>>      

I don’t think it is entirely possible except by using either a tuple or a namedtuple. No matter what, if you override __setattr__() the user can always bypass it by calling object.__setattr__() directly. Any solution that depends on __setattr__ is guaranteed not to work.

The following is about the nearest you can get without using some sort of tuple:

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__

but it breaks if you try hard enough:

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

but Sven’s use of namedtuple is genuinely immutable.

Update

Since the question has been updated to ask how to do it properly in C, here’s my answer on how to do it properly in Cython:

First 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)

and a setup.py to compile it (using the command 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
)

Then to try it out:

>>> 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)
>>>      

回答 7

我通过重写来实现了不可变的类__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__更改对象,但是您明白了。

I’ve made immutable classes by overriding __setattr__, and allowing the set if the caller is __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)

This isn’t quite enough yet, since it allows anyone’s ___init__ to change the object, but you get the idea.


回答 8

除了出色的其他答案外,我还想为python 3.4(或3.3)添加一个方法。该答案建立在对该问题的多个先前答案的基础上。

在python 3.4中,可以使用不带setter的属性来创建无法修改的类成员。(在较早的版本中,可以不使用setter来分配属性。)

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

解释:没有setter的属性是python 3.4(我认为3.3)的最新功能。如果您尝试分配给这样的属性,则会引发错误。使用插槽,我将membervariables限制为__A_a(是__a)。

问题:_A__a仍然可以分配到(instance._A__a=2)。但是如果您分配一个私有变量,那是您自己的错…

但是,此答案除其他外,不鼓励使用__slots__。最好使用其他方式来防止属性创建。

In addition to the excellent other answers I like to add a method for python 3.4 (or maybe 3.3). This answer builds upon several previouse answers to this question.

In python 3.4, you can use properties without setters to create class members that cannot be modified. (In earlier versions assigning to properties without a setter was possible.)

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

You can use it like this:

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

which will print "constant"

But calling instance.a=10 will cause:

AttributeError: can't set attribute

Explaination: properties without setters are a very recent feature of python 3.4 (and I think 3.3). If you try to assign to such a property, an Error will be raised. Using slots I restrict the membervariables to __A_a (which is __a).

Problem: Assigning to _A__a is still possible (instance._A__a=2). But if you assign to a private variable, it is your own fault…

This answer among others, however, discourages the use of __slots__. Using other ways to prevent attribute creation might be preferrable.


回答 9

这是一个优雅的解决方案:

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))

从此类继承,初始化构造函数中的字段,一切就绪。

Here’s an elegant solution:

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))

Inherit from this class, initialize your fields in the constructor, and you’e all set.


回答 10

如果您对具有行为的对象感兴趣,那么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和2.7(也已在IronPython上测试)。
唯一的缺点是继承树有点怪异。但这不是您通常玩的东西。

If you are interested in objects with behavior, then namedtuple is almost your solution.

As described at the bottom of the namedtuple documentation, you can derive your own class from namedtuple; and then, you can add the behavior you want.

For example (code taken directly from the documentation):

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)

This will result in:

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

This approach works for both Python 3 and Python 2.7 (tested on IronPython as well).
The only downside is that the inheritance tree is a bit weird; but this is not something you usually play with.


回答 11

Immutable__init__方法完成执行后,继承自以下类的类及其实例是不可变的。正如其他人所指出的那样,由于它是纯python,所以没有什么可以阻止某人使用从base object和的变异特殊方法的type,但这足以阻止任何人无意间变异一个类/实例。

它通过劫持一个元类的类创建过程来工作。

"""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__')

Classes which inherit from the following Immutable class are immutable, as are their instances, after their __init__ method finishes executing. Since it’s pure python, as others have pointed out, there’s nothing stopping someone from using the mutating special methods from the base object and type, but this is enough to stop anyone from mutating a class/instance by accident.

It works by hijacking the class-creation process with a metaclass.

"""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__')

回答 12

前一阵子我需要这个,并决定为此制作一个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

希望它能有所帮助,它如前所述包装了一个namedtuple,但是使实例化变得更加简单。

I needed this a little while ago and decided to make a Python package for it. The initial version is on PyPI now:

$ pip install immutable

To use:

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

Full docs here: https://github.com/theengineear/immutable

Hope it helps, it wraps a namedtuple as has been discussed, but makes instantiation much simpler.


回答 13

这种方法不会停止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__根据用例覆盖更多的内容(例如)。

This way doesn’t stop object.__setattr__ from working, but I’ve still found it useful:

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)

you may need to override more stuff (like __setitem__) depending on the use case.


回答 14

从Python 3.7开始,您可以在类中使用@dataclass装饰器,它将像结构一样是不可变的!虽然,它可能会也可能不会__hash__()在您的类中添加方法。引用:

hash()由内置的hash()以及将对象添加到哈希集合(如字典和集合)时使用。带有hash()表示该类的实例是不可变的。可变性是一个复杂的属性,它取决于程序员的意图,eq()的存在和行为以及dataclass()装饰器中的eq和冻结标志的值。

默认情况下,除非这样做是安全的,否则dataclass()不会隐式添加hash()方法。它不会添加或更改现有的显式定义的hash()方法。如hash()文档中所述,设置类属性hash = None对Python具有特定的含义。

如果未明确定义hash()或将其设置为None,则dataclass()可以添加隐式hash()方法。尽管不建议这样做,但是您可以强制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

As of Python 3.7, you can use the @dataclass decorator in your class and it will be immutable like a struct! Though, it may or may not add a __hash__() method to your class. Quote:

hash() is used by built-in hash(), and when objects are added to hashed collections such as dictionaries and sets. Having a hash() implies that instances of the class are immutable. Mutability is a complicated property that depends on the programmer’s intent, the existence and behavior of eq(), and the values of the eq and frozen flags in the dataclass() decorator.

By default, dataclass() will not implicitly add a hash() method unless it is safe to do so. Neither will it add or change an existing explicitly defined hash() method. Setting the class attribute hash = None has a specific meaning to Python, as described in the hash() documentation.

If hash() is not explicit defined, or if it is set to None, then dataclass() may add an implicit hash() method. Although not recommended, you can force dataclass() to create a hash() method with unsafe_hash=True. This might be the case if your class is logically immutable but can nonetheless be mutated. This is a specialized use case and should be considered carefully.

Here the example from the docs linked above:

@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

回答 15

您可以覆盖setattr并仍然使用init来设置变量。您将使用超类setattr。这是代码。

不可变的类别:
    __slots__ =('a','b')
    def __init __(self,a,b):
        super().__ setattr __('a',a)
        super().__ setattr __('b',b)

    def __str __():
        返回“” .format(self.a,self.b)

    def __setattr __(自己,*忽略):
        引发NotImplementedError

    def __delattr __(自己,*忽略):
        引发NotImplementedError

You can override setattr and still use init to set the variable. You would use super class setattr. here is the code.

class Immutable:
    __slots__ = ('a','b')
    def __init__(self, a , b):
        super().__setattr__('a',a)
        super().__setattr__('b',b)

    def __str__(self):
        return "".format(self.a, self.b)

    def __setattr__(self, *ignored):
        raise NotImplementedError

    def __delattr__(self, *ignored):
        raise NotImplementedError

回答 16

第三方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那么它可能会特别有用,因为它可以为您处理样板(但不会产生任何魔力)。特别是,它为您编写了九种dunder(__X__)方法(除非您将其中的任何一种都关闭了),包括repr,init,hash和所有比较功能。

attr还为提供了帮助__slots__

The third party attr module provides this functionality.

Edit: python 3.7 has adopted this idea into the stdlib with @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 implements frozen classes by overriding __setattr__ and has a minor performance impact at each instantiation time, according to the documentation.

If you’re in the habit of using classes as datatypes, attr may be especially useful as it takes care of the boilerplate for you (but doesn’t do any magic). In particular, it writes nine dunder (__X__) methods for you (unless you turn any of them off), including repr, init, hash and all the comparison functions.

attr also provides a helper for __slots__.


回答 17

因此,我正在分别编写python 3:

I)在数据类修饰器的帮助下并设置Frozen = True。我们可以在python中创建不可变的对象。

为此,需要从数据类库中导入数据类,并需要设置Frozen = True

例如

从数据类导入数据类

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

o / p:

l =位置(“ Delhi”,112.345,234.788)l.name’Delhi’l。经度112.345 l.latitude 234.788 l.name =“加尔各答”数据类。FrozenInstanceError:无法分配给字段’name’

资料来源:https : //realpython.com/python-data-classes/

So, I am writing respective of python 3:

I) with the help of data class decorator and set frozen=True. we can create immutable objects in python.

for this need to import data class from data classes lib and needs to set frozen=True

ex.

from dataclasses import dataclass

@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: cannot assign to field 'name'
>>> 

Source: https://realpython.com/python-data-classes/


回答 18

一种替代方法是创建使实例不可变的包装器。

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的动态特性,也可以防止,但也容易出错。

An alternative approach is to create a wrapper which makes an instance immutable.

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)

This is useful in situations where only some instances have to be immutable (like default arguments of function calls).

Can also be used in immutable factories like:

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

Also protects from object.__setattr__, but fallable to other tricks due to Python’s dynamic nature.


回答 19

我使用了与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]

I used the same idea as Alex: a meta-class and an “init marker”, but in combination with over-writing __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)

Note: I’m calling the meta-class directly to make it work both for Python 2.x and 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.

It does work also with slots …:

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

… and multiple inheritance:

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

Note, however, that mutable attributes stay to be mutable:

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

回答 20

这里没有真正包含的一件事是完全不变性……不仅是父对象,还包括所有子对象。例如,元组/ frozensets可能是不可变的,但它所属的对象可能不是不变的。这是一个小的(不完整的)版本,可以很好地执行强制不变性的操作:

# 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)

One thing that’s not really included here is total immutability… not just the parent object, but all the children as well. tuples/frozensets may be immutable for instance, but the objects that it’s part of may not be. Here’s a small (incomplete) version that does a decent job of enforcing immutability all the way down:

# 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)

回答 21

您可以在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方法时阻止创建多个对象,这意味着无法创建第二个对象作为self.a = will在第二次初始化时失败。

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")

You can just override setAttr in the final statement of init. THen you can construct but not change. Obviously you can still override by usint object.setAttr but in practice most languages have some form of reflection so immutablility is always a leaky abstraction. Immutability is more about preventing clients from accidentally violating the contract of an object. I use:

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

The original solution offered was incorrect, this was updated based on the comments using the solution from here

The original solution is wrong in an interesting way, so it is included at the bottom.

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

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

Output :

1
2
Attempted To Modify Immutable Object
1
2

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

Original Implementation:

It was pointed out in the comments, correctly, that this does not in fact work, as it prevents the creation of more than one object as you are overriding the class setattr method, which means a second cannot be created as self.a = will fail on the second initialisation.

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")

回答 22

下面的基本解决方案解决了以下情况:

  • __init__() 可以像往常一样访问属性。
  • 冻结对象以更改属性后,仅:

想法是在__setattr__每次对象冻结状态更改时重写方法并替换其实现。

因此,我们需要一些方法(_freeze),用于存储这两种实现并在需要时在它们之间进行切换。

该机制可以在用户类内部实现,也可以从特殊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()

The basic solution below addresses the following scenario:

  • __init__() can be written accessing the attributes as usual.
  • AFTER that the OBJECT is frozen for attributes changes only:

The idea is to override __setattr__ method and replace its implementation each time the object frozen status is changed.

So we need some method (_freeze) which stores those two implementations and switches between them when requested.

This mechanism may be implemented inside the user class or inherited from a special Freezer class as shown below:

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()

回答 23

就像一个 dict

我有一个开放源代码库,在这里我以一种功能性的方式来做事,因此在不可变对象中移动数据很有帮助。但是,我不需要转换数据对象以使客户端与之交互。因此,我想出了这一点- 它为您提供了一个像对象一样不变的字典 +一些辅助方法。

感谢斯文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

Just Like a dict

I have an open source library where I’m doing things in a functional way so moving data around in an immutable object is helpful. However, I don’t want to have to transform my data object for the client to interact with them. So, I came up with this – it gives you a dict like object thats immutable + some helper methods.

Credit to Sven Marnach in his answer for the basic implementation of restricting property updating and deleting.

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__

Helper methods

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)

Examples

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