I'm curious about good way to define value object in Python. Per Wikipedia: "value object is a small object that represents a simple entity whose equality isn't based on identity: i.e. two value objects are equal when they have the same value, not necessarily being the same object". In Python that essentially means redefined __eq__ and __hash__ methods, as well as immutability.
Standard namedtuple seems like almost perfect solution with exception that they don't play well with modern Python IDE like PyCharm. I mean that IDE will not really provide any helpful insights about class defined as namedtuple. While it's possible to attach docstring to such class using trick like this:
class Point2D(namedtuple("Point2D", "x y")):
"""Class for immutable value objects"""
pass
there's simply no place where to put description of constructor arguments and specify their types. PyCharm is smart enough to guess arguments for Point2D "constructor", but type-wise it's blind.
This code have some type information pushed in, but it's not very useful:
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 will see types when constructing new objects, but will not grasp that point.x and point.y are floats, so would not not help to detect their misuse. And I also dislike the idea of redefining "magic" methods on routine basis.
So I'm looking for something that will be:
- just as easy to define as normal Python class or namedtuple
- provide value semantics (equality, hashes, immutability)
- easy to document in a way that will play nicely with IDE
Ideal solution could look like this:
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)
Or that:
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
I tried to find something like this but without success. I thought that it will be wise to ask for help before implementing it by myself.
UPDATE: With help of user4815162342 I managed to come up with something that works. Here's the code:
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)
It's very far from an ideal solution. Class declaration looks like this:
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
It's total FOUR times to list all attributes: in __slots__, in ctor arguments, in docstring and in ctor body. So far I have no idea how to make it less awkward.


namedtuplehas a primary purpose of providing both the tuple interface (indexing, unpacking), and attribute access. It was invented for backward-compatibility of functions that used to return tuples, such asos.statortime.gmtime. It is likely not the optimal choice for a simple value type.namedtuplealmost do the right job for me. It's definitely more then simple value object, but I can live with it. As for using statically typed language, I wish I could. But I have a Python project on hands and looking for a way to make development more comfortable. And PyCharm already does very good job for inferring type of variables using docstrings.