Files
practical-python/Notes/04_Classes_objects/02_Inheritance.md
A D Vishnu Prasad 1809ac0006 Fix broken links
2020-05-29 19:26:03 +05:30

628 lines
17 KiB
Markdown

[Contents](../Contents.md) \| [Previous (4.1 Classes)](01_Class.md) \| [Next (4.3 Special methods)](03_Special_methods.md)
# 4.2 Inheritance
Inheritance is a commonly used tool for writing extensible programs.
This section explores that idea.
### Introduction
Inheritance is used to specialize existing objects:
```python
class Parent:
...
class Child(Parent):
...
```
The new class `Child` is called a derived class or subclass. The
`Parent` class is known as base class or superclass. `Parent` is
specified in `()` after the class name, `class Child(Parent):`.
### Extending
With inheritance, you are taking an existing class and:
* Adding new methods
* Redefining some of the existing methods
* Adding new attributes to instances
In the end you are **extending existing code**.
### Example
Suppose that this is your starting class:
```python
class Stock:
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
def cost(self):
return self.shares * self.price
def sell(self, nshares):
self.shares -= nshares
```
You can change any part of this via inheritance.
### Add a new method
```python
class MyStock(Stock):
def panic(self):
self.sell(self.shares)
```
Usage example.
```python
>>> s = MyStock('GOOG', 100, 490.1)
>>> s.sell(25)
>>> s.shares
75
>>> s.panic()
>>> s.shares
0
>>>
```
### Redefining an existing method
```python
class MyStock(Stock):
def cost(self):
return 1.25 * self.shares * self.price
```
Usage example.
```python
>>> s = MyStock('GOOG', 100, 490.1)
>>> s.cost()
61262.5
>>>
```
The new method takes the place of the old one. The other methods are unaffected. It's tremendous.
## Overriding
Sometimes a class extends an existing method, but it wants to use the
original implementation inside the redefinition. For this, use `super()`:
```python
class Stock:
...
def cost(self):
return self.shares * self.price
...
class MyStock(Stock):
def cost(self):
# Check the call to `super`
actual_cost = super().cost()
return 1.25 * actual_cost
```
Use `super()` to call the previous version.
*Caution: In Python 2, the syntax was more verbose.*
```python
actual_cost = super(MyStock, self).cost()
```
### `__init__` and inheritance
If `__init__` is redefined, it is essential to initialize the parent.
```python
class Stock:
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
class MyStock(Stock):
def __init__(self, name, shares, price, factor):
# Check the call to `super` and `__init__`
super().__init__(name, shares, price)
self.factor = factor
def cost(self):
return self.factor * super().cost()
```
You should call the `__init__()` method on the `super` which is the
way to call the previous version as shown previously.
### Using Inheritance
Inheritance is sometimes used to organize related objects.
```python
class Shape:
...
class Circle(Shape):
...
class Rectangle(Shape):
...
```
Think of a logical hierarchy or taxonomy. However, a more common (and
practical) usage is related to making reusable or extensible code.
For example, a framework might define a base class and instruct you
to customize it.
```python
class CustomHandler(TCPHandler):
def handle_request(self):
...
# Custom processing
```
The base class contains some general purpose code.
Your class inherits and customized specific parts.
### "is a" relationship
Inheritance establishes a type relationship.
```python
class Shape:
...
class Circle(Shape):
...
```
Check for object instance.
```python
>>> c = Circle(4.0)
>>> isinstance(c, Shape)
True
>>>
```
*Important: Ideally, any code that worked with instances of the parent
class will also work with instances of the child class.*
### `object` base class
If a class has no parent, you sometimes see `object` used as the base.
```python
class Shape(object):
...
```
`object` is the parent of all objects in Python.
*Note: it's not technically required, but you often see it specified
as a hold-over from it's required use in Python 2. If omitted, the
class still implicitly inherits from `object`.
### Multiple Inheritance
You can inherit from multiple classes by specifying them in the definition of the class.
```python
class Mother:
...
class Father:
...
class Child(Mother, Father):
...
```
The class `Child` inherits features from both parents. There are some
rather tricky details. Don't do it unless you know what you are doing.
Some further information will be given in the next section, but we're not
going to utilize multiple inheritance further in this course.
## Exercises
A major use of inheritance is in writing code that's meant to be
extended or customized in various ways--especially in libraries or
frameworks. To illustrate, consider the `print_report()` function
in your `report.py` program. It should look something like this:
```python
def print_report(reportdata):
'''
Print a nicely formated table from a list of (name, shares, price, change) tuples.
'''
headers = ('Name','Shares','Price','Change')
print('%10s %10s %10s %10s' % headers)
print(('-'*10 + ' ')*len(headers))
for row in reportdata:
print('%10s %10d %10.2f %10.2f' % row)
```
When you run your report program, you should be getting output like this:
```
>>> import report
>>> report.portfolio_report('Data/portfolio.csv', 'Data/prices.csv')
Name Shares Price Change
---------- ---------- ---------- ----------
AA 100 9.22 -22.98
IBM 50 106.28 15.18
CAT 150 35.46 -47.98
MSFT 200 20.89 -30.34
GE 95 13.48 -26.89
MSFT 50 20.89 -44.21
IBM 100 106.28 35.84
```
### Exercise 4.5: An Extensibility Problem
Suppose that you wanted to modify the `print_report()` function to
support a variety of different output formats such as plain-text,
HTML, CSV, or XML. To do this, you could try to write one gigantic
function that did everything. However, doing so would likely lead to
an unmaintainable mess. Instead, this is a perfect opportunity to use
inheritance instead.
To start, focus on the steps that are involved in a creating a table.
At the top of the table is a set of table headers. After that, rows
of table data appear. Let's take those steps and and put them into
their own class. Create a file called `tableformat.py` and define the
following class:
```python
# tableformat.py
class TableFormatter:
def headings(self, headers):
'''
Emit the table headings.
'''
raise NotImplementedError()
def row(self, rowdata):
'''
Emit a single row of table data.
'''
raise NotImplementedError()
```
This class does nothing, but it serves as a kind of design specification for
additional classes that will be defined shortly. A class like this is
sometimes called an "abstract base class."
Modify the `print_report()` function so that it accepts a
`TableFormatter` object as input and invokes methods on it to produce
the output. For example, like this:
```python
# report.py
...
def print_report(reportdata, formatter):
'''
Print a nicely formated table from a list of (name, shares, price, change) tuples.
'''
formatter.headings(['Name','Shares','Price','Change'])
for name, shares, price, change in reportdata:
rowdata = [ name, str(shares), f'{price:0.2f}', f'{change:0.2f}' ]
formatter.row(rowdata)
```
Since you added an argument to print_report(), you're going to need to modify the
`portfolio_report()` function as well. Change it so that it creates a `TableFormatter`
like this:
```python
# report.py
import tableformat
...
def portfolio_report(portfoliofile, pricefile):
'''
Make a stock report given portfolio and price data files.
'''
# Read data files
portfolio = read_portfolio(portfoliofile)
prices = read_prices(pricefile)
# Create the report data
report = make_report_data(portfolio, prices)
# Print it out
formatter = tableformat.TableFormatter()
print_report(report, formatter)
```
Run this new code:
```python
>>> ================================ RESTART ================================
>>> import report
>>> report.portfolio_report('Data/portfolio.csv', 'Data/prices.csv')
... crashes ...
```
It should immediately crash with a `NotImplementedError` exception. That's not
too exciting, but it's exactly what we expected. Continue to the next part.
### Exercise 4.6: Using Inheritance to Produce Different Output
The `TableFormatter` class you defined in part (a) is meant to be
extended via inheritance. In fact, that's the whole idea. To
illustrate, define a class `TextTableFormatter` like this:
```python
# tableformat.py
...
class TextTableFormatter(TableFormatter):
'''
Emit a table in plain-text format
'''
def headings(self, headers):
for h in headers:
print(f'{h:>10s}', end=' ')
print()
print(('-'*10 + ' ')*len(headers))
def row(self, rowdata):
for d in rowdata:
print(f'{d:>10s}', end=' ')
print()
```
Modify the `portfolio_report()` function like this and try it:
```python
# report.py
...
def portfolio_report(portfoliofile, pricefile):
'''
Make a stock report given portfolio and price data files.
'''
# Read data files
portfolio = read_portfolio(portfoliofile)
prices = read_prices(pricefile)
# Create the report data
report = make_report_data(portfolio, prices)
# Print it out
formatter = tableformat.TextTableFormatter()
print_report(report, formatter)
```
This should produce the same output as before:
```python
>>> ================================ RESTART ================================
>>> import report
>>> report.portfolio_report('Data/portfolio.csv', 'Data/prices.csv')
Name Shares Price Change
---------- ---------- ---------- ----------
AA 100 9.22 -22.98
IBM 50 106.28 15.18
CAT 150 35.46 -47.98
MSFT 200 20.89 -30.34
GE 95 13.48 -26.89
MSFT 50 20.89 -44.21
IBM 100 106.28 35.84
>>>
```
However, let's change the output to something else. Define a new
class `CSVTableFormatter` that produces output in CSV format:
```python
# tableformat.py
...
class CSVTableFormatter(TableFormatter):
'''
Output portfolio data in CSV format.
'''
def headings(self, headers):
print(','.join(headers))
def row(self, rowdata):
print(','.join(rowdata))
```
Modify your main program as follows:
```python
def portfolio_report(portfoliofile, pricefile):
'''
Make a stock report given portfolio and price data files.
'''
# Read data files
portfolio = read_portfolio(portfoliofile)
prices = read_prices(pricefile)
# Create the report data
report = make_report_data(portfolio, prices)
# Print it out
formatter = tableformat.CSVTableFormatter()
print_report(report, formatter)
```
You should now see CSV output like this:
```python
>>> ================================ RESTART ================================
>>> import report
>>> report.portfolio_report('Data/portfolio.csv', 'Data/prices.csv')
Name,Shares,Price,Change
AA,100,9.22,-22.98
IBM,50,106.28,15.18
CAT,150,35.46,-47.98
MSFT,200,20.89,-30.34
GE,95,13.48,-26.89
MSFT,50,20.89,-44.21
IBM,100,106.28,35.84
```
Using a similar idea, define a class `HTMLTableFormatter`
that produces a table with the following output:
```
<tr><th>Name</th><th>Shares</th><th>Price</th><th>Change</th></tr>
<tr><td>AA</td><td>100</td><td>9.22</td><td>-22.98</td></tr>
<tr><td>IBM</td><td>50</td><td>106.28</td><td>15.18</td></tr>
<tr><td>CAT</td><td>150</td><td>35.46</td><td>-47.98</td></tr>
<tr><td>MSFT</td><td>200</td><td>20.89</td><td>-30.34</td></tr>
<tr><td>GE</td><td>95</td><td>13.48</td><td>-26.89</td></tr>
<tr><td>MSFT</td><td>50</td><td>20.89</td><td>-44.21</td></tr>
<tr><td>IBM</td><td>100</td><td>106.28</td><td>35.84</td></tr>
```
Test your code by modifying the main program to create a
`HTMLTableFormatter` object instead of a
`CSVTableFormatter` object.
### Exercise 4.7: Polymorphism in Action
A major feature of object-oriented programming is that you can
plug an object into a program and it will work without having to
change any of the existing code. For example, if you wrote a program
that expected to use a `TableFormatter` object, it would work no
matter what kind of `TableFormatter` you actually gave it. This
behavior is sometimes referred to as "polymorphism."
One potential problem is figuring out how to allow a user to pick out
the formatter that they want. Direct use of the class names such as
`TextTableFormatter` is often annoying. Thus, you might consider some
simplified approach. Perhaps you embed an `if-`statement into the
code like this:
```python
def portfolio_report(portfoliofile, pricefile, fmt='txt'):
'''
Make a stock report given portfolio and price data files.
'''
# Read data files
portfolio = read_portfolio(portfoliofile)
prices = read_prices(pricefile)
# Create the report data
report = make_report_data(portfolio, prices)
# Print it out
if fmt == 'txt':
formatter = tableformat.TextTableFormatter()
elif fmt == 'csv':
formatter = tableformat.CSVTableFormatter()
elif fmt == 'html':
formatter = tableformat.HTMLTableFormatter()
else:
raise RuntimeError(f'Unknown format {fmt}')
print_report(report, formatter)
```
In this code, the user specifies a simplified name such as `'txt'` or
`'csv'` to pick a format. However, is putting a big `if`-statement in
the `portfolio_report()` function like that the best idea? It might
be better to move that code to a general purpose function somewhere
else.
In the `tableformat.py` file, add a function `create_formatter(name)`
that allows a user to create a formatter given an output name such as
`'txt'`, `'csv'`, or `'html'`. Modify `portfolio_report()` so that it
looks like this:
```python
def portfolio_report(portfoliofile, pricefile, fmt='txt'):
'''
Make a stock report given portfolio and price data files.
'''
# Read data files
portfolio = read_portfolio(portfoliofile)
prices = read_prices(pricefile)
# Create the report data
report = make_report_data(portfolio, prices)
# Print it out
formatter = tableformat.create_formatter(fmt)
print_report(report, formatter)
```
Try calling the function with different formats to make sure it's working.
### Exercise 4.8: Putting it all together
Modify the `report.py` program so that the `portfolio_report()` function takes
an optional argument specifying the output format. For example:
```python
>>> report.portfolio_report('Data/portfolio.csv', 'Data/prices.csv', 'txt')
Name Shares Price Change
---------- ---------- ---------- ----------
AA 100 9.22 -22.98
IBM 50 106.28 15.18
CAT 150 35.46 -47.98
MSFT 200 20.89 -30.34
GE 95 13.48 -26.89
MSFT 50 20.89 -44.21
IBM 100 106.28 35.84
>>>
```
Modify the main program so that a format can be given on the command line:
```bash
bash $ python3 report.py Data/portfolio.csv Data/prices.csv csv
Name,Shares,Price,Change
AA,100,9.22,-22.98
IBM,50,106.28,15.18
CAT,150,35.46,-47.98
MSFT,200,20.89,-30.34
GE,95,13.48,-26.89
MSFT,50,20.89,-44.21
IBM,100,106.28,35.84
bash $
```
### Discussion
Writing extensible code is one of the most common uses of inheritance
in libraries and frameworks. For example, a framework might instruct
you to define your own object that inherits from a provided base
class. You're then told to fill in various methods that implement
various bits of functionality.
Another somewhat deeper concept is the idea of "owning your
abstractions." In the exercises, we defined *our own class* for
formatting a table. You may look at your code and tell yourself "I should
just use a formatting library or something that someone else already
made instead!" No, you should use BOTH your class and a library.
Using your own class promotes loose coupling and is more flexible.
As long as your application uses the programming interface of your class,
you can change the internal implementation to work in any way that you
want. You can write all-custom code. You can use someone's third
party package. You swap out one third-party package for a different
package when you find a better one. It doesn't matter--none of
your application code will break as long as you preserve keep the
interface. That's a powerful idea and it's one of the reasons why
you might consider inheritance for something like this.
That said, designing object oriented programs can be extremely
difficult. For more information, you should probably look for books
on the topic of design patterns (although understanding what happened
in this exercise will take you pretty far in terms of using objects in
a practically useful way).
[Contents](../Contents.md) \| [Previous (4.1 Classes)](01_Class.md) \| [Next (4.3 Special methods)](03_Special_methods.md)