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:
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.
def str(self): is a python method which is called when we use print/str to convert object into a string.
Python __str__() This method returns the string representation of the object. This method is called when print() or str() function is invoked on an object. This method must return the String object.
A Value Object is one of the fundamental building blocks of Domain-Driven Design. It is a small object (in terms of memory), which consists of one or more attributes, and which represents a conceptual whole. Value Object is usually a part of Entity.
Use Python's vars() to Print an Object's Attributes The dir() function, as shown above, prints all of the attributes of a Python object. Let's say you only wanted to print the object's instance attributes as well as their values, we can use the vars() function.
typing
module and NamedTuple
In version 3.5, the typing
module has been added, in it, you will find a class that perfectly fits your needs.
NamedTuple
It works just as you'd expect:
Simple type definition:
from typing import NamedTuple
class DownloadableFile(NamedTuple):
file_path: str
download_url: str
Recognized in PyCharm:
Note:
As of today, the API is still in a provisional stage. It means it isn't guaranteed to be backwards compatible when new version is released. Changes to the interface though are not expected. My personal take on it is: given the simplicity of the design, if change there is, I am sure it will be an easy refactor ;)
Your requirements, although carefully expressed, are not quite clear to me, partly because I don't use the PyCharm GUI. But here is an attempt:
class ValueObject(object):
__slots__ = ()
def __init__(self, *vals):
if len(vals) != len(self.__slots__):
raise TypeError, "%s.__init__ accepts %d arguments, got %d" \
% (type(self).__name__, len(self.__slots__), len(vals))
for slot, val in zip(self.__slots__, vals):
super(ValueObject, self).__setattr__(slot, val)
def __repr__(self):
return ('<%s[0x%x] %s>'
% (type(self).__name__, id(self),
' '.join('%s=%r' % (slot, getattr(self, slot))
for slot in self.__slots__)))
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 __setattr__(self, attr, val):
if attr in self.__slots__:
raise AttributeError, "%s slot '%s' is read-only" % (type(self).__name__, attr)
super(ValueObject, self).__setattr__(attr, val)
Usage is like this:
class X(ValueObject):
__slots__ = 'a', 'b'
This gets you a concrete value class with two read-only slots and an autogenerated constructor, __eq__
, and __hash__
. For example:
>>> x = X(1.0, 2.0, 3.0)
Traceback (most recent call last):
File "<input>", line 1, in <module>
File "<input>", line 5, in __init__
TypeError: X.__init__ accepts 2 arguments, got 3
>>> x = X(1.0, 2.0)
>>> x
<X[0x4440a50] a=1.0 b=2.0>
>>> x.a
1.0
>>> x.b
2.0
>>> x.a = 10
Traceback (most recent call last):
File "<input>", line 1, in <module>
File "<input>", line 32, in __setattr__
AttributeError: X slot 'a' is read-only
>>> x.c = 10
Traceback (most recent call last):
File "<input>", line 1, in <module>
File "<input>", line 33, in __setattr__
AttributeError: 'X' object has no attribute 'c'
>>> dir(x)
['__class__', '__delattr__', '__dict__', '__doc__', '__eq__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__slots__', '__str__', '__subclasshook__', '__weakref__', '_vals', 'a', 'b']
>>> x == X(1.0, 2.0)
True
>>> x == X(1.0, 3.0)
False
>>> hash(x)
3713081631934410656
>>> hash(X(1.0, 2.0))
3713081631934410656
>>> hash(X(1.0, 3.0))
3713081631933328131
If you want, you can define your own __init__
with the docstring that (presumably) provides your IDE with type annotation hints.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With