Мне интересно, как определить объект значения в Python. В Википедии: объект ценности - это небольшой объект, который представляет собой простой объект, чье равенство не основано на идентичности: т.е. два объекта значения равны, когда они имеют одинаковое значение, не обязательно являясь одним и тем же объектом ". В Python по существу означает переопределенные методы __eq__
и __hash__
, а также неизменность.
Стандартная namedtuple
кажется почти идеальным решением, за исключением того, что они не хорошо сочетаются с современной Python IDE, такой как PyCharm. Я имею в виду, что среда IDE действительно не даст никакой полезной информации о классе, определенном как namedtuple
. Хотя можно прикрепить docstring к такому классу, используя трюк, подобный этому:
class Point2D(namedtuple("Point2D", "x y")):
"""Class for immutable value objects"""
pass
просто нет места, где можно задать описание аргументов конструктора и указать их типы. PyCharm достаточно умен, чтобы угадать аргументы для Point2D
"constructor", но по типу он слеп.
Этот код содержит некоторую информацию о типе, но это не очень полезно:
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 являются float, поэтому не помогли бы обнаружить их неправильное использование. И мне также не нравится идея переопределения "магических" методов на обычной основе.
Итак, я ищу что-то, что будет:
- так же легко определить, как обычный класс Python или namedtuple
- предоставлять семантику ценности (равенство, хэши, неизменность)
- легко документировать, что будет хорошо работать с 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
Я пытался найти что-то подобное, но безуспешно. Я подумал, что было бы разумно попросить о помощи, прежде чем реализовать ее самостоятельно.
UPDATE: С помощью 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
Всего четыре раза перечислить все атрибуты: в __slots__
, в аргументах ctor, в docstring и в корпусе ctor. До сих пор я понятия не имею, как сделать это менее неудобным.