如何在Python中创建不可变对象?

How to make an immutable object in Python?

虽然我从未需要过这个,但我突然想到,在Python中生成一个不变的对象可能有点棘手。您不能仅仅重写__setattr__,因为这样您甚至不能在__init__中设置属性。对元组进行子类化是一个有效的技巧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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

但是你可以通过self[0]self[1]访问ab变量,这很烦人。

这在纯Python中是可能的吗?如果没有,我怎么做C扩展?

(只在Python3中工作的答案是可以接受的)。

更新:

因此,子类化tuple是在纯python中实现它的方法,除了通过[0][1]等方式访问数据的额外可能性外,它工作得很好。因此,要完成这个问题,所缺少的就是如何在c中"正确"地实现它,我认为这很简单,不实现任何geititemsetattribute。等等,但我不是自己做的,而是提供一份赏金,因为我很懒。:)


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

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

它并不能解决通过[0]等方式访问属性的问题,但至少它要短得多,并且提供了与picklecopy兼容的额外优势。

namedtuple创建了一个类似于我在这个答案中描述的类型,即派生自tuple并使用__slots__的类型。它在python 2.6或更高版本中提供。


最简单的方法是使用__slots__

1
2
class A(object):
    __slots__ = []

A的实例现在是不可变的,因为您不能对它们设置任何属性。

如果希望类实例包含数据,可以将其与从tuple派生的数据结合起来:

1
2
3
4
5
6
7
8
9
10
11
12
13
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__()

1
2
3
4
5
6
7
8
9
10
11
12
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通常依赖于库实现者和库用户作为同意的成年人,而不是真正执行接口,应该在文档中清楚地说明接口。这就是为什么我不考虑通过称object.__setattr__()为问题来规避被重写的__setattr__()的可能性。如果有人这样做,风险由她自己承担。


..howto do it"properly" in C..

可以使用cython为python创建扩展类型:

1
2
3
4
5
6
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。

测验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# 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是首选的:

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


另一个想法是完全不允许__setattr__并在构造函数中使用object.__setattr__

1
2
3
4
5
6
7
8
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,但您的原始实现也有同样的问题(在Immutable实例上尝试tuple.__setattr__(i,"x", 42))。

您可以在最初的实现中应用相同的技巧:去掉__getitem__(),在属性函数中使用tuple.__getitem__()


您可以创建一个@immutable修饰器,它可以重写__setattr__并将__slots__更改为空列表,然后用它修饰__init__方法。

编辑:如操作说明,更改__slots__属性只会阻止创建新属性,而不是修改。

edit2:这是一个实现:

edit3:使用__slots__会破坏此代码,因为如果停止创建对象的__dict__。我正在寻找另一种选择。

伊迪丝4:好吧,就是这样。这是一个很简单的方法,但作为一个练习是可行的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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


我不认为这是完全可能的,除非使用一个元组或一个命名的元组。不管怎样,如果您覆盖__setattr__(),用户总是可以通过直接调用object.__setattr__()来绕过它。任何依赖于__setattr__的解决方案都保证不起作用。

以下是不使用某种元组就可以得到的最接近的值:

1
2
3
4
5
6
7
8
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__

但如果你足够努力,它就会断裂:

1
2
3
4
5
6
>>> t = Immutable(1, 2)
>>> t.a
1
>>> object.__setattr__(t, 'a', 2)
>>> t.a
2

但是Sven使用namedtuple是不可改变的。

更新

既然这个问题已经被更新为询问如何在C语言中正确地完成它,下面是我关于如何在Cython中正确地完成它的答案:

第一个immutable.pyx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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

1
2
3
4
5
6
7
8
9
10
11
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
)

然后尝试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> 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)
>>>


除了其他优秀的答案之外,我还想为python 3.4(或者3.3)添加一个方法。这个答案建立在以前对这个问题的几个答案的基础上。

在Python3.4中,可以使用不带setter的属性来创建不能修改的类成员。(在早期版本中,可以不使用setter分配属性。)

1
2
3
4
5
6
7
class A:
    __slots__=['_A__a']
    def __init__(self, aValue):
      self.__a=aValue
    @property
    def a(self):
        return self.__a

您可以这样使用它:

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

将打印"constant"

但是打电话给instance.a=10会导致:

1
AttributeError: can't set attribute

