10.4 Properties for Data Access

Let’s develop a Time class that stores the time in 24-hour clock format with hours in the range 0–23, and minutes and seconds each in the range 0–59. For this class, we’ll provide properties, which look like data attributes to client-code programmers, but control the manner in which they get and modify an object’s data. This assumes that other programmers follow Python conventions to correctly use objects of your class.

10.4.1 Test-Driving Class Time

Before we look at class Time’s definition, let’s demonstrate its capabilities. First, ensure that you’re in the ch10 folder, then import class Time from timewithproperties.py:

In [1]: from timewithproperties import Time

Creating a Time Object

Next, let’s create a Time object. Class Time’s __init__ method has hour, minute and second parameters, each with a default argument value of 0. Here, we specify the hour and minutesecond defaults to 0:

In [2]: wake_up = Time(hour=6, minute=30)

Displaying a Time Object

Class Time defines two methods that produce string representations of Time object. When you evaluate a variable in IPython as in snippet [3], IPython calls the object’s __repr__ special method to produce a string representation of the object. Our __repr__ implementation creates a string in the following format:

In [3]: wake_up
Out[3]: Time(hour=6, minute=30, second=0)

We’ll also provide the __str__ special method, which is called when an object is converted to a string, such as when you output the object with print.1 Our __str__ implementation creates a string in 12-hour clock format:

In [4]: print(wake_up)
6:30:00 AM

Getting an Attribute Via a Property

Class time provides hour, minute and second properties, which provide the convenience of data attributes for getting and modifying an object’s data. However, as you’ll see, properties are implemented as methods, so they may contain additional logic, such as specifying the format in which to return a data attribute’s value or validating a new value before using it to modify a data attribute. Here, we get the wake_up object’s hour value:

In [5]: wake_up.hour
Out[5]: 6

Though this snippet appears to simply get an hour data attribute’s value, it’s actually a call to an hour method that returns the value of a data attribute (which we named _hour, as you’ll see in the next section).

Setting the Time

You can set a new time with the Time object’s set_time method. Like method __init__, method set_time provides hour, minute and second parameters, each with a default of 0:

In [6]: wake_up.set_time(hour=7, minute=45)

In [7]: wake_up
Out[7]: Time(hour=7, minute=45, second=0)

Setting an Attribute via a Property

Class Time also supports setting the hour, minute and second values individually via its properties. Let’s change the hour value to 6:

In [8]: wake_up.hour = 6

In [9]: wake_up
Out[9]: Time(hour=6, minute=45, second=0)

Though snippet [8] appears to simply assign a value to a data attribute, it’s actually a call to an hour method that takes 6 as an argument. The method validates the value, then assigns it to a corresponding data attribute (which we named _hour, as you’ll see in the next section).

Attempting to Set an Invalid Value

To prove that class Time’s properties validate the values you assign to them, let’s try to assign an invalid value to the hour property, which results in a ValueError:

In [10]: wake_up.hour = 100
-------------------------------------------------------------------------
ValueError                              Traceback (most recent call last)
<ipython-input-10-1fce0716ef14> in <module>()
----> 1 wake_up.hour = 100

~/Documents/examples/ch10/timewithproperties.py in hour(self, hour)
     20        """Set the hour."""
     21        if not (0 <= hour < 24):
---> 22            raise ValueError(f'Hour ({hour}) must be 0-23')
     23
     24        self._hour = hour

ValueError: Hour (100) must be 0-23

tick mark Self Check

  1. (Fill-In) The print function implicitly invokes special method _________.
    Answer: __str__.

  2. (Fill-In) IPython calls an object’s special method _________ to produce a string representation of the object
    Answer: __repr__.

  3. (True/False) Properties are used like methods.
    Answer: False. Properties are used like data attributes, but (as we’ll see in the next section) are implemented as methods.

10.4.2 Class Time Definition

Now that we’ve seen class Time in action, let’s look at its definition.

Class Time: __init__ Method with Default Parameter Values

Class Time’s __init__ method specifies hour, minute and second parameters, each with a default argument of 0. Similar to class Account’s __init__ method, recall that the self parameter is a reference to the Time object being initialized. The statements containing self.hour, self.minute and self.second appear to create hour, minute and second attributes for the new Time object (self). However, these statements actually call methods that implement the class’s hour, minute and second properties (lines 13–50). Those methods then create attributes named _hour, _minute and _second that are meant for use only inside the class:

