144 lines
4.4 KiB
Markdown
144 lines
4.4 KiB
Markdown
\[ [Index](index.md) | [Exercise 3.6](ex3_6.md) | [Exercise 3.8](ex3_8.md) \]
|
|
|
|
# Exercise 3.7
|
|
|
|
*Objectives:*
|
|
|
|
- Type checking and interfaces
|
|
- Abstract base classes
|
|
|
|
*Files Modified:* `tableformat.py`
|
|
|
|
In [Exercise 3.5](ex3_5.md), we modified the `tableformat.py` file to have a `TableFormatter`
|
|
class and to use various subclasses for different output formats. In this exercise, we extend that
|
|
code a bit more.
|
|
|
|
## (a) Interfaces and type checking
|
|
|
|
Modify the `print_table()` function so that it checks if the
|
|
supplied formatter instance inherits from `TableFormatter`. If
|
|
not, raise a `TypeError`.
|
|
|
|
Your new code should catch situations like this:
|
|
|
|
```python
|
|
>>> import stock, reader, tableformat
|
|
>>> portfolio = reader.read_csv_as_instances('Data/portfolio.csv', stock.Stock)
|
|
>>> class MyFormatter:
|
|
def headings(self,headers): pass
|
|
def row(self,rowdata): pass
|
|
|
|
>>> tableformat.print_table(portfolio, ['name','shares','price'], MyFormatter())
|
|
Traceback (most recent call last):
|
|
...
|
|
TypeError: Expected a TableFormatter
|
|
>>>
|
|
```
|
|
|
|
Adding a check like this might add some degree of safety to the program. However you should
|
|
still be aware that type-checking is rather weak in Python. There is no guarantee that the
|
|
object passed as a formatter will work correctly even if it happens to inherit from the
|
|
proper base class. This next part addresses that issue.
|
|
|
|
## (b) Abstract Base Classes
|
|
|
|
Modify the `TableFormatter` base class so that it is defined as a proper
|
|
abstract base class using the `abc` module. Once you have done that, try
|
|
this experiment:
|
|
|
|
```python
|
|
>>> class NewFormatter(TableFormatter):
|
|
def headers(self, headings):
|
|
pass
|
|
def row(self, rowdata):
|
|
pass
|
|
|
|
>>> f = NewFormatter()
|
|
Traceback (most recent call last):
|
|
File "<stdin>", line 1, in <module>
|
|
TypeError: Can't instantiate abstract class NewFormatter with abstract methods headings
|
|
>>>
|
|
```
|
|
|
|
Here, the abstract base class caught a spelling error in the class--the fact that
|
|
the `headings()` method was incorrectly given as `headers()`.
|
|
|
|
## (c) Algorithm Template Classes
|
|
|
|
The file `reader.py` contains two functions, `read_csv_as_dicts()` and `read_csv_as_instances()`.
|
|
Both of those functions are almost identical--there is just one tiny bit of code that's
|
|
different. Maybe that code could be consolidated into a class definition of some sort.
|
|
Add the following class to the `reader.py` file:
|
|
|
|
```python
|
|
# reader.py
|
|
|
|
|
|
import csv
|
|
from abc import ABC, abstractmethod
|
|
|
|
class CSVParser(ABC):
|
|
|
|
def parse(self, filename):
|
|
records = []
|
|
with open(filename) as f:
|
|
rows = csv.reader(f)
|
|
headers = next(rows)
|
|
for row in rows:
|
|
record = self.make_record(headers, row)
|
|
records.append(record)
|
|
return records
|
|
|
|
@abstractmethod
|
|
def make_record(self, headers, row):
|
|
pass
|
|
```
|
|
|
|
This code provides a shell (or template) of the CSV parsing functionality. To use it, you subclass it, add
|
|
any additional attributes you might need, and redefine the `make_record()` method. For example:
|
|
|
|
```python
|
|
class DictCSVParser(CSVParser):
|
|
def __init__(self, types):
|
|
self.types = types
|
|
|
|
def make_record(self, headers, row):
|
|
return { name: func(val) for name, func, val in zip(headers, self.types, row) }
|
|
|
|
class InstanceCSVParser(CSVParser):
|
|
def __init__(self, cls):
|
|
self.cls = cls
|
|
|
|
def make_record(self, headers, row):
|
|
return self.cls.from_row(row)
|
|
```
|
|
|
|
Add the above classes to the `reader.py` file. Here's how you would use one of them:
|
|
|
|
```python
|
|
>>> from reader import DictCSVParser
|
|
>>> parser = DictCSVParser([str, int, float])
|
|
>>> port = parser.parse('Data/portfolio.csv')
|
|
>>>
|
|
```
|
|
|
|
It works, but it's kind of annoying. To fix this, reimplement the `read_csv_as_dicts()` and
|
|
`read_csv_as_instances()` functions to use these classes. Your refactored code should work
|
|
exactly the same way that it did before. For example:
|
|
|
|
```python
|
|
>>> import reader
|
|
>>> import stock
|
|
>>> port = reader.read_csv_as_instances('Data/portfolio.csv', stock.Stock)
|
|
>>>
|
|
```
|
|
|
|
\[ [Solution](soln3_7.md) | [Index](index.md) | [Exercise 3.6](ex3_6.md) | [Exercise 3.8](ex3_8.md) \]
|
|
|
|
----
|
|
`>>>` Advanced Python Mastery
|
|
`...` A course by [dabeaz](https://www.dabeaz.com)
|
|
`...` Copyright 2007-2023
|
|
|
|
. This work is licensed under a [Creative Commons Attribution-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-sa/4.0/)
|