虽然我从来都不需要这样做,但我突然意识到用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装饰器,参见最新接受的答案。
除了其他优秀的答案之外,我喜欢为python 3.4(或者可能是3.3)添加一个方法。这个答案建立在之前对这个问题的几个答案的基础上。
在python 3.4中,可以使用不带设置符的属性来创建不可修改的类成员。(在早期版本中,可以不使用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
而是调用实例。A =10会导致:
AttributeError: can't set attribute
解释:不带设置符的属性是python 3.4(我认为是3.3)的最新特性。如果您尝试给这样的属性赋值,则会引发Error。
使用插槽,我将成员变量限制为__A_a(即__a)。
问题:赋值给_aa仍然是可能的(instance. _aa =2)。但是如果你给一个私有变量赋值,那是你自己的错…
然而,这个答案不鼓励使用__slots__。使用其他方法来阻止属性创建可能更可取。
我找到了一种方法,不用子类化tuple, namedtuple等。你所需要做的就是在初始化后禁用setattr和delattr(如果你想让一个集合成为不可变的,也要禁用setitem和delitem):
def __init__(self, *args, **kwargs):
# something here
self.lock()
其中lock可以是这样的:
@classmethod
def lock(cls):
def raiser(*a):
raise TypeError('this instance is immutable')
cls.__setattr__ = raiser
cls.__delattr__ = raiser
if hasattr(cls, '__setitem__'):
cls.__setitem__ = raiser
cls.__delitem__ = raiser
你可以用这个方法创建类Immutable,并像我展示的那样使用它。
如果你不想在每个init中都写self.lock(),你可以用元类自动实现:
class ImmutableType(type):
@classmethod
def change_init(mcs, original_init_method):
def __new_init__(self, *args, **kwargs):
if callable(original_init_method):
original_init_method(self, *args, **kwargs)
cls = self.__class__
def raiser(*a):
raise TypeError('this instance is immutable')
cls.__setattr__ = raiser
cls.__delattr__ = raiser
if hasattr(cls, '__setitem__'):
cls.__setitem__ = raiser
cls.__delitem__ = raiser
return __new_init__
def __new__(mcs, name, parents, kwargs):
kwargs['__init__'] = mcs.change_init(kwargs.get('__init__'))
return type.__new__(mcs, name, parents, kwargs)
class Immutable(metaclass=ImmutableType):
pass
Test
class SomeImmutableClass(Immutable):
def __init__(self, some_value: int):
self.important_attr = some_value
def some_method(self):
return 2 * self.important_attr
ins = SomeImmutableClass(3)
print(ins.some_method()) # 6
ins.important_attr += 1 # TypeError
ins.another_attr = 2 # TypeError
如果您对具有行为的对象感兴趣,那么namedtuple几乎是您的解决方案。
正如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.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)
这将导致:
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上也进行了测试)。
唯一的缺点是继承树有点奇怪;但这不是你经常玩的东西。
最简单的方法是使用__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__()是一个问题的可能性。如果有人这么做,风险自负。
我已经创建了一个小型类装饰器decorator,以使类不可变(除了在__init__内部)。作为https://github.com/google/etils的一部分。
from etils import epy
@epy.frozen
class A:
def __init__(self):
self.x = 123 # Inside `__init__`, attribute can be assigned
a = A()
a.x = 456 # AttributeError
这也支持继承。
实现:
_Cls = TypeVar('_Cls')
def frozen(cls: _Cls) -> _Cls:
"""Class decorator which prevent mutating attributes after `__init__`."""
if not isinstance(cls, type):
raise TypeError(f'{cls.__name__} is not a class.')
cls.__init__ = _wrap_init(cls.__init__)
cls.__setattr__ = _wrap_setattr(cls.__setattr__)
return cls
def _wrap_init(init_fn):
"""`__init__` wrapper."""
@functools.wraps(init_fn)
def new_init(self, *args, **kwargs):
if hasattr(self, '_epy_is_init_done'):
# `_epy_is_init_done` already created, so it means we're
# a `super().__init__` call.
return init_fn(self, *args, **kwargs)
object.__setattr__(self, '_epy_is_init_done', False)
init_fn(self, *args, **kwargs)
object.__setattr__(self, '_epy_is_init_done', True)
return new_init
def _wrap_setattr(setattr_fn):
"""`__setattr__` wrapper."""
@functools.wraps(setattr_fn)
def new_setattr(self, name, value):
if not hasattr(self, '_epy_is_init_done'):
raise ValueError(
'Child of `@epy.frozen` class should be `@epy.frozen` too. (Error'
f' raised by {type(self)})'
)
if not self._epy_is_init_done: # pylint: disable=protected-access
return setattr_fn(self, name, value)
else:
raise AttributeError(
f'Cannot assign {name!r} in `@epy.frozen` class {type(self)}'
)
return new_setattr