Python is an object-oriented language and everything in Python is an object. Objects are created from classes, like houses are built from blueprints.
This chapter focuses on creating useful custom classes and on classes, objects, inheritance and polymorphism. It also explains that much Python programming is object-based: we commonly reuse classes supplied by Python and third-party libraries.
The chapter begins with a bank Account class. It stores a name and balance and provides methods such as deposit().
from account import Account
from decimal import Decimal
account1 = Account('John Green', Decimal('50.00'))
account1.name
account1.balance
account1.deposit(Decimal('25.53'))
account1.balanceAccount(...) is a constructor expression. It creates and initializes an object.
class Account:
def __init__(self, name, balance):
if balance < Decimal('0.00'):
raise ValueError('Initial balance must be >= to 0.00.')
self.name = name
self.balance = balance
def deposit(self, amount):
if amount < Decimal('0.00'):
raise ValueError('amount must be positive.')
self.balance += amountclass starts a class definition.__init__ initializes an object.self refers to the current object.self.name and self.balance are object attributes.An Account's attributes can refer to objects of other classes, such as a string for the name and a Decimal for the balance. This is composition, also called a has-a relationship.
Direct attribute assignment can bypass validation:
account1.balance = Decimal('-1000.00')Encapsulation means controlling how client code interacts with an object's internal data. Python relies strongly on conventions rather than absolute private-data enforcement.
name → normally public._name → conventionally internal use.A property looks like an attribute but is implemented using methods. This allows a class to validate and format data.
class Time:
def __init__(self, hour=0, minute=0, second=0):
self.hour = hour
self.minute = minute
self.second = second
@property
def hour(self):
return self._hour
@hour.setter
def hour(self, hour):
if not (0 <= hour < 24):
raise ValueError(f'Hour ({hour}) must be 0-23')
self._hour = hourThe getter reads the internal value. The setter validates a new value before storing it. A read-write property has both getter and setter; a read-only property has only a getter.
wake_up = Time(hour=6, minute=30) print(wake_up) wake_up.hour = 7
The chapter also introduces __repr__ for an official-style representation and __str__ for a human-friendly representation.
Python uses two leading underscores to trigger name mangling and discourage direct client access.
class PrivateClass:
def __init__(self):
self.public_data = "public"
self.__private_data = "private"my_object.public_data works, while my_object.__private_data raises AttributeError. Python internally changes the name to a form such as _PrivateClass__private_data.
Card represents one playing card. DeckOfCards represents a 52-card deck containing Card objects.
class Card:
FACES = ['Ace', '2', '3', '4', '5', '6',
'7', '8', '9', '10', 'Jack', 'Queen', 'King']
SUITS = ['Hearts', 'Diamonds', 'Clubs', 'Spades']FACES and SUITS are class attributes: the information belongs to the class and is shared by its objects.
def __repr__(self):
return f"Card(face='{self.face}', suit='{self.suit}')"
def __str__(self):
return f'{self.face} of {self.suit}'The chapter also uses __format__ so Card objects can be formatted inside f-strings.
class DeckOfCards:
NUMBER_OF_CARDS = 52
def __init__(self):
self._current_card = 0
self._deck = []
for count in range(52):
self._deck.append(
Card(Card.FACES[count % 13],
Card.SUITS[count // 13])
)
def shuffle(self):
self._current_card = 0
random.shuffle(self._deck)
def deal_card(self):
try:
card = self._deck[self._current_card]
self._current_card += 1
return card
except:
return NoneThe case study then uses Matplotlib, Path, imshow and 52 subplots to display card images.
Inheritance creates a new class from an existing class. The existing class is the base class or superclass; the new class is the subclass or derived class.
Inheritance represents an is-a relationship. A Car is a Vehicle. A subclass is more specific than its base class.
The payroll example uses CommissionEmployee and SalariedCommissionEmployee. The second is a specialized CommissionEmployee with a base salary.
class CommissionEmployee:
def earnings(self):
return self.gross_sales * self.commission_rateProperties validate gross sales and commission rate.
super()class SalariedCommissionEmployee(CommissionEmployee):
def __init__(self, first_name, last_name, ssn,
gross_sales, commission_rate, base_salary):
super().__init__(
first_name, last_name, ssn,
gross_sales, commission_rate
)
self.base_salary = base_salary
def earnings(self):
return super().earnings() + self.base_salarysuper() calls functionality from the base class. The subclass overrides earnings().
issubclass(SalariedCommissionEmployee, CommissionEmployee) isinstance(s, CommissionEmployee) isinstance(s, SalariedCommissionEmployee)
employees = [commission_employee, salaried_employee]
for employee in employees:
print(employee)
print(employee.earnings())The same method call can produce different behavior depending on the object's class. This is polymorphism.
Python can use polymorphic behavior even when objects are not related through inheritance. This is duck typing.
class WellPaidDuck:
def __repr__(self):
return 'I am a well-paid duck'
def earnings(self):
return Decimal('1000000.00')
employees = [c, s, WellPaidDuck()]
for employee in employees:
print(employee)
print(employee.earnings())The duck works because it provides the required interface: a usable string representation and an earnings() method.
Operator overloading lets custom classes define how operators such as + and += work.
| Operator | Special method |
|---|---|
| + | __add__ |
| += | __iadd__ |
| * | __mul__ |
The chapter demonstrates this with a custom Complex class.
def __add__(self, right):
return Complex(
self.real + right.real,
self.imaginary + right.imaginary
)
def __iadd__(self, right):
self.real += right.real
self.imaginary += right.imaginary
return self__add__ returns a new object. __iadd__ modifies the left object.
Exceptions are objects belonging to an exception class hierarchy. The hierarchy begins with BaseException.
Other direct BaseException subclasses include SystemExit, KeyboardInterrupt and GeneratorExit.
An except Exception handler can catch exceptions derived from Exception. Specific handlers should come before a broad handler.
You can define custom exceptions by inheriting from Exception, but the chapter recommends using an existing standard exception whenever one is appropriate.
A named tuple lets you access tuple members by name instead of numeric index.
from collections import namedtuple
Card = namedtuple('Card', ['face', 'suit'])
card = Card(face='Ace', suit='Spades')
card.face
card.suit
cardNamed tuples also provide features such as _make() for constructing an object from an iterable and _asdict() for getting an ordered dictionary representation.
Data classes provide a concise way to define classes that mainly store related data. The @dataclass decorator can automatically generate common code.
from dataclasses import dataclass
from typing import ClassVar, List
@dataclass
class Card:
FACES: ClassVar[List[str]] = [...]
SUITS: ClassVar[List[str]] = [...]
face: str
suit: strData attributes use variable annotations. Class attributes use ClassVar. Python remains dynamically typed; annotations are not enforced at runtime.
c1 = Card(Card.FACES[0], Card.SUITS[3]) c1 c1 == Card(Card.FACES[0], Card.SUITS[3])
__init__, __repr__ and __eq__.The doctest module tests examples written inside docstrings. It searches for statements beginning with >>> and compares actual results with expected output.
def add(a, b):
"""Add two numbers.
>>> add(2, 3)
5
"""
return a + b
if __name__ == '__main__':
import doctest
doctest.testmod(verbose=True)Each example acts like a small unit test. Successful tests can be retested after code changes. IPython provides %doctest_mode to make interactive sessions easier to copy into doctests.
A namespace associates identifiers with objects. Important namespaces include local, global, built-in, enclosing, class and object namespaces.
| Namespace | Contains |
|---|---|
| Local | Function/method parameters and local variables |
| Global | Module-level names |
| Built-in | print, range, int, str, etc. |
| Enclosing | Outer-function names for nested functions |
| Class/Object | Class attributes / object attributes |
For nested functions, Python searches Local → Enclosing → Global → Built-in.
z = 'global z'
def print_variables():
y = 'local y'
print(y)
print(z)
print_variables()After the function ends, its local namespace disappears, so its local variables are no longer available.
A time series is a sequence of observations associated with points in time. Examples include stock prices and temperature readings.
The chapter first illustrates a linear relationship with Fahrenheit and Celsius temperatures.
c = lambda f: 5 / 9 * (f - 32)
temps = [(f, c(f)) for f in range(0, 101, 10)]
import pandas as pd
temps_df = pd.DataFrame(
temps, columns=['Fahrenheit', 'Celsius']
)
temps_df.plot(x='Fahrenheit', y='Celsius', style='.-')It then uses New York City January average high temperatures from 1895–2018 and SciPy's stats.linregress.
from scipy import stats
linear_regression = stats.linregress(
x=nyc.Date,
y=nyc.Temperature
)The resulting slope and intercept are used in y = mx + b to predict temperatures. Seaborn's regplot is used to visualize the observations and regression line.
You learned how to craft valuable classes, create objects, initialize attributes, define methods and use properties.
You learned composition, internal naming conventions, name mangling, class attributes and special methods such as __repr__, __str__ and __format__.
You then learned inheritance, super(), method overriding, polymorphism, issubclass(), isinstance() and duck typing.
The advanced topics included operator overloading, the exception hierarchy, named tuples, data classes, doctest and namespaces. The data-science section introduced time series and simple linear regression using pandas, SciPy and Seaborn.
self in instance methods.None value from __init__.super().__init__() when base initialization is required.__init__?self used?super() do?@dataclass provide?doctest work?Student class with name and marks.Employee class and a specialized subclass.area() and process them in one loop.+.