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.
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
Time
ObjectNext, 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 minute
—second
defaults to 0
:
In [2]: wake_up = Time(hour=6, minute=30)
Time
ObjectClass 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
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).
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)
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).
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
(Fill-In) The print
function implicitly invokes special method _________.
Answer: __str__
.
(Fill-In) IPython calls an object’s special method _________ to produce a string representation of the object
Answer: __repr__
.
(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.
Time
DefinitionNow that we’ve seen class Time
in action, let’s look at its definition.
__init__
Method with Default Parameter ValuesClass 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
hour
Read-Write PropertyLines 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.
minute
and second
Read-Write PropertiesLines 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 0
–59
(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
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
__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.
__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'))
(Fill-In) The print
function implicitly invokes special method _________.
Answer: __str__
.
(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.
(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)
Time
Definition Design NotesLet’s consider some class-design issues in the context of our Time
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.
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
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.
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.
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.
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.
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.
(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.
(Fill-In) A class’s _________ methods are used only inside the class and are not intended to be used by client code.
Answer: utility.