虽然我从来都不需要这样做,但我突然意识到用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
但是你可以通过self[0]和self[1]访问a和b变量,这很烦人。
这在Pure Python中可行吗?如果不是,我该如何用C扩展来做呢?
(只能在python3中工作的答案是可以接受的)。
更新:
从Python 3.7开始,要使用的方法是使用@dataclass装饰器,参见最新接受的答案。
第三方attr模块提供了此功能。
编辑:python 3.7已经通过@dataclass在stdlib中采用了这个想法。
$ 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在每次实例化时都有轻微的性能影响。
如果您习惯使用类作为数据类型,attr可能特别有用,因为它为您处理样板文件(但没有任何魔力)。特别地,它为你编写了9个dunder (__X__)方法(除非你关闭其中任何一个),包括repr, init, hash和所有比较函数。
Attr还为__slots__提供了一个帮助器。
你可以在init的最后一条语句中重写setAttr。那么你可以构建,但不能改变。显然,你仍然可以用usint对象重写。但在实践中,大多数语言都有某种形式的反射,因此不可变始终是一个有漏洞的抽象。不可变性更多的是防止客户端意外地违反对象的契约。我使用:
=============================
最初提供的解决方案是不正确的,这是基于使用这里的解决方案的评论而更新的
原来的解决方案是错误的,这是一种有趣的方式,所以它被包括在底部。
===============================
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 =将在第二次初始化时失败。
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")
你可以创建一个@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
下面的基本解决方案针对以下场景:
__init__()可以像往常一样访问属性。
在此之后,对象仅冻结属性更改:
其思想是覆盖__setattr__方法,并在每次对象冻结状态改变时替换其实现。
因此,我们需要一些方法(_freeze)来存储这两个实现,并在请求时在它们之间切换。
这个机制可以在用户类内部实现,也可以从一个特殊的freeze类继承,如下所示:
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()
这种方式不停止对象。__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__)。