虽然我从来都不需要这样做,但我突然意识到用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装饰器,参见最新接受的答案。
另一种方法是创建一个使实例不可变的包装器。
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))
也保护对象。__setattr__,但由于Python的动态特性,可能会被其他技巧所绊倒。
最简单的方法是使用__slots__:
class A(object):
__slots__ = []
A的实例现在是不可变的,因为您不能在它们上设置任何属性。
如果你想让类实例包含数据,你可以将this和derived 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
编辑:如果你想摆脱索引,你可以重写__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__()。此外,这不会阻止使用元组。__getitem__(p, 0),但我很难想象这应该如何构成一个问题。
我不认为创建不可变对象的“正确”方法是编写C扩展。Python通常依赖于库实现者和库用户是成年人,而不是真正强制执行接口,接口应该在文档中清楚地说明。这就是为什么我不认为通过调用object.__setattr__()来规避被重写的__setattr__()是一个问题的可能性。如果有人这么做,风险自负。
你可以创建一个@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
您可以覆盖setattr,仍然使用init来设置变量。你可以使用超类setattr。这是代码。
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
这里没有包括的是完全不可变性……不仅仅是父对象,还有所有的子对象。例如,元组/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)
我通过重写__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__来改变对象,但你懂的。