How to make an immutable object in Python?
虽然我从未需要过这个,但我突然想到,在Python中生成一个不变的对象可能有点棘手。您不能仅仅重写
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 |
但是你可以通过
这在纯Python中是可能的吗?如果没有,我怎么做C扩展?
(只在Python3中工作的答案是可以接受的)。
更新:
因此,子类化tuple是在纯python中实现它的方法,除了通过
我刚才想到的另一个解决方案是:获得与原始代码相同行为的最简单方法是
1 | Immutable = collections.namedtuple("Immutable", ["a","b"]) |
它并不能解决通过
最简单的方法是使用
1 2 | class A(object): __slots__ = [] |
如果希望类实例包含数据,可以将其与从
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 |
编辑:如果您想取消索引,可以覆盖
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 |
请注意,在这种情况下,不能将
我认为创建不可变对象的"正确"方法不是编写C扩展。python通常依赖于库实现者和库用户作为同意的成年人,而不是真正执行接口,应该在文档中清楚地说明接口。这就是为什么我不考虑通过称
..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建议的
1 | Immutable = collections.namedtuple("Immutable","a b") |
另一个想法是完全不允许
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 |
当然,您可以使用
您可以在最初的实现中应用相同的技巧:去掉
您可以创建一个
编辑:如操作说明,更改
edit2:这是一个实现:
edit3:使用
伊迪丝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 |
我不认为这是完全可能的,除非使用一个元组或一个命名的元组。不管怎样,如果您覆盖
以下是不使用某种元组就可以得到的最接近的值:
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使用
更新
既然这个问题已经被更新为询问如何在C语言中正确地完成它,下面是我关于如何在Cython中正确地完成它的答案:
第一个
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) |
以及一个
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) |
将打印
但是打电话给
1 | AttributeError: can't set attribute |
解释:没有setter的属性是Python3.4(我认为是3.3)最新的特性。如果试图分配给此类属性,将引发错误。使用插槽i将成员变量限制为
问题:分配到
然而,这个答案不鼓励使用
下面是一个优雅的解决方案:
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)) |
从这个类继承,初始化构造函数中的字段,然后全部设置。
我通过重写
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) |
这还不够,因为它允许任何人的
如果您对具有行为的对象感兴趣,那么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上测试过)。唯一的缺点是继承树有点奇怪,但这不是你通常玩的东西。
这样做并不能阻止
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) |
根据用例的不同,您可能需要覆盖更多的内容(如
从python 3.7开始,您可以在类中使用
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 |
继承自以下
它通过使用元类劫持类创建过程来工作。
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__') |
第三方
编辑:python 3.7已经在stdlib和
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 |
根据文档,
如果您习惯使用类作为数据类型,那么
另一种方法是创建一个包装器,使实例不可变。
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)) |
也可以保护自己不受
我和亚历克斯的想法是一样的:一个元类和一个"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__() ,访问属性。 - 在此之后,对象仅为属性更改而冻结:
其思想是重写
所以我们需要一些方法(
这种机制可以在用户类内部实现,也可以从一个特殊的
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() |