我很好奇在 Python 中定义值对象的好方法。根据维基百科:“值对象是一个小对象,它代表一个简单实体,其相等性不基于身份:即两个值对象在具有相同值时相等,不一定是同一个对象”。在 Python 中,这本质上意味着重新定义__eq__
和__hash__
方法,以及不变性。
标准namedtuple
似乎几乎是完美的解决方案,但它们不能很好地与 PyCharm 等现代 Python IDE 配合使用。我的意思是 IDE 不会真正提供关于定义为namedtuple
. 虽然可以使用以下技巧将文档字符串附加到此类:
class Point2D(namedtuple("Point2D", "x y")):
"""Class for immutable value objects"""
pass
根本没有地方可以放置构造函数参数的描述并指定它们的类型。PyCharm 足够聪明,可以猜测Point2D
“构造函数”的参数,但在类型方面它是盲目的。
这段代码推入了一些类型信息,但它不是很有用:
class Point2D(namedtuple("Point2D", "x y")):
"""Class for immutable value objects"""
def __new__(cls, x, y):
"""
:param x: X coordinate
:type x: float
:param y: Y coordinate
:type y: float
:rtype: Point2D
"""
return super(Point2D, cls).__new__(cls, x, y)
point = Point2D(1.0, 2.0)
PyCharm 在构造新对象时会看到类型,但不会掌握 point.x 和 point.y 是浮点数,因此无助于检测它们的滥用。而且我也不喜欢在常规基础上重新定义“魔术”方法的想法。
所以我正在寻找的东西是:
- 就像普通的 Python 类或命名元组一样容易定义
- 提供值语义(平等、散列、不变性)
- 易于以与 IDE 完美配合的方式记录
理想的解决方案可能如下所示:
class Point2D(ValueObject):
"""Class for immutable value objects"""
def __init__(self, x, y):
"""
:param x: X coordinate
:type x: float
:param y: Y coordinate
:type y: float
"""
super(Point2D, self).__init__(cls, x, y)
或者那个:
class Point2D(object):
"""Class for immutable value objects"""
__metaclass__ = ValueObject
def __init__(self, x, y):
"""
:param x: X coordinate
:type x: float
:param y: Y coordinate
:type y: float
"""
pass
我试图找到这样的东西,但没有成功。我认为在自己实施之前寻求帮助是明智的。
更新:在 user4815162342 的帮助下,我设法想出了一些可行的方法。这是代码:
class ValueObject(object):
__slots__ = ()
def __repr__(self):
attrs = ' '.join('%s=%r' % (slot, getattr(self, slot)) for slot in self.__slots__)
return '<%s %s>' % (type(self).__name__, attrs)
def _vals(self):
return tuple(getattr(self, slot) for slot in self.__slots__)
def __eq__(self, other):
if not isinstance(other, ValueObject):
return NotImplemented
return self.__slots__ == other.__slots__ and self._vals() == other._vals()
def __ne__(self, other):
return not self == other
def __hash__(self):
return hash(self._vals())
def __getstate__(self):
"""
Required to pickle classes with __slots__
Must be consistent with __setstate__
"""
return self._vals()
def __setstate__(self, state):
"""
Required to unpickle classes with __slots__
Must be consistent with __getstate__
"""
for slot, value in zip(self.__slots__, state):
setattr(self, slot, value)
这与理想的解决方案相去甚远。类声明如下所示:
class X(ValueObject):
__slots__ = "a", "b", "c"
def __init__(self, a, b, c):
"""
:param a:
:type a: int
:param b:
:type b: str
:param c:
:type c: unicode
"""
self.a = a
self.b = b
self.c = c
列出所有属性总共有四次: in __slots__
, in ctor arguments, in docstring 和 in ctor body。到目前为止,我不知道如何使它不那么尴尬。