解释:没有setter的属性是Python3.4(我认为是3.3)最新的特性。如果试图分配给此类属性,将引发错误。使用插槽i将成员变量限制为__A_a(即__a)。

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

然而,这个答案不鼓励使用__slots__。最好使用其他方法来防止属性创建。


下面是一个优雅的解决方案:

1
2
3
4
5
6
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))

从这个类继承,初始化构造函数中的字段,然后全部设置。


我通过重写__setattr__并在调用者是__init__时允许集合来创建不可变类:

1
2
3
4
5
6
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__改变对象,但你得到了这个想法。


如果您对具有行为的对象感兴趣,那么namedDuple几乎就是您的解决方案。

如NamedDuple文档底部所述,您可以从NamedDuple派生自己的类;然后,您可以添加所需的行为。

例如(直接从文档中获取的代码):

1
2
3
4
5
6
7
8
9
10
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)

这将导致:

1
2
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上测试过)。唯一的缺点是继承树有点奇怪,但这不是你通常玩的东西。


这样做并不能阻止object.__setattr__的工作,但我仍然觉得它很有用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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__)。


从python 3.7开始,您可以在类中使用@dataclass修饰器,它将像结构一样是不可变的!不过,它可能会也可能不会向类中添加__hash__()方法。报价:

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.

下面是上面链接的文档的示例:

1
2
3
4
5
6
7
8
9
@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


不久前我就需要它,并决定为它制作一个python包。初始版本现在在pypi上:

1
$ pip install immutable

使用:

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

完整文档:https://github.com/theengineear/immutable

希望它有帮助,它像前面讨论过的那样包装了一个名称双重,但是使实例化变得简单多了。


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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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


继承自以下Immutable类的类在其__init__方法完成执行后是不可变的,它们的实例也是不可变的。因为它是纯Python,正如其他人指出的那样,没有什么能阻止某人使用基础objecttype中的突变特殊方法,但这足以阻止任何人意外地改变类/实例。

它通过使用元类劫持类创建过程来工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
"""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__')

第三方attr模块提供此功能。

编辑:python 3.7已经在stdlib和@dataclass中采用了这个思想。

1
2
3
4
5
6
7
8
9
10
$ 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(uux_uuuuu)方法(除非您关闭其中任何一个方法),包括repr、in it、hash和所有比较函数。

attr也为__slots__提供帮助。


另一种方法是创建一个包装器,使实例不可变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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)

在只有某些实例必须是不可变的情况下(如函数调用的默认参数),这很有用。

也可用于不可变的工厂,如:

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

也可以保护自己不受object.__setattr__的影响,但由于python的动态特性,它很容易被其他技巧所欺骗。


我和亚历克斯的想法是一样的:一个元类和一个"init marker",但是结合了过度写作uusetattr_uuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuux:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
>>> 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都有效。

1
2
3
4
5
6
7
8
9
10
11
>>> 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.

它也适用于插槽……:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> 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.

…以及多重继承:

1
2
3
4
5
6
7
8
9
10
11
>>> 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.

但是,请注意可变属性是可变的:

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

这里没有真正包括的一件事是完全不变…不仅是父对象,还包括所有子对象。例如,元组/冻结集可能是不可变的,但它所属的对象可能不是。下面是一个小的(不完整的)版本,它可以很好地实现不可变性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
# 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)


您可以在init的最后一条语句中重写setattr。然后可以构造,但不能更改。显然,您仍然可以使用object.setattr进行重写,但是在实践中,大多数语言都有某种形式的反射,因此不变性始终是一种泄漏的抽象。不变性更多的是防止客户机意外地违反对象的契约。我使用:

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

提供的原始解决方案不正确,这是根据使用此处解决方案的注释进行更新的。

最初的解决方案在一个有趣的方面是错误的,所以它包含在底部。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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
3
4
5
1
2
Attempted To Modify Immutable Object
1
2

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

原始实施:

注释中指出,这实际上不起作用,因为它会阻止在重写类setattr方法时创建多个对象,这意味着不能将第二个对象创建为自身。第二次初始化时,A=将失败。

1
2
3
4
5
6
7
8
9
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")


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

  • 可以像往常一样写入__init__(),访问属性。
  • 在此之后,对象仅为属性更改而冻结:

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

所以我们需要一些方法(_freeze)来存储这两个实现,并在需要时在它们之间进行切换。

这种机制可以在用户类内部实现,也可以从一个特殊的Freezer类继承,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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()