Chapter 10
Chapter 10
Chapter 10
Programming 10
Objectives
In this chapter, you’ll:
Create custom classes and
objects of those classes.
Understand the benefits of
crafting valuable classes.
Control access to attributes.
Appreciate the value of object
orientation.
Use Python special methods
__repr__, __str__ and
__format__ to get an
object’s string representations.
Use Python special methods
to overload (redefine)
operators to use them with
objects of new classes.
Inherit methods, properties
and attributes from existing
classes into new classes, then
customize those classes.
Understand the inheritance
notions of base classes
(superclasses) and derived
classes (subclasses).
Understand duck typing and
polymorphism that enable
“programming in the general.”
Understand class object
from which all classes inherit
fundamental capabilities.
Compare composition and
inheritance.
Build test cases into
docstrings and run these tests
with doctest,
Understand namespaces and
how they affect scope.
404 Object-Oriented Programming
10.1 Introduction
Section 1.5 introduced the basic terminology and concepts of object-oriented program-
ming. Everything in Python is an object, so you’ve been using objects constantly through-
out this book. Just as houses are built from blueprints, objects are built from classes—one
of the core technologies of object-oriented programming. Building a new object from even
a large class is simple—you typically write one statement.
Crafting Valuable Classes
You’ve already used lots of classes created by other people. In this chapter you’ll learn how
to create your own custom classes. You’ll focus on “crafting valuable classes” that help you
meet the requirements of the applications you’ll build. You’ll use object-oriented pro-
gramming with its core technologies of classes, objects, inheritance and polymorphism.
Software applications are becoming larger and more richly functional. Object-oriented
programming makes it easier for you to design, implement, test, debug and update such
edge-of-the-practice applications. Read Sections 10.1 through 10.9 for a code-intensive
introduction to these technologies. Most people can skip Sections 10.10 through 10.15,
which provide additional perspectives on these technologies and present additional related
features.
10.1 Introduction 405
explain how to implement polymorphism through inheritance and a Python feature called
duck typing. We’ll explain both and show examples of each.
An Entertaining Case Study: Card-Shuffling-and-Dealing Simulation
You’ve already used a random-numbers-based die-rolling simulation and used those tech-
niques to implement the popular dice game craps. Here, we present a card-shuffling-and-
dealing simulation, which you can use to implement your favorite card games. You’ll use
Matplotlib with attractive public-domain card images to display the full deck of cards both
before and after the deck is shuffled. In the exercises, you can implement the popular card
games blackjack and solitaire, and evaluate a five-card poker hand.
Data Classes
Python 3.7’s new data classes help you build classes faster by using a more concise notation
and by autogenerating portions of the classes. The Python community’s early reaction to
data classes has been positive. As with any major new feature, it may take time before it’s
widely used. We present class development with both the older and newer technologies.
Other Concepts Introduced in This Chapter
Other concepts you’ll learn include:
• How to specify that certain identifiers should be used only inside a class and not
be accessible to clients of the class.
• Special methods for creating string representations of your classes’ objects and
specifying how objects of your classes work with Python’s built-in operators (a
process called operator overloading).
• An introduction to the Python exception class hierarchy and creating custom
exception classes.
• Testing code with the Python Standard Library’s doctest module.
• How Python uses namespaces to determine the scopes of identifiers.
Class Account maintains and manipulates the account balance as a Decimal, so we also
import class Decimal:
In [2]: from decimal import Decimal
This is known as a constructor expression because it builds and initializes an object of the
class, similar to the way a house is constructed from a blueprint then painted with the
buyer’s preferred colors. Constructor expressions create new objects and initialize their
data using argument(s) specified in parentheses. The parentheses following the class name
are required, even if there are no arguments.
Let’s use a constructor expression to create an Account object and initialize it with an
account holder’s name (a string) and balance (a Decimal):
In [3]: account1 = Account('John Green', Decimal('50.00'))
In [5]: account1.balance
Out[5]: Decimal('50.00')
In [7]: account1.balance
Out[7]: Decimal('75.53')
Self Check
1 (Fill-In) Each new class you create becomes a new data type that can be used to create
objects. This is one reason why Python is said to be a(n) language.
Answer: extensible.
2 (Fill-In) A(n) expression creates and initializes an object of a class.
Answer: constructor.
Each class typically provides a descriptive docstring (line 6). When provided, it must
appear in the line or lines immediately following the class header. To view any class’s doc-
string in IPython, type the class name and a question mark, then press Enter:
In [9]: Account?
Init signature: Account(name, balance)
Docstring: Account class for maintaining a bank account balance.
Init docstring: Initialize an Account object.
File: ~/Documents/examples/ch10/account.py
Type: type
The identifier Account is both the class name and the name used in a constructor expres-
sion to create an Account object and invoke the class’s __init__ method. For this reason,
IPython’s help mechanism shows both the class’s docstring ("Docstring:") and the
__init__ method’s docstring ("Init docstring:").
creates a new object, then initializes its data by calling the class’s __init__ method. Each
new class you create can provide an __init__ method that specifies how to initialize an
object’s data attributes. Returning a value other than None from __init__ results in a
TypeError. Recall that None is returned by any function or method that does not contain
a return statement. Class Account’s __init__ method (lines 8–16) initializes an Account
object’s name and balance attributes if the balance is valid:
10.2 Custom Class Account 409
Python classes may define many special methods, like __init__, each identified by
leading and trailing double-underscores (__) in the method name. Python class object,
which we’ll discuss later in this chapter, defines the special methods that are available for
all Python objects.
Method deposit
The Account class’s deposit method adds a positive amount to the account’s balance
attribute. If the amount argument is less than 0.00, the method raises a ValueError, indi-
cating that only positive deposit amounts are allowed. If the amount is valid, line 25 adds
it to the object’s balance attribute.
18 def deposit(self, amount):
19 """Deposit money to the account."""
20
21 # if amount is less than 0.00, raise an exception
22 if amount < Decimal('0.00'):
23 raise ValueError('amount must be positive.')
24
25 self.balance += amount
Self Check
1 (Fill-In) A class’s method is called by a constructor expression to initialize a
new object of the class.
Answer: __init__.
2 (True/False) A class’s __init__ method returns an object of the class.
Answer: False. A class’s __init__ method initializes an object of the class and implicitly
returns None.
3 (IPython Session) Add a withdraw method to class Account. If the withdrawal amount
is greater than the balance, raise a ValueError, indicating that the withdrawal amount
must be less than or equal to the balance. If the withdrawal amount is less than 0.00, raise
a ValueError indicating that the withdrawal amount must be positive. If the withdrawal
amount is valid, subtract it from the balance attribute. Create an Account object, then test
method withdraw first with a valid withdrawal amount, then with a withdrawal amount
greater than the balance and finally with a negative withdrawal amount.
Answer: The new method in class Account is:
def withdraw(self, amount):
"""Withdraw money from the account."""
self.balance -= amount
In [4]: account1.withdraw(Decimal('20.00'))
In [5]: account1.balance
Out[5]: Decimal('30.00')
In [6]: account1.withdraw(Decimal('100.00'))
-------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-6-61bb6aa89aa4> in <module>()
----> 1 account1.withdraw(Decimal('100.00'))
10.3 Controlling Access to Attributes 411
~/Documents/examples/ch10/snippets_py/account.py in withdraw(self,
amount)
30 # if amount is greater than balance, raise an exception
31 if amount > self.balance:
---> 32 raise ValueError('amount must be <= to balance.')
33 elif amount < Decimal('0.00'):
34 raise ValueError('amount must be positive.')
In [7]: account1.withdraw(Decimal('-10.00'))
-------------------------------------------------------------------------
ValueError Traceback (most recent call last)
<ipython-input-7-ab50927d9727> in <module>()
----> 1 account1.withdraw(Decimal('-10.00'))
~/Documents/examples/ch10/snippets_py/account.py in withdraw(self,
amount)
32 raise ValueError('amount must be <= to balance.')
33 elif amount < Decimal('0.00'):
---> 34 raise ValueError('amount must be positive.')
35
36 self.balance -= amount
In [4]: account1.balance
Out[4]: Decimal('50.00')
Initially, account1 contains a valid balance. Now, let’s set the balance attribute to an
invalid negative value, then display the balance:
In [5]: account1.balance = Decimal('-1000.00')
In [6]: account1.balance
Out[6]: Decimal('-1000.00')
Snippet [6]’s output shows that account1’s balance is now negative. As you can see,
unlike methods, data attributes cannot validate the values you assign to them.
412 Object-Oriented Programming
Encapsulation
A class’s client code is any code that uses objects of the class. Most object-oriented pro-
gramming languages enable you to encapsulate (or hide) an object’s data from the client
code. Such data in these languages is said to be private data.
Leading Underscore (_) Naming Convention
Python does not have private data. Instead, you use naming conventions to design classes
that encourage correct use. By convention, Python programmers know that any attribute
name beginning with an underscore (_) is for a class’s internal use only. Client code should
use the class’s methods and—as you’ll see in the next section—the class’s properties to
interact with each object’s internal-use data attributes. Attributes whose identifiers do not
begin with an underscore (_) are considered publicly accessible for use in client code. In the
next section, we’ll define a Time class and use these naming conventions. However, even
when we use these conventions, attributes are always accessible.
Self Check
1 (True/False) Like most object-oriented programming languages, Python provides
capabilities for encapsulating an object’s data attributes so client code cannot access the
data directly.
Answer: False. In Python, all data attributes are accessible. You use attribute naming con-
ventions to indicate that attributes should not be accessed directly from client code.
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
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)
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:
1. If a class does not provide __str__ and an object of the class is converted to a string, the class’s
__repr__ method is called instead.
414 Object-Oriented Programming
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 repre-
sentation 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.
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 fol-
lowing 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) pre-
cedes 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 param-
eter’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
Using the setter enabled us to validate __init__’s hour argument before creating and ini-
tializing 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 0–59 (the valid range of values for
minutes and seconds):
416 Object-Oriented Programming
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
2. https://docs.python.org/3/reference/datamodel.html.
10.4 Properties for Data Access 417
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'))
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 pro-
vided, 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 [2]: t = Time()
In [3]: t
Out[3]: Time(hour=0, minute=0, second=0)
In [5]: t
Out[5]: Time(hour=12, minute=30, second=45)
In [6]: t.time
Out[6]: (12, 30, 45)
418 Object-Oriented Programming
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)
In [3]: wake_up._hour
Out[3]: 7
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-ori-
ented 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 pos-
sible 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 sec-
ond, 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
3. https://docs.python.org/3/tutorial/classes.html#random-remarks.
10.5 Simulating “Private” Attributes 419
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 capa-
bilities. 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.
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.
that would set the hour to an invalid value. Rather than _hour, we can name the attribute
__hour with two leading underscores. This convention indicates that __hour is “private” and
should not be accessible to the class’s clients. To help prevent clients from accessing “private”
attributes, Python renames them by preceding the attribute name with _ClassName, as in
_Time__hour. This is called name mangling. If you try assign to __hour, as in
wake_up.__hour = 100
Python raises an AttributeError, indicating that the class does not have an __hour attri-
bute. We’ll show this momentarily.
IPython Auto-Completion Shows Only “Public” Attributes
In addition, IPython does not show attributes with one or two leading underscores when
you try to auto-complete an expression like
wake_up.
by pressing Tab. Only attributes that are part of the wake_up object’s “public” interface are
displayed in the IPython auto-completion list.
Demonstrating “Private” Attributes
To demonstrate name mangling, consider class PrivateClass with one “public” data
attribute public_data and one “private” data attribute __private_data:
1 # private.py
2 """Class with public and private attributes."""
3
4 class PrivateClass:
5 """Class with public and private attributes."""
6
7 def __init__(self):
8 """Initialize the public and private attributes."""
9 self.public_data = "public" # public attribute
10 self.__private_data = "private" # private attribute
Snippet [3] shows that we can access the public_data attribute directly:
In [3]: my_object.public_data
Out[3]: 'public'
This occurs because python changed the attribute’s name. Unfortunately, as you’ll see in
one of this section’s Self Check exercises, __private_data is still indirectly accessible.
Self Check
1 (Fill-In) Python mangles attribute names that begin with underscore(s).
Answer: two.
2 (True/False) An attribute that begins with a single underscore is a private attribute.
Answer: False. An attribute that begins with a single underscore simply conveys the con-
vention that a client of the class should not access the attribute directly, but it does allow
access. Again, “nothing in Python makes it possible to enforce data hiding—it is all
based upon convention.”4
3 (IPython Session) Even with double-underscore (__) naming, we can still access and
modify __private_data, because we know that Python renames attributes simply by pre-
fixing their names with '_ClassName'. Demonstrate this for class PrivateData’s data
attribute __private_data.
Answer:
In [5]: my_object._PrivateClass__private_data
Out[5]: 'private'
In [7]: my_object._PrivateClass__private_data
Out[7]: 'modified'
4. https://docs.python.org/3/tutorial/classes.html#random-remarks.
422 Object-Oriented Programming
DeckOfCards method __init__ creates the 52 Card objects in order by suit and by face
within each suit. You can see this by printing the deck_of_cards object, which calls the
DeckOfCards class’s __str__ method to get the deck’s string representation. Read each
row left-to-right to confirm that all the cards are displayed in order from each suit (Hearts,
Diamonds, Clubs and Spades):
In [3]: print(deck_of_cards)
Ace of Hearts 2 of Hearts 3 of Hearts 4 of Hearts
5 of Hearts 6 of Hearts 7 of Hearts 8 of Hearts
9 of Hearts 10 of Hearts Jack of Hearts Queen of Hearts
King of Hearts Ace of Diamonds 2 of Diamonds 3 of Diamonds
4 of Diamonds 5 of Diamonds 6 of Diamonds 7 of Diamonds
8 of Diamonds 9 of Diamonds 10 of Diamonds Jack of Diamonds
Queen of Diamonds King of Diamonds Ace of Clubs 2 of Clubs
3 of Clubs 4 of Clubs 5 of Clubs 6 of Clubs
7 of Clubs 8 of Clubs 9 of Clubs 10 of Clubs
Jack of Clubs Queen of Clubs King of Clubs Ace of Spades
2 of Spades 3 of Spades 4 of Spades 5 of Spades
6 of Spades 7 of Spades 8 of Spades 9 of Spades
10 of Spades Jack of Spades Queen of Spades King of Spades
Next, let’s shuffle the deck and print the deck_of_cards object again. We did not specify
a seed for reproducibility, so each time you shuffle, you’ll get different results:
In [4]: deck_of_cards.shuffle()
In [5]: print(deck_of_cards)
King of Hearts Queen of Clubs Queen of Diamonds 10 of Clubs
5 of Hearts 7 of Hearts 4 of Hearts 2 of Hearts
5 of Clubs 8 of Diamonds 3 of Hearts 10 of Hearts
8 of Spades 5 of Spades Queen of Spades Ace of Clubs
8 of Clubs 7 of Spades Jack of Diamonds 10 of Spades
4 of Diamonds 8 of Hearts 6 of Spades King of Spades
9 of Hearts 4 of Spades 6 of Clubs King of Clubs
3 of Spades 9 of Diamonds 3 of Clubs Ace of Spades
Ace of Hearts 3 of Diamonds 2 of Diamonds 6 of Hearts
King of Diamonds Jack of Spades Jack of Clubs 2 of Spades
5 of Diamonds 4 of Clubs Queen of Hearts 9 of Clubs
10 of Diamonds 2 of Clubs Ace of Diamonds 7 of Diamonds
9 of Spades Jack of Hearts 6 of Diamonds 7 of Clubs
Dealing Cards
We can deal one Card at a time by calling method deal_card. IPython calls the returned
Card object’s __repr__ method to produce the string output shown in the Out[] prompt:
In [6]: deck_of_cards.deal_card()
Out[6]: Card(face='King', suit='Hearts')
In [8]: str(card)
Out[8]: 'Queen of Clubs'
10.6 Case Study: Card Shuffling and Dealing Simulation 423
Each Card has a corresponding image file name, which you can get via the image_name
read-only property. We’ll use this soon when we display the Cards as images:
In [9]: card.image_name
Out[9]: 'Queen_of_Clubs.png'
You define a class attribute by assigning a value to it inside the class’s definition, but
not inside any of the class’s methods or properties (in which case, they’d be local variables).
FACES and SUITS are constants that are not meant to be modified. Recall that the Style
Guide for Python Code recommends naming your constants with all capital letters.5
We’ll use elements of these lists to initialize each Card we create. However, we do not
need a separate copy of each list in every Card object. Class attributes can be accessed through
any object of the class, but are typically accessed through the class’s name (as in, Card.FACES
or Card.SUITS). Class attributes exist as soon as you import their class’s definition.
Card Method __init__
When you create a Card object, method __init__ defines the object’s _face and _suit
data attributes:
9 def __init__(self, face, suit):
10 """Initialize a Card with a face and suit."""
11 self._face = face
12 self._suit = suit
13
5. Recall that Python does not have true constants, so FACES and SUITS are still modifiable.
424 Object-Oriented Programming
Method __str__ returns a string of the format 'face of suit', such as 'Ace of Hearts':
33 def __str__(self):
34 """Return string representation for str()."""
35 return f'{self.face} of {self.suit}'
36
When the preceding section’s IPython session printed the entire deck, you saw that
the Cards were displayed in four left-aligned columns. As you’ll see in the __str__ method
of class DeckOfCards, we use f-strings to format the Cards in fields of 19 characters each.
Class Card’s special method __format__ is called when a Card object is formatted as a
string, such as in an f-string:
37 def __format__(self, format):
38 """Return formatted string representation for str()."""
39 return f'{str(self):{format}}'
10.6 Case Study: Card Shuffling and Dealing Simulation 425
This method’s second argument is the format string used to format the object. To use the
format parameter’s value as the format specifier, enclose the parameter name in braces to
the right of the colon. In this case, we’re formatting the Card object’s string representation
returned by str(self). We’ll discuss __format__ again when we present the __str__
method in class DeckOfCards.
• _current_card keeps track of which Card will be dealt next (0–51) and
• _deck (line 12) is a list of 52 Card objects.
Method __init__
DeckOfCards method __init__ initializes a _deck of Cards. The for statement fills the list
_deck by appending new Card objects, each initialized with two strings—one from the list
Card.FACES and one from Card.SUITS. The calculation count % 13 always results in a
value from 0 to 12 (the 13 indices of Card.FACES), and the calculation count // 13 always
results in a value from 0 to 3 (the four indices of Card.SUITS). When the _deck list is ini-
tialized, it contains the Cards with faces 'Ace' through 'King' in order for all the Hearts,
then the Diamonds, then the Clubs, then the Spades.
1 # deck.py
2 """Deck class represents a deck of Cards."""
3 import random
4 from card import Card
5
6 class DeckOfCards:
7 NUMBER_OF_CARDS = 52 # constant number of Cards
8
9 def __init__(self):
10 """Initialize the deck."""
11 self._current_card = 0
12 self._deck = []
13
14 for count in range(DeckOfCards.NUMBER_OF_CARDS):
15 self._deck.append(Card(Card.FACES[count % 13],
16 Card.SUITS[count // 13]))
17
Method shuffle
Method shuffle resets _current_card to 0, then shuffles the Cards in _deck using the
random module’s shuffle function:
18 def shuffle(self):
19 """Shuffle deck."""
20 self._current_card = 0
21 random.shuffle(self._deck)
22
426 Object-Oriented Programming
Method deal_card
Method deal_card deals one Card from _deck. Recall that _current_card indicates the
index (0–51) of the next Card to be dealt (that is, the Card at the top of the deck). Line 26
tries to get the _deck element at index _current_card. If successful, the method incre-
ments _current_card by 1, then returns the Card being dealt; otherwise, the method
returns None to indicate there are no more Cards to deal.
23 def deal_card(self):
24 """Return one Card."""
25 try:
26 card = self._deck[self._current_card]
27 self._current_card += 1
28 return card
29 except:
30 return None
31
Method __str__
Class DeckOfCards also defines special method __str__ to get a string representation of
the deck in four columns with each Card left aligned in a field of 19 characters. When line
37 formats a given Card, its __format__ special method is called with format specifier
'<19' as the method’s format argument. Method __format__ then uses '<19' to create
the Card’s formatted string representation.
32 def __str__(self):
33 """Return a string representation of the current _deck."""
34 s = ''
35
36 for index, card in enumerate(self._deck):
37 s += f'{self._deck[index]:<19}'
38 if (index + 1) % 4 == 0:
39 s += '\n'
40
41 return s
These are located in the ch10 examples folder’s card_images subfolder. First, let’s create a
DeckOfCards:
In [1]: from deck import DeckOfCards
6. https://creativecommons.org/publicdomain/zero/1.0/deed.en.
10.6 Case Study: Card Shuffling and Dealing Simulation 427
When you execute this statement in IPython, the Matplotlib window appears immediately
with 52 empty subplots.
Configure the Axes Objects and Display the Images
Next, we iterate through all the Axes objects in axes_list. Recall that ravel provides a
one-dimensional view of a multidimensional array. For each Axes object, we perform the
following tasks:
• We’re not plotting data, so we do not need axis lines and labels for each image.
The first two statements in the loop hide the x- and y-axes.
• The third statement deals a Card and gets its image_name.
• The fourth statement uses Path method joinpath to append the image_name to
the Path, then calls Path method resolve to determine the full path to the image
on our system. We pass the resulting Path object to the built-in str function to
get the string representation of the image’s location. Then, we pass that string to
the matplotlib.image module’s imread function, which loads the image.
• The last statement calls Axes method imshow to display the current image in the
current subplot.
428 Object-Oriented Programming
Self Check
1 (Fill-In) Matplotlib function returns a tuple containing a Figure and an
array of Axes objects.
Answer: subplots.
2 (True/False) Path method appendpath appends to a Path object.
Answer: False. Path method joinpath appends to a Path object
3 (Fill-In) The Path object Path('.') represents .
Answer: the current folder from which the code was executed.
4 (IPython Session) Continue this section’s session by reshuffling the cards, then creat-
ing a new Figure containing two rows of five cards each—these might represent two five-
card poker hands.
In [13]: deck_of_cards.shuffle()
In [16]: figure.tight_layout()
430 Object-Oriented Programming
Answer:
Because every subclass object is an object of its base class, and one base class can have
many subclasses, the set of objects represented by a base class is often larger than the set of
objects represented by any of its subclasses. For example, the base class Vehicle represents
all vehicles, including cars, trucks, boats, bicycles and so on. By contrast, subclass Car rep-
resents a smaller, more specific subset of vehicles.
CommunityMember Inheritance Hierarchy
Inheritance relationships form tree-like hierarchical structures. A base class exists in a hierar-
chical relationship with its subclasses. Let’s develop a sample class hierarchy (shown in the
following diagram), also called an inheritance hierarchy. A university community has thou-
sands of members, including employees, students and alumni. Employees are either faculty
or staff members. Faculty members are either administrators (e.g., deans and department
chairpersons) or teachers. The hierarchy could contain many other classes. For example, stu-
10.7 Inheritance: Base Classes and Subclasses 431
CommunityMember
Faculty Staff
Administrator Teacher
Each arrow in the hierarchy represents an is-a relationship. As we follow the arrows
upward in this class hierarchy, we can state, for example, that “an Employee is a
CommunityMember” and “a Teacher is a Faculty member.” CommunityMember is the direct
base class of Employee, Student and Alum and is an indirect base class of all the other classes
in the diagram. Starting from the bottom, you can follow the arrows and apply the is-a
relationship up to the topmost superclass. For example, Administrator is a Faculty mem-
ber, is an Employee, is a CommunityMember and, of course, ultimately is an object.
Shape Inheritance Hierarchy
Now consider the Shape inheritance hierarchy in the following class diagram, which
begins with base class Shape, followed by subclasses TwoDimensionalShape and ThreeDim-
ensionalShape. Each Shape is either a TwoDimensionalShape or a ThreeDimensional-
Shape. The third level of this hierarchy contains specific types of TwoDimensionalShapes
and ThreeDimensionalShapes. Again, we can follow the arrows from the bottom of the
diagram to the topmost base class in this class hierarchy to identify several is-a relation-
ships. For example, a Triangle is a TwoDimensionalShape and is a Shape, while a Sphere
is a ThreeDimensionalShape and is a Shape. This hierarchy could contain many other
classes. For example, ellipses and trapezoids also are TwoDimensionalShapes, and cones
and cylinders also are ThreeDimensionalShapes.
Shape
TwoDimensionalShape ThreeDimensionalShape
Self Check
1 (Fill-In) A base class exists in a(n) relationship with its subclasses.
Answer: hierarchical.
2 (Fill-In) In this section’s Shape class hierarchy, TwoDimensionalShape is a(n)
of Shape and a(n) of Circle, Square and Triangle.
Answer: subclass, base class.
1 # commmissionemployee.py
2 """CommissionEmployee base class."""
3 from decimal import Decimal
4
10.8 Building an Inheritance Hierarchy; Introducing Polymorphism 433
5 class CommissionEmployee:
6 """An employee who gets paid commission based on gross sales."""
7
8 def __init__(self, first_name, last_name, ssn,
9 gross_sales, commission_rate):
10 """Initialize CommissionEmployee's attributes."""
11 self._first_name = first_name
12 self._last_name = last_name
13 self._ssn = ssn
14 self.gross_sales = gross_sales # validate via property
15 self.commission_rate = commission_rate # validate via property
16
17 @property
18 def first_name(self):
19 return self._first_name
20
21 @property
22 def last_name(self):
23 return self._last_name
24
25 @property
26 def ssn(self):
27 return self._ssn
28
29 @property
30 def gross_sales(self):
31 return self._gross_sales
32
33 @gross_sales.setter
34 def gross_sales(self, sales):
35 """Set gross sales or raise ValueError if invalid."""
36 if sales < Decimal('0.00'):
37 raise ValueError('Gross sales must be >= to 0')
38
39 self._gross_sales = sales
40
41 @property
42 def commission_rate(self):
43 return self._commission_rate
44
45 @commission_rate.setter
46 def commission_rate(self, rate):
47 """Set commission rate or raise ValueError if invalid."""
48 if not (Decimal('0.0') < rate < Decimal('1.0')):
49 raise ValueError(
50 'Interest rate must be greater than 0 and less than 1')
51
52 self._commission_rate = rate
53
54 def earnings(self):
55 """Calculate earnings."""
56 return self.gross_sales * self.commission_rate
57
434 Object-Oriented Programming
58 def __repr__(self):
59 """Return string representation for repr()."""
60 return ('CommissionEmployee: ' +
61 f'{self.first_name} {self.last_name}\n' +
62 f'social security number: {self.ssn}\n' +
63 f'gross sales: {self.gross_sales:.2f}\n' +
64 f'commission rate: {self.commission_rate:.2f}')
Properties first_name, last_name and ssn are read-only. We chose not to validate
them, though we could have. For example, we could validate the first and last names—
perhaps by ensuring that they’re of a reasonable length. We could validate the Social Secu-
rity number to ensure that it contains nine digits, with or without dashes (for example, to
ensure that it’s in the format ###-##-#### or #########, where each # is a digit).
All Classes Inherit Directly or Indirectly from Class object
You use inheritance to create new classes from existing ones. In fact, every Python class
inherits from an existing class. When you do not explicitly specify the base class for a new
class, Python assumes that the class inherits directly from class object. The Python class
hierarchy begins with class object, the direct or indirect base class of every class. So, class
CommissionEmployee’s header could have been written as
class CommissionEmployee(object):
The parentheses after CommissionEmployee indicate inheritance and may contain a single
class for single inheritance or a comma-separated list of base classes for multiple inheri-
tance. Once again, multiple inheritance is beyond the scope of this book.
Class CommissionEmployee inherits all the methods of class object. Class object
does not have any data attributes. Two of the many methods inherited from object are
__repr__ and __str__. So every class has these methods that return string representations
of the objects on which they’re called. When a base-class method implementation is inap-
propriate for a derived class, that method can be overridden (i.e., redefined) in the derived
class with an appropriate implementation. Method __repr__ (lines 58–64) overrides the
default implementation inherited into class CommissionEmployee from class object.7
Testing Class CommissionEmployee
Let’s quickly test some of CommissionEmployee’s features. First, create and display a Com-
missionEmployee:
In [1]: from commissionemployee import CommissionEmployee
In [4]: c
Out[4]:
CommissionEmployee: Sue Jones
social security number: 333-33-3333
gross sales: 10000.00
commission rate: 0.06
Finally, let’s change the CommissionEmployee’s gross sales and commission rate, then
recalculate the earnings:
In [6]: c.gross_sales = Decimal('20000.00')
In [8]: print(f'{c.earnings():,.2f}')
2,000.00
Self Check
1 (Fill-In) When a base-class method implementation is inappropriate for a derived
class, that method can be (i.e., redefined) in the derived class with an appropriate
implementation.
Answer: overridden.
2 (What Does This Code Do?) In this section’s IPython session, explain in detail what
snippet [6] does:
c.gross_sales = Decimal('20000.00')
Notice that the SalariedCommissionEmployee object has all of the properties of classes
CommissionEmployee and SalariedCommissionEmployee.
Next, let’s calculate and display the SalariedCommissionEmployee’s earnings.
Because we call method earnings on a SalariedCommissionEmployee object, the subclass
version of the method executes:
In [12]: print(f'{s.earnings():,.2f}')
500.00
In [16]: print(s)
SalariedCommissionEmployee: Bob Lewis
social security number: 444-44-4444
gross sales: 10000.00
commission rate: 0.05
base salary: 1000.00
Self Check
1 (Fill-In) Function determines whether an object has an “is a” relationship
with a specific type.
Answer: isinstance.
2 (Fill-In) Function determines whether one class is derived from another.
Answer: issubclass.
10.8 Building an Inheritance Hierarchy; Introducing Polymorphism 439
3 (What Does This Code Do?) Explain in detail what the following statement from class
SalariedCommissionEmployee’s earnings method does:
return super().earnings() + self.base_salary
As you can see, the correct string representation and earnings are displayed for each
employee. This is called polymorphism—a key capability of object-oriented programming
(OOP).
Self Check
1 (Fill-In) enables us to take advantage of the “subclass-object-is-a-base-class-
object” relationship to process objects in a general way.
Answer: Polymorphism.
In Python, this loop works properly as long as employees contains only objects that:
• can be displayed with print (that is, they have a string representation) and
• have an earnings method which can be called with no arguments.
All classes inherit from object directly or indirectly, so they all inherit the default methods
for obtaining string representations that print can display. If a class has an earnings
method that can be called with no arguments, we can include objects of that class in the
list employees, even if the object’s class does not have an “is a” relationship with class Com-
missionEmployee. To demonstrate this, consider class WellPaidDuck:
In [1]: class WellPaidDuck:
...: def __repr__(self):
...: return 'I am a well-paid duck'
...: def earnings(self):
...: return Decimal('1_000_000.00')
...:
8. https://docs.python.org/3/glossary.html#term-duck-typing.
10.10 Operator Overloading 441
WellPaidDuck objects, which clearly are not meant to be employees, will work with
the preceding loop. To prove this, let’s create objects of our classes CommissionEmployee,
SalariedCommissionEmployee and WellPaidDuck and place them in a list:
In [2]: from decimal import Decimal
In [7]: d = WellPaidDuck()
Now, let’s process the list using the loop from Section 10.8.3. As you can see in the output,
Python is able to use duck typing to polymorphically process all three objects in the list:
In [9]: for employee in employees:
...: print(employee)
...: print(f'{employee.earnings():,.2f}\n')
...:
CommissionEmployee: Sue Jones
social security number: 333-33-3333
gross sales: 10000.00
commission rate: 0.06
600.00
I am a well-paid duck
1,000,000.00
9. Note that Python has built-in support for complex values. In the exercises, we’ll ask you to explore
using these built-in capabilities.
10.10 Operator Overloading 443
Next, create and display a couple of Complex objects. Snippets [3] and [5] implicitly call
the Complex class’s __repr__ method to get a string representation of each object:
In [2]: x = Complex(real=2, imaginary=4)
In [3]: x
Out[3]: (2 + 4i)
In [5]: y
Out[5]: (5 - 1i)
We chose the __repr__ string format shown in snippets [3] and [5] to mimic the
__repr__ strings produced by Python’s built-in complex type.10
Now, let’s use the + operator to add the Complex objects x and y. This expression adds
the real parts of the two operands (2 and 5) and the imaginary parts of the two operands
(4i and -1i), then returns a new Complex object containing the result:
In [6]: x + y
Out[6]: (7 + 3i)
In [8]: y
Out[8]: (5 - 1i)
Finally, let’s use the += operator to add y to x and store the result in x. The += operator
modifies its left operand but not its right operand:
In [9]: x += y
In [10]: x
Out[10]: (7 + 3i)
In [11]: y
Out[11]: (5 - 1i)
10. Python uses j rather than i for – 1. For example, 3+4j (with no spaces around the operator) creates
a complex object with real and imag attributes. The __repr__ string for this complex value is
'(3+4j)'.
444 Object-Oriented Programming
1 # complexnumber.py
2 """Complex class with overloaded operators."""
3
4 class Complex:
5 """Complex class that represents a complex number
6 with real and imaginary parts."""
7
8 def __init__(self, real, imaginary):
9 """Initialize Complex class's attributes."""
10 self.real = real
11 self.imaginary = imaginary
12
Overloaded + Operator
The following overridden special method __add__ defines how to overload the + operator
for use with two Complex objects:
13 def __add__(self, right):
14 """Overrides the + operator."""
15 return Complex(self.real + right.real,
16 self.imaginary + right.imaginary)
17
Methods that overload binary operators must provide two parameters—the first (self) is
the left operand and the second (right) is the right operand. Class Complex’s __add__
method takes two Complex objects as arguments and returns a new Complex object con-
taining the sum of the operands’ real parts and the sum of the operands’ imaginary parts.
We do not modify the contents of either of the original operands. This matches our
intuitive sense of how this operator should behave. Adding two numbers does not modify
either of the original values.
Overloaded += Augmented Assignment
Lines 18–22 overload special method __iadd__ to define how the += operator adds two
Complex objects:
18 def __iadd__(self, right):
19 """Overrides the += operator."""
20 self.real += right.real
21 self.imaginary += right.imaginary
22 return self
23
Augmented assignments modify their left operands, so method __iadd__ modifies the
selfobject, which represents the left operand, then returns self.
Method __repr__
Lines 24–28 return the string representation of a Complex number.
24 def __repr__(self):
25 """Return string representation for repr()."""
26 return (f'({self.real} ' +
27 ('+' if self.imaginary >= 0 else '-') +
28 f' {abs(self.imaginary)}i)')
10.11 Exception Class Hierarchy and Custom Exceptions 445
Self Check
1 (Fill-In) Suppose a and b are integer variables and a program calculates a + b. Now
suppose c and d are string variables and a program performs the concatenation c + d. The
two + operators here are clearly being used for different purposes. This is an example of
.
Answer: operator overloading.
2 (True/False) Python allows you to create new operators to overload and to change
how existing operators work for built-in types.
Answer: False. Python prohibits you from creating new operators, and operator overload-
ing cannot change how an operator works with built-in types.
3 (IPython Session) Modify class Complex to support operators - and -=, then test these
operators.
Answer: The new method definitions (located in complexnumber2.py) are:
def __sub__(self, right):
"""Overrides the - operator."""
return Complex(self.real - right.real,
self.imaginary - right.imaginary)
In [4]: x - y
Out[4]: (-3 + 5i)
In [5]: x -= y
In [6]: x
Out[6]: (-3 + 5i)
In [7]: y
Out[7]: (5 - 1i)
11. https://docs.python.org/3/library/exceptions.html.
446 Object-Oriented Programming
Self Check
1 (Fill-In) Most exceptions you’ll encounter inherit from base class and are
defined in module .
Answer: Exception, exceptions.
2 (True/False) When you raise an exception from your code, you should generally use
a new exception class.
Answer: False. When you raise an exception from your code, you should generally use one
of the existing exception classes from the Python Standard Library.
10.12 Named Tuples 447
Function namedtuple creates a subclass of the built-in tuple type. The function’s first
argument is your new type’s name and the second is a list of strings representing the iden-
tifiers you’ll use to reference the new type’s members:
In [2]: Card = namedtuple('Card', ['face', 'suit'])
We now have a new tuple type named Card that we can use anywhere a tuple can be used.
Let’s create a Card object, access its members by name and display its string representation:
In [3]: card = Card(face='Ace', suit='Spades')
In [4]: card.face
Out[4]: 'Ace'
In [5]: card.suit
Out[5]: 'Spades'
In [6]: card
Out[6]: Card(face='Ace', suit='Spades')
In [9]: card
Out[9]: Card(face='Queen', suit='Hearts')
This could be useful, for example, if you have a named tuple type representing records in
a CSV file. As you read and tokenize CSV records, you could convert them into named
tuple objects.
For a given object of a named tuple type, you can get an OrderedDict dictionary rep-
resentation of the object’s member names and values. An OrderedDict remembers the
order in which its key–value pairs were inserted in the dictionary:
In [10]: card._asdict()
Out[10]: OrderedDict([('face', 'Queen'), ('suit', 'Hearts')])
Self Check
1 (Fill-In) The Python Standard Library’s collections module’s function
creates a custom tuple type that enables you to reference the tuple’s members by name
rather than by index number.
Answer: namedtuple.
2 (IPython Session) Create a namedtuple called Time with members named hour, min-
ute and second. Then, create a Time object, access its members and display its string rep-
resentation.
Answer:
In [1]: from collections import namedtuple
In [5]: t
Out[5]: Time(hour=13, minute=30, second=45)
12. https://www.python.org/dev/peps/pep-0557/.
10.13 A Brief Intro to Python 3.7’s New Data Classes 449
Optionally, the @dataclass decorator may specify parentheses containing arguments that
help the data class determine what autogenerated methods to include. For example, the
decorator @dataclass(order=True) would cause the data class to autogenerate over-
loaded comparison operator methods for <, <=, > and >=. This might be useful, for exam-
ple, if you need to sort your data-class objects.
Variable Annotations: Class Attributes
Unlike regular classes, data classes declare both class attributes and data attributes inside
the class, but outside the class’s methods. In a regular class, only class attributes are declared
this way, and data attributes typically are created in __init__. Data classes require addi-
tional information, or hints, to distinguish class attributes from data attributes, which also
affects the autogenerated methods’ implementation details.
Lines 9–11 define and initialize the class attributes FACES and SUITS:
9 FACES: ClassVar[List[str]] = ['Ace', '2', '3', '4', '5', '6', '7',
10 '8', '9', '10', 'Jack', 'Queen', 'King']
11 SUITS: ClassVar[List[str]] = ['Hearts', 'Diamonds', 'Clubs', 'Spades']
12
13. https://docs.python.org/3/library/dataclasses.html#module-level-decorators-
classes-and-functions.
450 Object-Oriented Programming
is a variable annotation14,15 (sometimes called a type hint) specifying that FACES is a class
attribute (ClassVar) which refers to a list of strings (List[str]). SUITS also is a class attri-
bute which refers to a list of strings.
Class variables are initialized in their definitions and are specific to the class, not indi-
vidual objects of the class. Methods __init__, __repr__ and __eq__, however, are for use
with objects of the class. When a data class generates these methods, it inspects all the vari-
able annotations and includes only the data attributes in the method implementations.
Variable Annotations: Data Attributes
Normally, we create an object’s data attributes in the class’s __init__ method (or methods
called by __init__) via assignments of the form self.attribute_name = value. Because a
data class autogenerates its __init__ method, we need another way to specify data attri-
butes in a data class’s definition. We cannot simply place their names inside the class,
which generates a NameError, as in:
In [1]: from dataclasses import dataclass
In [2]: @dataclass
...: class Demo:
...: x # attempting to create a data attribute x
...:
-------------------------------------------------------------------------
NameError Traceback (most recent call last)
<ipython-input-2-79ffe37b1ba2> in <module>()
----> 1 @dataclass
2 class Demo:
3 x # attempting to create a data attribute x
4
<ipython-input-2-79ffe37b1ba2> in Demo()
1 @dataclass
2 class Demo:
----> 3 x # attempting to create a data attribute x
4
Like class attributes, each data attribute must be declared with a variable annotation.
Lines 13–14 define the data attributes face and suit. The variable annotation ": str"
indicates that each should refer to string objects:
13 face: str
14 suit: str
14. https://www.python.org/dev/peps/pep-0526/.
15. Variable annotations are a recent language feature and are optional for regular classes. You will not
see them in most legacy Python code.
10.13 A Brief Intro to Python 3.7’s New Data Classes 451
Self Check
1 (Fill-In) Data classes require that specify each class attribute’s or data attri-
bute’s data type.
Answer: variable annotations.
2 (Fill-In) The decorator specifies that a new class is a data class.
Answer: @dataclass.
3 (True/False) The Python Standard Library’s annotations module defines the vari-
able annotations that are required in data class definitions.
Answer: False. The typing module defines the variable annotations that are required in
data-class definitions.
4 (True/False) Data classes have auto-generated <, <=, > and >= operators, by default.
Answer: False. The == and != operators are autogenerated by default. The <, <=, > and >=
operators are autogenerated only if the @dataclass decorator specifies the keyword argu-
ment order=True.
Next, let’s use Card’s autogenerated __repr__ method to display the Card:
In [3]: c1
Out[3]: Card(face='Ace', suit='Spades')
Our custom __str__ method, which print calls when passing it a Card object, returns a
string of the form 'face of suit':
In [4]: print(c1)
Ace of Spades
In [6]: c1.suit
Out[6]: 'Spades'
In [7]: c1.image_name
Out[7]: 'Ace_of_Spades.png'
Next, let’s demonstrate that Card objects can be compared via the autogenerated == oper-
ator and inherited != operator. First, create two additional Card objects—one identical to
the first and one different:
In [8]: c2 = Card(Card.FACES[0], Card.SUITS[3])
In [9]: c2
Out[9]: Card(face='Ace', suit='Spades')
In [11]: c3
Out[11]: Card(face='Ace', suit='Hearts')
In [13]: c1 == c3
Out[13]: False
In [14]: c1 != c3
Out[14]: True
Our Card data class is interchangeable with the Card class developed earlier in this
chapter. To demonstrate this, we created the deck2.py file containing a copy of class
DeckOfCards from earlier in the chapter and imported the Card data class into the file. The
following snippets import class DeckOfCards, create an object of the class and print it.
Recall that print implicitly calls the DeckOfCards __str__ method, which formats each
Card in a field of 19 characters, resulting in a call to each Card’s __format__ method. Read
each row left-to-right to confirm that all the Cards are displayed in order from each suit
(Hearts, Diamonds, Clubs and Spades):
In [15]: from deck2 import DeckOfCards # uses Card data class
In [17]: print(deck_of_cards)
Ace of Hearts 2 of Hearts 3 of Hearts 4 of Hearts
5 of Hearts 6 of Hearts 7 of Hearts 8 of Hearts
9 of Hearts 10 of Hearts Jack of Hearts Queen of Hearts
King of Hearts Ace of Diamonds 2 of Diamonds 3 of Diamonds
4 of Diamonds 5 of Diamonds 6 of Diamonds 7 of Diamonds
8 of Diamonds 9 of Diamonds 10 of Diamonds Jack of Diamonds
Queen of Diamonds King of Diamonds Ace of Clubs 2 of Clubs
3 of Clubs 4 of Clubs 5 of Clubs 6 of Clubs
7 of Clubs 8 of Clubs 9 of Clubs 10 of Clubs
Jack of Clubs Queen of Clubs King of Clubs Ace of Spades
2 of Spades 3 of Spades 4 of Spades 5 of Spades
6 of Spades 7 of Spades 8 of Spades 9 of Spades
10 of Spades Jack of Spades Queen of Spades King of Spades
Self Check
1 (IPython Session) Python is a dynamically typed language, so variable annotations are
not enforced on objects of data classes. To prove this, create a Card object, then assign the
integer 100 to its face attribute and display the Card. Display the face attribute’s type
before and after the assignment
Answer:
In [1]: from carddataclass import Card
In [3]: c
Out[3]: Card(face='Ace', suit='Spades')
In [4]: type(c.face)
Out[4]: str
In [6]: c
Out[6]: Card(face=100, suit='Spades')
In [7]: type(c.face)
Out[7]: int
16. https://www.python.org/dev/peps/pep-0526/.
454 Object-Oriented Programming
• If you have code that unpacks a tuple, adding more members to that tuple breaks
the unpacking code. Data class objects cannot be unpacked. So you can add more
data attributes to a data class without breaking existing code.
• A data class can be a base class or a subclass in an inheritance hierarchy.
• When you change data attributes defined in a data class, then use it in a script or
interactive session, the autogenerated code updates automatically. So, you have
less code to maintain and debug.
• The required variable annotations for class attributes and data attributes enable
you to take advantage of static code analysis tools. So, you might be able to elim-
inate additional errors before they can occur at execution time.
• Some static code analysis tools and IDEs can inspect variable annotations and
issue warnings if your code uses the wrong type. This can help you locate logic
errors in your code before you execute it. In an end-of-chapter exercise, we ask you
to use the static code analysis tool MyPy to demonstrate such warnings.
More Information
Data classes have additional capabilities, such as creating “frozen” instances which do not
allow you to assign values to a data class object’s attributes after the object is created. For
a complete list of data class benefits and capabilities, see
https://www.python.org/dev/peps/pep-0557/
and
https://docs.python.org/3/library/dataclasses.html
We’ll ask you to experiment with additional data class features in this chapter’s exercises.
17. J. N. Buxton and B. Randell, eds, Software Engineering Techniques, April 1970, p. 16. Report on a
conference sponsored by the NATO Science Committee, Rome, Italy, 27–31 October 1969
10.14 Unit Testing with Docstrings and doctest 455
ule’s testmod function, it inspects your functions’, methods’ and classes' docstrings look-
ing for sample Python statements preceded by >>>, each followed on the next line by the
given statement’s expected output (if any).18 The testmod function then executes those
statements and confirms that they produce the expected output. If they do not, testmod
reports errors indicating which tests failed so you can locate and fix the problems in your
code. Each test you define in a docstring typically tests a specific unit of code, such as a
function, a method or a class. Such tests are called unit tests.
Modified Account Class
The file accountdoctest.py contains the class Account from this chapter’s first example.
We modified the __init__ method’s docstring to include four tests which can be used to
ensure that the method works correctly:
• The test in line 11 creates a sample Account object named account1. This state-
ment does not produce any output.
• The test in line 12 shows what the value of account1’s name attribute should be
if line 11 executed successfully. The sample output is shown in line 13.
• The test in line 14 shows what the value of account1’s balance attribute should
be if line 11 executed successfully. The sample output is shown in line 15.
• The test in line 18 creates an Account object with an invalid initial balance. The
sample output shows that a ValueError exception should occur in this case. For
exceptions, the doctest module’s documentation recommends showing just the
first and last lines of the traceback.19
You can intersperse your tests with descriptive text, such as line 17.
1 # accountdoctest.py
2 """Account class definition."""
3 from decimal import Decimal
4
5 class Account:
6 """Account class for demonstrating doctest."""
7
8 def __init__(self, name, balance):
9 """Initialize an Account object.
10
11 >>> account1 = Account('John Green', Decimal('50.00'))
12 >>> account1.name
13 'John Green'
14 >>> account1.balance
15 Decimal('50.00')
16
17 The balance argument must be greater than or equal to 0.
18 >>> account2 = Account('John Green', Decimal('-50.00'))
19 Traceback (most recent call last):
20 ...
21 ValueError: Initial balance must be >= to 0.00.
22 """
18. The notation >>> mimics the standard python interpreter’s input prompts.
19. https://docs.python.org/3/library/doctest.html?highlight=doctest#module-doctest.
456 Object-Oriented Programming
23
24 # if balance is less than 0.00, raise an exception
25 if balance < Decimal('0.00'):
26 raise ValueError('Initial balance must be >= to 0.00.')
27
28 self.name = name
29 self.balance = balance
30
31 def deposit(self, amount):
32 """Deposit money to the account."""
33
34 # if amount is less than 0.00, raise an exception
35 if amount < Decimal('0.00'):
36 raise ValueError('amount must be positive.')
37
38 self.balance += amount
39
40 if __name__ == '__main__':
41 import doctest
42 doctest.testmod(verbose=True)
Module __main__
When you load any module, Python assigns a string containing the module’s name to a
global attribute of the module called __name__. When you execute a Python source file
(such as accountdoctest.py) as a script, Python uses the string '__main__' as the mod-
ule’s name. You can use __name__ in an if statement like lines 40–42 to specify code that
should execute only if the source file is executed as a script. In this example, line 41 imports
the doctest module and line 42 calls the module’s testmod function to execute the doc-
string unit tests.
Running Tests
Run the file accountdoctest.py as a script to execute the tests. By default, if you call
testmod with no arguments, it does not show test results for successful tests. In that case, if
you get no output, all the tests executed successfully. In this example, line 42 calls testmod
with the keyword argument verbose=True. This tells testmod to produce verbose output
showing every test’s results:
Trying:
account1 = Account('John Green', Decimal('50.00'))
Expecting nothing
ok
Trying:
account1.name
Expecting:
'John Green'
ok
Trying:
account1.balance
Expecting:
Decimal('50.00')
ok
Trying:
account2 = Account('John Green', Decimal('-50.00'))
10.14 Unit Testing with Docstrings and doctest 457
Expecting:
Traceback (most recent call last):
...
ValueError: Initial balance must be >= to 0.00.
ok
3 items had no tests:
__main__
__main__.Account
__main__.Account.deposit
1 items passed all tests:
4 tests in __main__.Account.__init__
4 tests in 4 items.
4 passed and 0 failed.
Test passed.
In verbose mode, testmod shows for each test what it’s "Trying" to do and what it’s
"Expecting" as a result, followed by "ok" if the test is successful. After completing the
tests in verbose mode, testmod shows a summary of the results.
To demonstrate a failed test, “comment out” lines 25–26 in accountdoctest.py by
preceding each with a #, then run accountdoctest.py as a script. To save space, we show
just the portions of the doctest output indicating the failed test:
...
**********************************************************************
File "accountdoctest.py", line 18, in __main__.Account.__init__
Failed example:
account2 = Account('John Green', Decimal('-50.00'))
Expected:
Traceback (most recent call last):
...
ValueError: Initial balance must be >= to 0.00.
Got nothing
**********************************************************************
1 items had failures:
1 of 4 in __main__.Account.__init__
4 tests in 4 items.
3 passed and 1 failed.
***Test Failed*** 1 failures.
In this case, we see that line 18’s test failed. The testmod function was expecting a traceback
indicating that a ValueError was raised due to the invalid initial balance. That exception
did not occur, so the test failed. As the programmer responsible for defining this class, this
failing test would be an indication that something is wrong with the validation code in
your __init__ method.
IPython %doctest_mode Magic
A convenient way to create doctests for existing code is to use an IPython interactive ses-
sion to test your code, then copy and paste that session into a docstring. IPython’s In []
and Out[] prompts are not compatible with doctest, so IPython provides the magic
%doctest_mode to display prompts in the correct doctest format. The magic toggles
between the two prompt styles. The first time you execute %doctest_mode, IPython
switches to >>> prompts for input and no output prompts. The second time you execute
%doctest_mode, IPython switches back to In [] and Out[] prompts.
458 Object-Oriented Programming
Self Check
1 (Fill-In) When you execute a Python source file as a script, Python creates a global
attribute __name__ and assigns it the string .
Answer: '__main__'.
2 (True/False) When you execute the doctest module’s testmod function, it inspects
your code and automatically creates tests for you.
Answer: False. When you execute the doctest module’s testmod function, it inspects
your code’s function, method and class docstrings looking for sample Python statements
preceded by >>>, each followed on the next line by the given statement’s expected output
(if any).
3 (IPython Session) Add tests to the deposit method’s docstring, then execute the tests.
Your test should create an Account object, deposit a valid amount into it, then attempt to
deposit an invalid negative amount, which raises a ValueError.
Answer: The updated docstring for method deposit is shown below, followed by the ver-
bose doctest results:
"""Deposit money to the account.
>>> account1.deposit(Decimal('-100.00'))
Traceback (most recent call last):
...
ValueError: amount must be positive.
"""
Trying:
account1 = Account('John Green', Decimal('50.00'))
Expecting nothing
ok
Trying:
account1.name
Expecting:
'John Green'
ok
Trying:
account1.balance
Expecting:
Decimal('50.00')
ok
Trying:
account2 = Account('John Green', Decimal('-50.00'))
Expecting:
Traceback (most recent call last):
...
ValueError: Initial balance must be >= to 0.00.
ok
Trying:
account1 = Account('John Green', Decimal('50.00'))
10.15 Namespaces and Scopes 459
Expecting nothing
ok
Trying:
account1.deposit(Decimal('10.55'))
Expecting nothing
ok
Trying:
account1.balance
Expecting:
Decimal('60.55')
ok
Trying:
account1.deposit(Decimal('-100.00'))
Expecting:
Traceback (most recent call last):
...
ValueError: amount must be positive.
ok
2 items had no tests:
__main__
__main__.Account
2 items passed all tests:
4 tests in __main__.Account.__init__
4 tests in __main__.Account.deposit
8 tests in 4 items.
8 passed and 0 failed.
Test passed.
identifiers are in scope to the code within that module until the program (or interactive ses-
sion) terminates. An IPython session has its own global namespace for all the identifiers
you create in that session.
Each module’s global namespace also has an identifier called __name__ containing the
module’s name, such as 'math' for the math module or 'random' for the random module.
As you saw in the previous section’s doctest example, __name__ contains '__main__' for
a .py file that you run as a script.
Built-In Namespace
The built-in namespace associates identifiers for Python’s built-in functions (such as,
input and range) and types (such as, int, float and str) with objects that define those
functions and types. Python creates the built-in namespace when the interpreter starts exe-
cuting. The built-in namespace’s identifiers remain in scope for all code until the program
(or interactive session) terminates.20
Finding Identifiers in Namespaces
When you use an identifier, Python searches for that identifier in the currently accessible
namespaces, proceeding from local to global to built-in. To help you understand the name-
space search order, consider the following IPython session:
In [1]: z = 'global z'
In [3]: print_variables()
local y in print_variables
global z
The identifiers you define in an IPython session are placed in the session’s global
namespace. When snippet [3] calls print_variables, Python searches the local, global
and built-in namespaces as follows:
• Snippet [3] is not in a function or method, so the session’s global namespace and
the built-in namespace are currently accessible. Python first searches the session’s
global namespace, which contains print_variables. So print_variables is in
scope and Python uses the corresponding object to call print_variables.
• As print_variables begins executing, Python creates the function’s local name-
space. When function print_variables defines the local variable y, Python adds
y to the function’s local namespace. The variable y is now in scope until the func-
tion finishes executing.
20. This assumes you do not shadow the built-in functions or types by redefining their identifiers in a
local or global namespace. We discussed shadowing in the “Functions” chapter.
10.15 Namespaces and Scopes 461
• Next, print_variables calls the built-in function print, passing y as the argu-
ment. To execute this call, Python must resolve the identifiers y and print. The
identifier y is defined in the local namespace, so it’s in scope and Python will use
the corresponding object (the string 'local y in print_variables') as print’s
argument. To call the function, Python must find print’s corresponding object.
First, it looks in the local namespace, which does not define print. Next, it looks
in the session’s global namespace, which does not define print. Finally, it looks
in the built-in namespace, which does define print. So, print is in scope and
Python uses the corresponding object to call print.
• Next, print_variables calls the built-in function print again with the argu-
ment z, which is not defined in the local namespace. So, Python looks in the
global namespace. The argument z is defined in the global namespace, so z is in
scope and Python will use the corresponding object (the string 'global z') as
print’s argument. Again, Python finds the identifier print in the built-in name-
space and uses the corresponding object to call print.
• At this point, we reach the end of the print_variables function’s suite, so the
function terminates and its local namespace no longer exists, meaning the local
variable y is now undefined.
To prove that y is undefined, let’s try to display y:
In [4]: y
-------------------------------------------------------------------------
NameError Traceback (most recent call last)
<ipython-input-4-9063a9f0e032> in <module>()
----> 1 y
In this case, there’s no local namespace, so Python searches for y in the session’s global
namespace. The identifier y is not defined there, so Python searches for y in the built-in
namespace. Again, Python does not find y. There are no more namespaces to search, so
Python raises a NameError, indicating that y is not defined.
The identifiers print_variables and z still exist in the session’s global namespace, so
we can continue using them. For example, let’s evaluate z to see its value:
In [5]: z
Out[5]: 'global z'
Nested Functions
One namespace we did not cover in the preceding discussion is the enclosing namespace.
Python allows you to define nested functions inside other functions or methods. For
example, if a function or method performs the same task several times, you might define
a nested function to avoid repeating code in the enclosing function. When you access an
identifier inside a nested function, Python searches the nested function’s local namespace
first, then the enclosing function’s namespace, then the global namespace and finally the
built-in namespace. This is sometimes referred to as the LEGB (local, enclosing, global,
built-in) rule. In an exercise, we ask you to create a nested function to demonstrate this
namespace search order.
462 Object-Oriented Programming
Class Namespace
A class has a namespace in which its class attributes are stored. When you access a class
attribute, Python looks for that attribute first in the class’s namespace, then in the base
class’s namespace, and so on, until either it finds the attribute or it reaches class object. If
the attribute is not found, a NameError occurs.
Object Namespace
Each object has its own namespace containing the object’s methods and data attributes.
The class’s __init__ method starts with an empty object (self) and adds each attribute
to the object’s namespace. Once you define an attribute in an object’s namespace, clients
using the object may access the attribute’s value.
Self Check
1 (Fill-In) A function’s namespace stores information about identifiers cre-
ated in the function, such as its parameters and local variables.
Answer: local.
2 (True/False) When a function attempts to get an attribute’s value, Python searches the
local namespace, then the global namespace, then the built-in namespace until it finds the
attribute; otherwise, a NameError occurs.
Answer: True.
Time Series
The data we’ll use is a time series in which the observations are ordered by year. Univariate
time series have one observation per time, such as the average of the January high tempera-
tures in New York City for a particular year. Multivariate time series have two or more
observations per time, such as temperature, humidity and barometric pressure readings in
a weather application. Here, we’ll analyze a univariate time series.
Two tasks often performed with time series are:
• Time series analysis, which looks at existing time series data for patterns, helping
data analysts understand the data. A common analysis task is to look for season-
ality in the data. For example, in New York City, the monthly average high tem-
perature varies significantly based on the seasons (winter, spring, summer or fall).
• Time series forecasting, which uses past data to predict the future.
We’ll perform time series forecasting in this section.
Simple Linear Regression
Using a technique called simple linear regression, we’ll make predictions by finding a lin-
ear relationship between the months (January of each year) and New York City’s average
January high temperatures. Given a collection of values representing an independent vari-
able (the month/year combination) and a dependent variable (the average high tempera-
ture for that month/year), simple linear regression describes the relationship between these
variables with a straight line, known as the regression line.
Linear Relationships
To understand the general concept of a linear relationship, consider Fahrenheit and Cel-
sius temperatures. Given a Fahrenheit temperature, we can calculate the corresponding
Celsius temperature using the following formula:
c = 5 / 9 * (f - 32)
In this formula, f (the Fahrenheit temperature) is the independent variable, and c (the Cel-
sius temperature) is the dependent variable—each value of c depends on the value of f used
in the calculation.
Plotting Fahrenheit temperatures and their corresponding Celsius temperatures pro-
duces a straight line. To show this, let’s first create a lambda for the preceding formula and
use it to calculate the Celsius equivalents of the Fahrenheit temperatures 0–100 in 10-
degree increments. We store each Fahrenheit/Celsius pair as a tuple in temps:
In [1]: c = lambda f: 5 / 9 * (f - 32)
Next, let’s place the data in a DataFrame, then use its plot method to display the lin-
ear relationship between the Fahrenheit and Celsius temperatures. The plot method’s
style keyword argument controls the data’s appearance. The period in the string '.-'
indicates that each point should appear as a dot, and the dash indicates that lines should
connect the dots. We manually set the y-axis label to 'Celsius' because the plot method
shows 'Celsius' only in the graph’s upper-left corner legend, by default.
464 Object-Oriented Programming
The simple linear regression algorithm iteratively adjusts the slope and intercept and, for
each adjustment, calculates the square of each point’s distance from the line. The “best fit”
occurs when the slope and intercept values minimize the sum of those squared distances.
This is known as an ordinary least squares calculation.21
The SciPy (Scientific Python) library is widely used for engineering, science and
math in Python. This library’s linregress function (from the scipy.stats module) per-
forms simple linear regression for you. After calling linregress, you’ll plug the resulting
slope and intercept into the y = mx + b equation to make predictions.
Pandas
In the three previous Intro to Data Science sections, you used pandas to work with data.
You’ll continue using pandas throughout the rest of the book. In this example, we’ll load
the data for New York City’s 1895–2018 average January high temperatures from a CSV
file into a DataFrame. We’ll then format the data for use in this example.
Seaborn Visualization
We’ll use Seaborn to plot the DataFrame’s data with a regression line that shows the aver-
age high-temperature trend over the period 1895–2018.
Getting Weather Data from NOAA
Let’s get the data for our study. The National Oceanic and Atmospheric Administration
(NOAA)22 offers lots of public historical data including time series for average high tem-
peratures in specific cities over various time intervals.
We obtained the January average high temperatures for New York City from 1895
through 2018 from NOAA’s “Climate at a Glance” time series at:
https://www.ncdc.noaa.gov/cag/
21. https://en.wikipedia.org/wiki/Ordinary_least_squares.
22. http://www.noaa.gov.
466 Object-Oriented Programming
On that web page, you can select temperature, precipitation and other data for the entire
U.S., regions within the U.S., states, cities and more. Once you’ve set the area and time
frame, click Plot to display a diagram and view a table of the selected data. At the top of
that table are links for downloading the data in several formats including CSV, which we
discussed in the “Files and Exceptions” chapter. NOAA’s maximum date range available
at the time of this writing was 1895–2018. For your convenience, we provided the data in
the ch10 examples folder in the file ave_hi_nyc_jan_1895-2018.csv. If you download the
data on your own, delete the rows above the line containing "Date,Value,Anomaly".
This data contains three columns per observation:
• Date—A value of the form 'YYYYMM’ (such as '201801'). MM is always 01 because
we downloaded data for only January of each year.
• Value—A floating-point Fahrenheit temperature.
• Anomaly—The difference between the value for the given date and average values
for all dates. We do not use the Anomaly value in this example, so we’ll ignore it.
Loading the Average High Temperatures into a DataFrame
Let’s load and display the New York City data from ave_hi_nyc_jan_1895-2018.csv:
In [7]: nyc = pd.read_csv('ave_hi_nyc_jan_1895-2018.csv')
We can look at the DataFrame’s head and tail to get a sense of the data:
In [8]: nyc.head()
Out[8]:
Date Value Anomaly
0 189501 34.2 -3.2
1 189601 34.7 -2.7
2 189701 35.5 -1.9
3 189801 39.6 2.2
4 189901 36.4 -1.0
In [9]: nyc.tail()
Out[9]:
Date Value Anomaly
119 201401 35.5 -1.9
120 201501 36.1 -1.3
121 201601 40.8 3.4
122 201701 42.8 5.4
123 201801 38.7 1.3
In [11]: nyc.head(3)
Out[11]:
Date Temperature Anomaly
0 189501 34.2 -3.2
1 189601 34.7 -2.7
2 189701 35.5 -1.9
10.16 Intro to Data Science: Time Series and Simple Linear Regression 467
Seaborn labels the tick marks on the x-axis with Date values. Since this example pro-
cesses only January temperatures, the x-axis labels will be more readable if they do not con-
tain 01 (for January), we’ll remove it from each Date. First, let’s check the column’s type:
In [12]: nyc.Date.dtype
Out[12]: dtype('int64')
The values are integers, so we can divide by 100 to truncate the last two digits. Recall that
each column in a DataFrame is a Series. Calling Series method floordiv performs inte-
ger division on every element of the Series:
In [13]: nyc.Date = nyc.Date.floordiv(100)
In [14]: nyc.head(3)
Out[14]:
Date Temperature Anomaly
0 1895 34.2 -3.2
1 1896 34.7 -2.7
2 1897 35.5 -1.9
In [16]: nyc.Temperature.describe()
Out[16]:
count 124.00
mean 37.60
std 4.54
min 26.10
25% 34.58
50% 37.60
75% 40.60
max 47.60
Name: Temperature, dtype: float64
Function linregress receives two one-dimensional arrays23 of the same length represent-
ing the data points’ x- and y-coordinates. The keyword arguments x and y represent the
independent and dependent variables, respectively. The object returned by linregress
contains the regression line’s slope and intercept:
23. These arguments also can be one-dimensional array-like objects, such as lists or pandas Series.
468 Object-Oriented Programming
In [19]: linear_regression.slope
Out[19]: 0.00014771361132966167
In [20]: linear_regression.intercept
Out[20]: 8.694845520062952
We can use these values with the simple linear regression equation for a straight line,
y = mx + b, to predict the average January temperature in New York City for a given year.
Let’s predict the average Fahrenheit temperature for January of 2019. In the following cal-
culation, linear_regression.slope is m, 2019 is x (the date value for which you’d like to
predict the temperature), and linear_regression.intercept is b:
In [21]: linear_regression.slope * 2019 + linear_regression.intercept
Out[21]: 38.51837136113298
We also can approximate what the average temperature might have been in the years
before 1895. For example, let’s approximate the average temperature for January of 1890:
In [22]: linear_regression.slope * 1890 + linear_regression.intercept
Out[22]: 36.612865774980335
For this example, we had data for 1895–2018. You should expect that the further you go
outside this range, the less reliable the predictions will be.
Plotting the Average High Temperatures and a Regression Line
Next, let’s use Seaborn’s regplot function to plot each data point with the dates on the
x-axis and the temperatures on the y-axis. The regplot function creates the scatter plot or
scattergram below in which the scattered blue dots represent the Temperatures for the
given Dates, and the straight line displayed through the points is the regression line:
First, close the prior Matplotlib window if you have not done so already—otherwise,
regplot will use the existing window that already contains a graph. Function regplot’s x
and y keyword arguments are one-dimensional arrays24 of the same length representing
the x-y coordinate pairs to plot. Recall that pandas automatically creates attributes for each
column name if the name can be a valid Python identifier:25
24. These arguments also can be one-dimensional array-like objects, such as lists or pandas Series.
10.16 Intro to Data Science: Time Series and Simple Linear Regression 469
In [24]: sns.set_style('whitegrid')
https://data.gov/
This is the U.S. government’s open data portal. Searching for “time series” yields over 7200
time-series datasets.
25. For readers with more statistics background, the shaded area surrounding the regression line is the
95% confidence interval for the regression line (https://en.wikipedia.org/wiki/Simple_lin-
ear_regression#Confidence_intervals). To draw the diagram without a confidence interval, add
the keyword argument ci=None to the regplot function’s argument list.
470 Object-Oriented Programming
https://www.ncdc.noaa.gov/cag/
The National Oceanic and Atmospheric Administration (NOAA) Climate at a Glance portal
provides both global and U.S. weather-related time series.
https://www.esrl.noaa.gov/psd/data/timeseries/
NOAA’s Earth System Research Laboratory (ESRL) portal provides monthly and seasonal cli-
mate-related time series.
https://www.quandl.com/search
Quandl provides hundreds of free financial-related time series, as well as fee-based time series.
https://datamarket.com/data/list/?q=provider:tsdl
The Time Series Data Library (TSDL) provides links to hundreds of time series datasets across
many industries.
http://archive.ics.uci.edu/ml/datasets.html
The University of California Irvine (UCI) Machine Learning Repository contains dozens of
time-series datasets for a variety of topics.
http://inforumweb.umd.edu/econdata/econdata.html
The University of Maryland’s EconData service provides links to thousands of economic time
series from various U.S. government agencies.
Self Check
1 (Fill-In) Time series looks at existing time series data for patterns, helping
data analysts understand the data. Time series uses data from the past to predict
the future.
Answer: analysis, forecasting.
2 (True/False) In the formula, c = 5 / 9 * (f - 32), f (the Fahrenheit temperature) is
the independent variable and c (the Celsius temperature) is the dependent variable.
Answer: True.
3 (IPython Session) Assuming that this linear trend continues, based on the slope and
intercept values calculated in this section’s interactive session, in what year might the aver-
age January temperature in New York City reach 40 degrees Fahrenheit.
Answer:
In [27]: year = 2019
In [32]: year
Out[32]: 2120
10.17 Wrap-Up 471
10.17 Wrap-Up
In this chapter, we discussed the details of crafting valuable classes. You saw how to define
a class, create objects of the class, access an object’s attributes and call its methods. You
define the special method __init__ to create and initialize a new object’s data attributes.
We discussed controlling access to attributes and using properties. We showed that all
object attributes may be accessed directly by a client. We discussed identifiers with single
leading underscores (_), which indicate attributes that are not meant to be accessed by cli-
ent code. We showed how to implement “private” attributes via the double-leading-
underscore (__) naming convention, which tells Python to mangle an attribute’s name.
We implemented a card shuffling and dealing simulation consisting of a Card class
and a DeckOfCards class that maintained a list of Cards, and displayed the deck both as
strings and as card images using Matplotlib. We introduced special methods __repr__,
__str__ and __format__ for creating string representations of objects.
Next, we looked at Python’s capabilities for creating base classes and subclasses. We
showed how to create a subclass that inherits many of its capabilities from its superclass,
then adds more capabilities, possibly by overriding the base class’s methods. We created a
list containing both base class and subclass objects to demonstrate Python’s polymorphic
programming capabilities.
We introduced operator overloading for defining how Python’s built-in operators work
with objects of custom class types. You saw that overloaded operator methods are imple-
mented by overriding various special methods that all classes inherit from class object. We
discussed the Python exception class hierarchy and creating custom exception classes.
We showed how to create a named tuple that enables you to access tuple elements via
attribute names rather than index numbers. Next, we introduced Python 3.7’s new data
classes, which can autogenerate various boilerplate code commonly provided in class defi-
nitions, such as the __init__, __repr__ and __eq__ special methods.
You saw how to write unit tests for your code in docstrings, then execute those tests con-
veniently via the doctest module’s testmod function. Finally, we discussed the various
namespaces that Python uses to determine the scopes of identifiers. In the next chapter, we’ll
introduce the computer science concepts of recursion, searching and sorting and Big O.
Exercises
10.1 (What’s Wrong with This Code?) What is wrong with the code in the following
IPython session?
In [1]: try:
...: raise RuntimeError()
...: except Exception:
...: print('An Exception occurred')
...: except RuntimeError:
...: print('A RuntimeError occurred')
...:
An Exception occurred
10.2 (Car Class with Read-Only Properties) Write a class Car that contains the model,
year (of manufacture), and speed of a car as hidden attributes (name these attributes with
a leading underscore). Provide the necessary read-only properties. Write two methods to
drive the car faster and slower. Test your Car class.
472 Object-Oriented Programming
10.3 (Coin Class) Write a Coin Class to keep track of the number of 2 euro, 1 euro,
50 euro cent, 20 euro cent, and 10 euro cent coins in a wallet. Provide a read-only property
universal_str that returns a string with the number of coins for each coin type. Write a
second method that returns the total amount of money. Keep your attributes hidden and
use the read-only properties. Create a coin object and test the methods.
10.4 (Modifying the Internal Data Representation of a Class) Section 10.4.2’s Time
class represents the time as three integer values. Modify the class to store the time as the
total number of seconds since midnight. Replace the _hour, _minute and _second attri-
butes with one _total_seconds attribute. Modify the bodies of the hour, minute and
second properties’ methods to get and set _total_seconds. Re-execute Section 10.4’s
IPython session using the modified Time class to show that the updated class Time is inter-
changeable with the original one.
10.5 (Duck Typing) Recall that with duck typing, objects of unrelated classes can respond
to the same method calls if they implement those methods. In Section 10.8, you created a
list containing a CommissionEmployee and a SalariedCommissionEmployee. Then, you it-
erated through it, displaying each employee’s string representation and earnings. Create a
class SalariedEmployee for an employee that gets paid a fixed weekly salary. Do not inherit
from CommissionEmployee or SalariedCommissionEmployee. In class SalariedEmployee,
override method __repr__ and provide an earnings method. Demonstrate duck typing by
creating an object of your class, adding it to the list at the end of Section 10.8, then executing
the loop to show that it properly processes objects of all three classes.
10.6 (Calculating Vector Lengths) A vector is an object that has a magnitude and a di-
rection. A vector is represented as a line segment with an arrow where the arrow represents
the direction and the length of the vector is the magnitude. A vector can also be a single
point with x- and y-coordinates. The magnitude calculated from this type of vector equals
the distance between the origin of the cartesian coordinate system and the point. Write a
class Point that represents an (x-y) coordinate pair and provides x and y read-write prop-
erties as well as a method to calculate the distance between the point and the origin of the
coordinate system. Include __init__ and __repr__ methods. Write a class Vector that
has two Point objects as its attributes. Include __init__ and __repr__ methods and a
method that can calculate the magnitude of the vector. Test your Point and your
Vector class.
10.7 (Manipulating Dates and Times with Module datetime) The Python Standard Li-
brary’s datetime module contains a datetime class for manipulating dates and times. The
class provides various overloaded operators. Research class datetime’s capabilities, then
perform the following tasks:
a) Get the current date and time and store it in variable x.
b) Repeat Part (a) and store the result in variable y.
c) Display each datetime object.
d) Display each datetime object’s data attributes individually.
e) Use the comparison operators to compare the two datetime objects.
f) Calculate the difference between y and x.
10.8 (Converting Data Class Objects to Tuples and Dictionaries) In some cases, you
might want to treat data class objects as tuples or dictionaries. The dataclasses module
provides functions astuple and asdict for this purpose. Research these functions, then
create an object of this chapter’s Card data class and use these functions to convert the Card
to a tuple and a dictionary. Display the results.
Exercises 473
10.9 (Calculator Class) Write a class that implements a Calculator. The class should
contain 2 numbers. Provide an __init__ method that takes the 2 numbers as arguments.
Also, provide the following read-only properties:
a) sum returns the sum of the 2 numbers
b) difference returns the difference between the 2 numbers
c) product returns the product between the 2 numbers
d) division returns the quotient of number1 divided by number2
These properties should not have corresponding data attributes; rather, they should use
side in calculations that return the desired values. Create a Calculator object and dis-
play its properties.
10.10 (Player Class) Write a class called Player that can be used in a turn-based strategy
game to create player objects. Create the following data attributes-the name (a string), level
(an int), strength (an int), and Health Points (a Decimal). The class should have an
__init__ method to initialize the data attributes. Provide a property for each data attri-
bute. Use validation in the properties of the level and health attribute to ensure that they
remain non-negative. Provide a defence method to defend from an attack. The intensity
of the attack is calculated as follows—divide the strength of the attacker by a random num-
ber between 1 and 3, subtract from this result the strength of our player multiplied with
the level of our player. If the intensity is ≤ 0, the player loses, and the intensity is subtracted
from the health points of the player.
10.11 (Class Fraction) The Python Standard Library module fractions provides a
Fraction class that stores the numerator and denominator of a fraction, such as:
2---
4
Research Fraction’s capabilities, then demonstrate:
a) Adding two Fractions.
b) Subtracting two Fractions.
c) Multiplying two Fractions.
d) Dividing two Fractions.
e) Printing Fractions in the form a/b, where a is the numerator and b is the de-
nominator.
f) Converting Fractions to floating-point numbers with built-in function float.
10.12 (Built-in Type complex) Python supports complex numbers with the built-in type
complex.Research complex’s capabilities, then demonstrate:
a) Adding two complex numbers.
b) Subtracting two complex numbers.
c) Printing complex numbers.
d) Getting the real and imaginary parts of complex numbers.
10.13 (doctest) Create a script containing the following maximum function:
def maximum(value1, value2, value3):
"""Return the maximum of three values."""
max_value = value1
if value2 > max_value:
max_value = value2
if value3 > max_value:
max_value = value3
return max_value
474 Object-Oriented Programming
Modify the function’s docstring to define tests for calling function maximum with three
ints, three floats and three strings. For each type, provide three tests—one with the
largest value as the first argument, one with the largest value as the second argument, one
with the largest value as the third argument. Use doctest to run your tests and confirm
that all execute correctly. Next, modify the maximum function to use < operators rather
than > operators. Run your tests again to see which tests fail.
10.14 (Creating an Account Data Class Dynamically) The dataclasses module’s
make_dataclass function creates a data class dynamically from a list of strings that repre-
sent the data class’s attributes. Research function make_dataclass, then use it to generate
an Account class from the following list of strings:
['account', 'name', 'balance']
Create objects of the new Account class, then display their string representations and
compare the objects with the == and != operators.
10.15 (Immutable Data Class Objects) Built-in types int, float, str and tuple are im-
mutable. Data classes can simulate immutability by designating that objects of the class
should be “frozen” after they’re created. Client code cannot assign values to the attributes of
a frozen object. Research “frozen” data classes, then reimplement this chapter’s Complex class
as a “frozen” data class. Show that you cannot modify a Complex object after you create it.
10.16 (Account Inheritance Hierarchy) Create an inheritance hierarchy that a bank
might use to represent customer bank accounts. All customers at this bank can deposit
money into their accounts and withdraw money from their accounts. More specific types
of accounts also exist. Savings accounts, for instance, earn interest on the money they hold.
Checking accounts, on the other hand, don’t earn interest and charge a fee per transaction.
Start with class Account from this chapter and create two subclasses SavingsAccount
and CheckingAccount. A SavingsAccount should also include a data attribute indicating
the interest rate. A SavingsAccount’s calculate_interest method should return the
Decimal result of multiplying the interest rate by the account balance. SavingsAccount
should inherit methods deposit and withdraw without redefining them.
A CheckingAccount should include a Decimal data attribute that represents the fee
charged per transaction. Class CheckingAccount should override methods deposit and
withdraw so that they subtract the fee from the account balance whenever either transac-
tion is performed successfully. CheckingAccount’s versions of these methods should
invoke the base-class Account versions to update the account balance. CheckingAccount’s
withdraw method should charge a fee only if money is withdrawn (that is, the withdrawal
amount does not exceed the account balance).
Create objects of each class and tests their methods. Add interest to the SavingsAc-
count object by invoking its calculate_interest method, then passing the returned
interest amount to the object’s deposit method.
10.17 (Nested Functions and Namespaces) Section 10.15 discussed namespaces and how
Python uses them to determine which identifiers are in scope. We also mentioned the
LEGB (local, enclosing, global, built-in) rule for the order in which Python searches for
identifiers in namespaces. For each of the print function calls in the following IPython
session, list the namespaces that Python searches for print’s argument:
In [1]: z = 'global z'
Exercises 475
In [3]: print_variables()
local y in print_variables
global z
x in nested function
local y in print_variables
global z
10.18 (Intro to Data Science: Time Series) Reimplement the Intro to Data Science section’s
study using the Los Angeles Average January High Temperatures for 1985 through 2018,
which can be found in the file ave_hi_la_jan_1895-2018.csv located in the ch10 examples
folder. How does the Los Angeles temperature trend compare to that of New York City?
10.19 (Project: Static Code Analysis with Prospector and MyPy) In Exercise 3.24, you
used the prospector static code analysis tool to check your code for common errors and
suggested improvements. The prospector tool includes support for checking variable an-
notations with the MyPy static code analysis tool. Research MyPy online. Write a script
that creates objects of this chapter’s Card data class. In the script, assign integers to a Card’s
face and suit string attributes. Then, use MyPy to analyze the script and see the warning
messages that MyPy produces. For instructions on using MyPy with prospector, see
https://github.com/PyCQA/prospector/blob/master/docs/
supported_tools.rst
10.20 (Project: Solitaire) Using classes Card and DeckOfCards from this chapter’s exam-
ples, implement your favorite solitaire card game.
10.21 (Project: Blackjack) Using the DeckOfCards class from this chapter, create a simple
Blackjack game. The rules of the game are as follows:
• Two cards each are dealt to the dealer and the player. The player’s cards are dealt
face up. Only one of the dealer’s cards is dealt face up.
• Each card has a value. A card numbered 2 through 10 is worth its face value.
Jacks, queens and kings each count as 10. Aces can count as 1 or 11—whichever
value is more beneficial to the player (as we’ll soon see).
• If the sum of the player’s first two cards is 21 (that is, the player was dealt a card
valued at 10 and an ace, which counts as 11 in this situation), the player has
“blackjack” and immediately wins the game—if the dealer does not also have
blackjack, which would result in a “push” (or tie).
• Otherwise, the player can begin taking additional cards one at a time. These cards
are dealt face up, and the player decides when to stop taking cards. If the player
“busts” (that is, the sum of the player’s cards exceeds 21), the game is over and the
476 Object-Oriented Programming
player loses. When the player is satisfied with the current set of cards, the player
“stands” (that is, stops taking cards), and the dealer’s hidden card is revealed.
• If the dealer’s total is 16 or less, the dealer must take another card; otherwise, the
dealer must stand. The dealer must continue taking cards until the sum of the
cards is greater than or equal to 17. If the dealer exceeds 21, the player wins. Oth-
erwise, the hand with the higher point total wins. If the dealer and the player have
the same point total, the game is a “push,” and no one wins.
An ace’s value for a dealer depends on the dealer’s other card(s) and the casino’s house
rules. A dealer typically must hit for totals of 16 or less and must stand for 17 or more.
For a “soft 17”—a total of 17 with one ace counted as 11—some casinos require the
dealer to hit and some require the dealer to stand (we require the dealer to stand). Such a
hand is known as a “soft 17” because taking another card cannot bust the hand.
Enable a player to interact with the game using the keyboard—'H' means hit (take
another card and 'S' means stand (do not take another card). Display the dealer’s and
player’s hands as card images using Matplotlib, as we did in this chapter.
10.22 (Project: Card Class with Overloaded Comparison Operators) Modify class Card to
support the comparison operators, so you can determine whether one Card is less than, equal
to or greater than another. Investigate the functools module’s total_ordering decorator.
If your class is preceded by @total_ordering and defines methods __eq__ and __lt__ (for
the < operator), the remaining comparison methods for <=, > and >= are autogenerated.
10.23 (Project: Poker) Exercises 5.25–5.26 asked you to create functions for comparing
poker hands. Develop equivalent features for use with this chapter’s DeckOfCards class.
Develop a new class called Hand that represents a five-card poker hand. Use operator over-
loading to enable two Hands to be compared with the comparison operators. Use your new
capabilities in a simple poker game script.
10.24 (Project: PyDealer Library) We demonstrated basic card shuffling and dealing in
this chapter, but many card games require significant additional capabilities. As is often
the case in Python, libraries already exist that can help you build more substantial card
games. One such library is PyDealer. Research this library’s extensive capabilities, then use
it to implement your favorite card game.
10.25 (Project: Enumerations) Many programming languages provide a language element
called an enumeration for creating sets of named constants. Often, these are used to make
code more readable. The Python Standard Library’s enum module enables you to emulate
this concept by creating subclasses of the Enum base class. Investigate the enum module’s ca-
pabilities, then create subclasses of Enum that represent card faces and card suits. Modify class
Card to use these to represent the face and suit as enum constants rather than as strings.
10.26 (Software Engineering with Abstract Classes and Abstract Methods) When we
think of a class, we assume that programs use it to create objects. Sometimes, it’s useful to
declare classes for which you never instantiate objects, because in some way they are incom-
plete. As you’ll see, such classes can help you engineer effective inheritance hierarchies.
Concrete Classes—Consider Section 10.7’s Shape hierarchy. If Circle, Square and
Triangle objects all have draw methods, its reasonable to expect that calling draw on a
Circle will display a Circle, calling draw on a Square will display a Square and calling
draw on a Triangle will display a Triangle. Objects of each class know all the details of
Exercises 477
the specific shapes to draw. Classes that provide (or inherit) implementations of every
method they define and that can be used to create objects are called concrete classes.
Abstract Classes—Now, let’s consider class TwoDimensionalShape in the Shape hier-
archy’s second level. If we were to create a TwoDimensionalShape object and call its draw
method, class TwoDimensionalShape knows that all two-dimensional shapes are drawable,
but it does not know what specific two-dimensional shape to draw—there are many! So it
does not make sense for TwoDimensionalShape to fully implement a draw method. A
method that is defined in a given class, but for which you cannot provide an implementa-
tion is called an abstract method. Any class with an abstract method has a “hole”—the
incomplete method implementation—and is called an abstract class. TypeErrors occur
when you try to create objects of abstract classes. In the Shape hierarchy, classes Shape,
TwoDimensionalShape and ThreeDimensionalShape all are abstract classes. They all know
that shapes should be drawable, but do not know what specific shape to draw. Abstract base
classes are too general to create real objects.
Inheriting a Common Design—An abstract class’s purpose is to provide a base class
from which subclasses can inherit a common design, such as a specific set of attributes and
methods. So, such classes often are called abstract base classes. In the Shape hierarchy,
subclasses inherit from the abstract base class Shape the notion of what it means to be a
Shape—that is, common properties, such as location and color, and common behaviors,
such as draw, move and resize.
Polymorphic Employee Payroll System—Now, let’s develop an Employee class hier-
archy that begins with an abstract class, then use polymorphism to perform payroll calcu-
lations for objects of two concrete subclasses. Consider the following problem statement:
A company pays its employees weekly. The employees are of two types. Salaried
employees are paid a fixed weekly salary regardless of the number of hours worked.
Hourly employees are paid by the hour and receive overtime pay (1.5 times their
hourly salary rate) for all hours worked in excess of 40 hours. The company wants
to implement an app that performs its payroll calculations polymorphically.
Employee Hierarchy Class Diagram—The following diagram shows the Employee
hierarchy. Abstract class Employee represents the general concept of an employee. Subclasses
SalariedEmployee and HourlyEmployee inherit from Employee. Employee is italicized by
convention to indicate that it’s an abstract class. Concrete class names are not italicized:
Employee
SalariedEmployee HourlyEmployee
Abstract Base Class Employee—The Python Standard Library’s abc (abstract base
class) module helps you define abstract classes by inheriting from the module’s ABC class.
Your abstract base class Employee class should declare the methods and properties that all
employees should have. Each employee, regardless of the way his or her earnings are cal-
culated, has a first name, a last name and a Social Security number. Also, every employee
should have an earnings method, but the specific calculation depends on the employee’s
type, so you’ll make earnings an abstract method that the subclasses must override. Your
Employee class should contain:
478 Object-Oriented Programming
• An __init__ method that initializes the first name, last name and Social Security
number data attributes.
• Read-only properties for the first name, last name and Social Security number
data attributes.
• An abstract method earnings preceded by the abc module’s @abstractmethod
decorator. Concrete subclasses must implement this method. The Python docu-
mentation says you should raise a NotImplementedError in abstract methods.26
• A __repr__ method that returns a string containing the first name, last name and
Social Security number of the employee.
Concrete Subclass SalariedEmployee—This Employee subclass should override
earnings to return a SalariedEmployee’s weekly salary. The class also should include:
• An __init__ method that initializes the first name, last name, Social Security
number and weekly salary data attributes. The first three of these should be ini-
tialized by calling base class Employee’s __init__ method.
• A read-write weekly_salary property in which the setter ensures that the prop-
erty is always non-negative.
• A __repr__ method that returns a string starting with 'SalariedEmployee:' and
followed by all the information about a SalariedEmployee. This overridden
method should call Employee’s version.
Concrete Subclass HourlyEmployee—This Employee subclass should override earn-
ings to return an HourlyEmployee’s earnings, based on the hours worked and wage per
hour. The class also should include:
• An __init__ method to initialize the first name, last name, Social Security num-
ber, hours and wages data attributes. The first name, last name and Social Security
number should be initialized by calling base class Employee’s __init__ method.
• Read-write hours and wages properties in which the setters ensure that the
hours are in range (0–168) and wage per hour is always non-negative.
• A __repr__ method that returns a string starting with 'HourlyEmployee:' and
followed by all the information about a HourlyEmployee. This overridden
method should call Employee’s version.
Testing Your Classes—In an IPython session, test your hierarchy:
• Import the classes Employee, SalariedEmployee and HourlyEmployee.
• Attempt to create an Employee object to see the TypeError that occurs and prove
that you cannot create an object of an abstract class.
• Assign objects of the concrete classes SalariedEmployee and HourlyEmployee to
variables, then display each employee’s string representation and earnings.
• Place the objects into a list, then iterate through the list and polymorphically pro-
cess each object, displaying its string representation and earnings.
26. https://docs.python.org/3.7/library/exceptions.html#NotImplementedError.
Computer Science Thinking:
Recursion, Searching,
Sorting and Big O 11
Objectives
In this chapter you’ll:
Learn the concept of
recursion.
Write and use recursive
functions.
Determine the base case and
recursion step in a recursive
algorithm.
Learn how the system handles
recursive function calls.
Compare recursion and
iteration, including when it’s
best to use each approach.
Search for a given value in an
array using linear search and
binary search.
Sort arrays using the simple
iterative selection and
insertion sort algorithms.
Sort arrays using the more
complex but higher-
performance recursive merge
sort algorithm.
Use Big O notation to
compare the efficiencies of
searching and sorting
algorithms.
Use Seaborn and Matplotlib to
build an animated selection
sort algorithm visualization.
In the exercises, implement
additional sorting algorithms
and animated visualizations,
and determine the Big O of
additional algorithms.