This document is intended as a quick introduction to creating and using ezvalue value objects. After going through this tutorial you will be able to effectively use ezvalue value objects and understand code that uses them.

Creating value objects

New value objects are created by subclassing ezvalue.Value:

import ezvalue
class Point(ezvalue.Value):
    """Defines a cartesian point in 2D space."""

    x = """The x-coordinate in meters."""
    y = """The y-coordinate in meters."""

The class docstring should be used to document the value object and strings should be assigned to the class attributes to document them. After defining the value object it can be instantiated and used as follows:

>>> my_point = Point(x=1, y=13)
>>> my_point.x
>>> my_point.y

Note that value objects are immutable:

>>> my_point.x = 2
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "ezvalue/__init__.py", line 270, in __setattr__
    raise AttributeError()

It is however very simple to create a modified copy of the value object:

>>> different_point = Point(my_point, x=2)
>>> different_point.x
>>> different_point.y

As you can see a value object can take another object as it’s first parameter to be used as a source object, any keyword arguments provided will overwrite the values of the source object.

Equality of value objects

Equality of value objects is based on their value:

>>> my_point == Point(x=1, y=13)
>>> my_point == Point(x=2, y=13)

Value objects are never equal to objects of a different type with exception of their mutable companion classes which will be introduced below:

>>> from collections import namedtuple
>>> NamedTuple = namedtuple(my_point == NamedTuple(x=1, y=13)

If you must compare to another object with the same attributes you can convert it to a value object first:

>>> my_point == Point(NamedTuple(x=1, y=13))

Mutable value objects

Value objects should normally be immutable for various good reasons [1] however there are times that mutable value objects are useful. One such situation can occur when working with code that is outside your control. Suppose you need to use a function that takes a point and inverts it’s x-coordinate:

def invert_x(point):
    point.x = -point.x

If you would supply this function with an instance of the Point class we defined above it would raise an AttributeError:

>>> invert_x(my_point)
Traceback (most recent call last):
  File "<input>", line 1, in <module>
  File "<input>", line 2, in move_left
  File "ezvalue/__init__.py", line 270, in __setattr__
    raise AttributeError()

However you can create a mutable copy of the object with the to_mutable method and pass that to the function:

>>> my_mutable_point = my_point.mutable()
>>> invert_x(my_mutable_point)
>>> my_mutable_point

The mutable point can easily be converted back to an immutable object using the to_immutable method:

>>> my_mutable_point.immutable()

If you are able to modify the function it would however be better to modify it to return a new point:

def inverted_x(point):
    return Point(point, x=-point.x)

Note that the function was also renamed to describe the new functionality. It can then be used as follows:

>>> my_inverted_point = inverted_x(my_point)
>>> my_inverted_point


[1]See this post by Martin fowler for a very good discussion of value objects and why they should be immutable.