Programming for fun and profit

A blog about software engineering, programming languages and technical tinkering

Thu 05 April 2018

Properties as Pythonic setters

Posted by Simon Larsén in Programming   

This is the second part in a two part series on Python properties. In Part 1 (which readers will be assumed to have at least skimmed through), we saw how a property can be used to create a read-only attribute that can be accessed like any data attribute (i.e with obj.attr), but raises an AttributeError when written to. Now, we will look at how to expand the property to also allow us to write to count like it's a normal data attribute (i.e. with t.count = 42), while also doing input validation.

A property as a Pythonic setter

Using the Ticker class version from the final listing in Part 1, we are unable to set the count attribute to any value.

>>> t = Ticker(24)  # valid range for count is thus [0, 23]
>>> t.count = 11    # this is well within that range
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: can't set attribute
can't set attribute

>>> for _ in range(11): # doing it the hard way ...
...     t.tick()
>>> t.count
11

This presents something of a usability issue, as the only way to set the Ticker's internal count to a specific value (using the public API) is by calling tick() an appropriate amount of times. If we were to use the Ticker as, say, a clock, we'd definitely want to be able to set count to a value within the range [0, _end) by simple assignment. Fortunately, there is a simple way to expand a property with a setter method using the @<name>.setter decorator, where <name> is replaced with the name of the property. For the count property of the Ticker class, it looks like this:

@count.setter
def count(self, val):
    """Set the internal count to val."""
    if val < 0 or val >= self._end:
        raise ValueError(f"{val} is out of range for attribute count.")
    self._count = val

Note: A string literal preceeded with an f is an f-string. This is a Python 3.6 feature. For backwards compatability, you could change to using string.format like this: "{} is out of range for attribute count.".format(val)

The code should be fairly self-explanatory. The setter takes a value val as an argument. If val is outside of the allowed range [0, _end), a ValueError is raised. Otherwise, _count is set to val. The error message could be more informative, but I did not want to obscure the important parts with a lot of text. We have thus defeated the aforementioned usability issue, and usage now looks like this:

>>> t = Ticker(24)
>>> t.count
0
>>> t.tick()
>>> t.count
1
>>> t.count = 11
>>> t.count
11
>>> t.tick()
>>> t.count
12
>>> t.count = 24
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 23, in count
ValueError: 24 is out of range for attribute count.
24 is out of range for attribute count.

Seems to work just the way we want it to!

Ticker full listing (with getter/setter property)

Here is the full listing of the Ticker class.

class Ticker:
    """A Ticker ticks from 0 to an upper limit, and then starts over."""

    def __init__(self, end: int):
        """Create a Ticker that starts over at end"""
        if end <= 0:
            raise ValueError("end must be greater than 0!")
        self._end = end
        self._count = 0

    def tick(self):
        """Increment the internal count by 1."""
        self._count = (self._count + 1) % self._end

    @property
    def count(self):
        """Return the current count."""
        return self._count

    @count.setter
    def count(self, val):
        """Set the internal count to val."""
        if val < 0 or val >= self._end:
            raise ValueError(f"{val} is out of range for attribute count.")
        self._count = val