尽管我从来不需要这个,但我突然意识到,在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扩展呢?
(只能在Python 3中工作的答案是可以接受的)。
更新:
从Python3.7开始,方法是使用@dataclass
装饰器,请参阅新接受的答案。
发布于 2019-11-22 18:57:42
使用冻结的数据类
对于Python 3.7+,您可以使用带有frozen=True
option的,这是一种非常Python风格且可维护的方式来做您想做的事情。
它看起来像这样:
from dataclasses import dataclass
@dataclass(frozen=True)
class Immutable:
a: Any
b: Any
因为数据类的字段需要类型提示,所以我使用了Any from the typing
module。
不使用命名元组的原因
在Python3.7之前,经常会看到命名元组被用作不可变对象。这可能在很多方面都很棘手,其中之一是命名元组之间的__eq__
方法不考虑对象的类。例如:
from collections import namedtuple
ImmutableTuple = namedtuple("ImmutableTuple", ["a", "b"])
ImmutableTuple2 = namedtuple("ImmutableTuple2", ["a", "c"])
obj1 = ImmutableTuple(a=1, b=2)
obj2 = ImmutableTuple2(a=1, c=2)
obj1 == obj2 # will be True
正如您所看到的,即使obj1
和obj2
的类型不同,即使它们的字段名称不同,obj1 == obj2
仍然提供True
。这是因为使用的__eq__
方法是元组的方法,它只比较给定位置的字段的值。这可能是一个巨大的错误来源,特别是当您对这些类进行子类化时。
发布于 2011-01-28 21:39:09
我刚刚想到的另一个解决方案是:获得与原始代码相同的行为的最简单方法是
Immutable = collections.namedtuple("Immutable", ["a", "b"])
它没有解决可以通过[0]
等访问属性的问题,但至少它相当短,并提供了与pickle
和copy
兼容的额外优势。
namedtuple
创建了一个类似于我在this answer中描述的类型,即从tuple
派生并使用__slots__
。Python 2.6或更高版本中提供。
发布于 2011-01-28 20:17:03
最简单的方法是使用__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)
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__()
。此外,这不会阻止tuple.__getitem__(p, 0)
的使用,但我很难想象这会成为一个问题。
我不认为创建不可变对象的“正确”方法是编写C扩展。Python通常依赖于库实现者和库用户是consenting adults,而不是真正强制接口,接口应该在文档中清楚地说明。这就是为什么我不认为通过调用object.__setattr__()
来规避被覆盖的__setattr__()
的可能性是一个问题。如果有人这么做,那是她自己承担的风险。
https://stackoverflow.com/questions/4828080
复制相似问题