1 # timewithproperties.py
2 """Class Time with read-write properties."""
3
4 class Time:
5     """Class Time with read-write properties."""
6
7     def __init__(self, hour=0, minute=0, second=0):
8         """Initialize each attribute."""
9         self.hour = hour # 0-23
10        self.minute = minute # 0-59
11        self.second = second # 0-59
12

Class Time: hour Read-Write Property

Lines 13–24 define a publicly accessible read-write property named hour that manipulates a data attribute named _hour. The single-leading-underscore (_) naming convention indicates that client code should not access _hour directly. As you saw in the previous section’s snippets [5] and [8], properties look like data attributes to programmers working with Time objects. However, notice that properties are implemented as methods. Each property defines a getter method which gets (that is, returns) a data attribute’s value and can optionally define a setter method which sets a data attribute’s value:

13 @property
14 def hour(self):
15     """Return the hour."""
16     return self._hour
17
18 @hour.setter
19 def hour(self, hour):
20     """Set the hour."""
21     if not (0 <= hour < 24):
22         raise ValueError(f'Hour ({hour}) must be 0-23')
23
24     self._hour = hour
25

The @property decorator precedes the property’s getter method, which receives only a self parameter. Behind the scenes, a decorator adds code to the decorated function—in this case to make the hour function work with attribute syntax. The getter method’s name is the property name. This getter method returns the _hour data attribute’s value. The following client-code expression invokes the getter method:

wake_up.hour

You also can use the getter method inside the class, as you’ll see shortly.

A decorator of the form @property_name.setter (in this case, @hour.setter) precedes the property’s setter method. The method receives two parameters—self and a parameter (hour) representing the value being assigned to the property. If the hour parameter’s value is valid, this method assigns it to the self object’s _hour attribute; otherwise, the method raises a ValueError. The following client-code expression invokes the setter by assigning a value to the property:

wake_up.hour = 8

We also invoked this setter inside the class at line 9 of __init__:

self.hour = hour

Using the setter enabled us to validate __init__’s hour argument before creating and initializing the object’s _hour attribute, which occurs the first time the hour property’s setter executes as a result of line 9. A read-write property has both a getter and a setter. A read-only property has only a getter.

Class Time: minute and second Read-Write Properties

Lines 26–37 and 39–50 define read-write minute and second properties. Each property’s setter ensures that its second argument is in the range 059 (the valid range of values for minutes and seconds):

26 @property
27 def minute(self):
28     """Return the minute."""
29     return self._minute
30
31 @minute.setter
32 def minute(self, minute):
33     """Set the minute."""
34     if not (0 <= minute < 60):
35        raise ValueError(f'Minute ({minute}) must be 0-59')
36
37     self._minute = minute
38
39 @property
40 def second(self):
41     """Return the second."""
42     return self._second
43
44 @second.setter
45 def second(self, second):
46     """Set the second."""
47     if not (0 <= second < 60):
48         raise ValueError(f'Second ({second}) must be 0-59')
49
50     self._second = second
51

Class Time: Method set_time

We provide method set_time as a convenient way to change all three attributes with a single method call. Lines 54–56 invoke the setters for the hour, minute and second properties:

52 def set_time(self, hour=0, minute=0, second=0):
53     """Set values of hour, minute, and second."""
54     self.hour = hour
55     self.minute = minute
56     self.second = second
57

Class Time: Special Method __repr__

When you pass an object to built-in function repr—which happens implicitly when you evaluate a variable in an IPython session—the corresponding class’s __repr__ special method is called to get a string representation of the object:

58 def __repr__(self):
59     """Return Time string for repr()."""
60     return (f'Time(hour={self.hour}, minute={self.minute}, ' +
61             f'second={self.second})')
62

The Python documentation indicates that __repr__ returns the “official” string representation of the object. Typically this string looks like a constructor expression that creates and initializes the object,2 as in:

'Time(hour=6, minute=30, second=0)'

This is similar to the constructor expression in the previous section’s snippet [2]. Python has a built-in function eval that could receive the preceding string as an argument and use it to create and initialize a Time object containing values specified in the string.

Class Time: Special Method __str__

For our class Time we also define the __str__ special method. This method is called implicitly when you convert an object to a string with the built-in function str, such as when you print an object or call str explicitly. Our implementation of __str__ creates a string in 12-hour clock format, such as '7:59:59 AM' or '12:30:45 PM':

63 def __str__(self):
64     """Print Time in 12-hour clock format."""
65     return (('12' if self.hour in (0, 12) else str(self.hour % 12)) +
66             f':{self.minute:0>2}:{self.second:0>2}' +
67             (' AM' if self.hour < 12 else ' PM'))

