如何在Python中创build一个不可变的对象?

虽然我从来没有需要这样做,但让我感到惊讶的是,在Python中创build一个不可变的对象可能有点棘手。 你不能只是覆盖__setattr__ ,因为你甚至不能在__init__设置属性。 inheritance一个元组是一个有效的技巧:

 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]来访问abvariables,这很烦人。

纯Python中可能吗? 如果没有,我将如何做一个C扩展?

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

更新:

因此,子类化元组是在纯Python中执行的方式,除了[0][1]等访问数据的额外可能性,这种方法运行良好。因此,要完成这个问题,所有缺less的就是如何“正确“的C语言,我认为这很简单,只是没有实现任何geititemsetattribute等等。但是我自己却没有这么做,因此我提供了一个奖励,因为我很懒。 🙂

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

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

它不能解决通过[0]等可以访问属性的问题,但是至less它比较短,并且提供了兼容picklecopy的附加优点。

namedtuple创build一个类似于我在这个答案中描述的types,即派生自tuple__slots__ 。 它在Python 2.6或更高版本中可用。

最简单的方法是使用__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) px # 2 py # 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)的使用。但是我很难想象这是如何构成问题的。

我不认为创build一个不可变对象的“正确”方法是写一个C扩展。 Python通常依靠图书馆实施者和图书馆用户同意成人 ,而不是真正实施一个接口,接口应该在文档中明确说明。 这就是为什么我不考虑通过调用object.__setattr__()来绕过重写的__setattr__()的可能性。 如果有人这样做,这是在自己的风险。

..如何做到“正确”在C ..

你可以使用Cython为Python创build一个扩展types:

 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。

testing

 # compile on-the-fly import pyximport; pyximport.install() # $ pip install cython from immutable import Immutable o = Immutable(1, 2) assert oa == 1, str(oa) assert ob == 2 try: oa = 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: oc = 1 except AttributeError: pass else: assert 0, 'no new attributes are allowed' o = Immutable('a', []) assert oa == 'a' assert ob == [] obappend(3) # attribute may contain mutable object assert ob == [3] try: oc except AttributeError: pass else: assert 0, 'no c attribute' o = Immutable(b=3,a=1) assert oa == 1 and ob == 3 try: del ob except AttributeError: pass else: assert 0, "can't delete attribute" d = dict(b=3, a=1) o = Immutable(**d) assert oa == d['a'] and ob == d['b'] o = Immutable(1,b=3) assert oa == 1 and ob == 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 Marnachbuild议的collections.namedtu是可取的:

 Immutable = collections.namedtuple("Immutable", "ab") 

另一个想法是完全禁止__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)实例)。

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

你可以创build一个@immutable装饰器,覆盖__setattr__ 并将 __slots__改成空的列表,然后用它装饰__init__方法。

编辑:正如OP注意到的,更改__slots__属性只会阻止创build新的属性 ,而不是修改。

编辑2:这是一个实现:

编辑3:使用__slots__打破这个代码,因为如果停止创build对象的__dict__ 。 我正在寻找替代品。

编辑4:好吧,就是这样。 这是一个黑客,但作为一个练习:-)

 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) px = 3 # Exception: Attribute x has already been set pz = 4 # Exception: Cannot create atribute z 

我不认为这是完全可能的,除了通过使用一个元组或一个命名的元组。 不pipe怎样,如果你重写__setattr__() ,用户可以直接通过调用object.__setattr__()来绕过它。 任何依赖__setattr__解决scheme都保证不起作用。

以下是关于最近你可以得到,而不使用某种元组:

 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) >>> ta 1 >>> object.__setattr__(t, 'a', 2) >>> ta 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> >>> pa = 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 >>> pa, pb (2, 3) >>> 

除了优秀的其他答案,我喜欢为python 3.4(或者3.3)添加一个方法。 这个答案build立在这个问题的几个以前的答案。

在python 3.4中,你可以使用没有setter的属性来创build无法修改的类成员。 (在早期版本中,可以指派给没有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)。 如果您尝试分配这样的属性,将会引发错误。 使用槽我把成员__A_a限制为__A_a (这是__a )。

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

__slots__ , 这个答案不鼓励使用__slots__ 。 使用其他方式来防止属性创build可能是可取的。

我通过重写__setattr__创build不可变类,如果调用者是__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__改变对象,但你明白了。

我刚才需要这个,并决定为它做一个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,但使实例化更简单。

这种方式不会停止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__ )取决于用例。

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

正如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.3fy=%6.3f hypot=%6.3f' % (self.x, self.y, self.hypot) for p in Point(3, 4), Point(14, 5/7): print(p) 

这将导致:

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

这种方法适用于Python 3和Python 2.7(也在IronPython上testing过)。
唯一的缺点是inheritance树有点奇怪, 但这不是你经常玩的东西。

从下面的Immutable类inheritance的类是不可变的,它们的__init__方法执行完毕后,它们的实例也是不可变的。 正如其他人所指出的那样,由于它是纯粹的Python,所以没有任何东西阻止某人从基础objecttype使用变异的特殊方法,但这足以阻止任何人偶然地改变类/实例。

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

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

你可以在init的最后声明中重写setAttr。 你可以构build但不能改变。 显然你仍然可以通过usint对象覆盖。 setAttr,但实际上大多数语言都有某种forms的反思,所以不可动摇总是一种抽象的抽象。 不变性更多的是防止客户意外违反对象的合同。 我用:

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

第三方attr模块提供了这个function 。

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

attr通过覆盖__setattr__实现冻结类,并且在每个实例化时间上都有一个小的性能影响。

如果你习惯使用类作为数据types, attr可能会特别有用,因为它会照顾你的样板(但是没有任何魔法)。 特别是,它为你写了九个dunder(__X__)方法(除非你把它们中的任何一个关掉),包括repr,init,hash和所有的比较函数。

attr还为__slots__提供了一个帮助器 。

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

 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的dynamic特性而不能使用其他技巧。

我使用了与Alex相同的思想:一个元类和一个“init标记”,但是结合覆盖__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. 

…和多重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. 

但是请注意,可变属性保持可变:

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

有一件事情是不完全包括在内的,那就是完全的不变性……不仅仅是父对象,还有所有的孩子。 元组/ 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 eg 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)