tick mark Self Check

  1. (Fill-In) The print function implicitly invokes special method _________.
    Answer: __str__.

  2. (Fill-In) A(n) _________ property has both a getter and setter. If only a getter is provided, the property is a(n) _________ property, meaning that you only can get the property’s value.
    Answer: read-write, read-only.

  3. (IPython Session) Add to class Time a read-write property time in which the getter returns a tuple containing the values of the hour, minute and second properties, and the setter receives a tuple containing hour, minute and second values and uses them to set the time. Create a Time object and test the new property.
    Answer: The new read-write property definition is shown below:

    @property
    def time(self):
        """Return hour, minute and second as a tuple."""
        return (self.hour, self.minute, self.second)
    
    @time.setter
    def time(self, time_tuple):
        """Set time from a tuple containing hour, minute and second."""
        self.set_time(time_tuple[0], time_tuple[1], time_tuple[2])
    
    In [1]: from timewithproperties import Time
    
    In [2]: t = Time()
    
    In [3]: t
    Out[3]: Time(hour=0, minute=0, second=0)
    
    In [4]: t.time = (12, 30, 45)
    
    In [5]: t
    Out[5]: Time(hour=12, minute=30, second=45)
    
    In [6]: t.time
    Out[6]: (12, 30, 45)
    

    Note that the self.set_time call in the time property’s setter method may be expressed more concisely as

    self.set_time(*time_tuple)
    

    The expression *time_tuple uses the unary * operator to unpack the time_tuple’s values, then passes them as individual arguments. In the preceding IPython session, the setter would receive the tuple (12, 30, 45), then unpack the tuple and call self.set_time as follows:

    self.set_time(12, 30, 45)
    

10.4.3 Class Time Definition Design Notes

Let’s consider some class-design issues in the context of our Time class.

Interface of a Class

Class Time’s properties and methods define the class’s public interface—that is, the set of properties and methods programmers should use to interact with objects of the class.

Attributes Are Always Accessible

Though we provided a well-defined interface, Python does not prevent you from directly manipulating the data attributes _hour, _minute and _second, as in:

In [1]: from timewithproperties import Time

In [2]: wake_up = Time(hour=7, minute=45, second=30)

In [3]: wake_up._hour
Out[3]: 7

In [4]: wake_up._hour = 100

In [5]: wake_up
Out[5]: Time(hour=100, minute=45, second=30)

After snippet [4], the wake_up object contains invalid data. Unlike many other object-oriented programming languages, such as C++, Java and C#, data attributes in Python cannot be hidden from client code. The Python tutorial says, “nothing in Python makes it possible to enforce data hiding—it is all based upon convention.”3

Internal Data Representation

We chose to represent the time as three integer values for hours, minutes and seconds. It would be perfectly reasonable to represent the time internally as the number of seconds since midnight. Though we’d have to reimplement the properties hour, minute and second, programmers could use the same interface and get the same results without being aware of these changes. An exercise at the end of this chapter asks you to make this change and show that client code using Time objects does not need to change.

Evolving a Class’s Implementation Details

When you design a class, carefully consider the class’s interface before making that class available to other programmers. Ideally, you’ll design the interface such that existing code will not break if you update the class’s implementation details—that is, the internal data representation or how its method bodies are implemented.

If Python programmers follow convention and do not access attributes that begin with leading underscores, then class designers can evolve class implementation details without breaking client code.

Properties

It may seem that providing properties with both setters and getters has no benefit over accessing the data attributes directly, but there are subtle differences. A getter seems to allow clients to read the data at will, but the getter can control the formatting of the data. A setter can scrutinize attempts to modify the value of a data attribute to prevent the data from being set to an invalid value.

Utility Methods

Not all methods need to serve as part of a class’s interface. Some serve as utility methods used only inside the class and are not intended to be part of the class’s public interface used by client code. Such methods should be named with a single leading underscore. In other object-oriented languages like C++, Java and C#, such methods typically are implemented as private methods.

Module datetime

In professional Python development, rather than building your own classes to represent times and dates, you’ll typically use the Python Standard Library’s datetime module capabilities. You can learn more about the datetime module at:

https://docs.python.org/3/library/datetime.html

An exercise at the end of the chapter has you manipulate dates and times with this module.

tick mark Self Check

  1. (Fill-In) A class’s _________ is the set of public properties and methods programmers should use to interact with objects of the class.
    Answer: interface.

  2. (Fill-In) A class’s _________ methods are used only inside the class and are not intended to be used by client code.
    Answer: utility.