python-mastery/Exercises/ex3_8.md

250 lines
8.4 KiB
Markdown
Raw Normal View History

2023-07-17 03:21:00 +02:00
\[ [Index](index.md) | [Exercise 3.7](ex3_7.md) | [Exercise 4.1](ex4_1.md) \]
# Exercise 3.8
*Objectives:*
- Learn about mixin classes and cooperative inheritance
*Files Modified:* `tableformat.py`
## (a) The Trouble with Column Formatting
2023-07-17 17:41:16 +02:00
If you go all the way back to [Exercise 3.1](ex3_1.md), you
2023-07-17 03:21:00 +02:00
wrote a function `print_portfolio()` that produced a table like this:
```python
>>> portfolio = read_portfolio('Data/portfolio.csv')
>>> print_portfolio(portfolio)
name shares price
---------- ---------- ----------
AA 100 32.20
IBM 50 91.10
CAT 150 83.44
MSFT 200 51.23
GE 95 40.37
MSFT 50 65.10
IBM 100 70.44
>>>
```
The `print_table()` function developed in the last several exercises
almost replaces this functionality--almost. The one problem that it
has is that it can't precisely format the content of each column. For
example, notice how the values in the `price` column are precisely
formatted with 2 decimal points. The `TableFormatter` class and
related subclasses can't do that.
One way to fix it would be to modify the `print_table()` function to
accept an additional formats argument. For example, maybe something
like this:
```python
>>> def print_table(records, fields, formats, formatter):
formatter.headings(fields)
for r in records:
rowdata = [(fmt % getattr(r, fieldname))
for fieldname,fmt in zip(fields,formats)]
formatter.row(rowdata)
>>> import stock, reader
>>> portfolio = reader.read_csv_as_instances('Data/portfolio.csv', stock.Stock)
>>> from tableformat import TextTableFormatter
>>> formatter = TextTableFormatter()
>>> print_table(portfolio,
['name','shares','price'],
['%s','%d','%0.2f'],
formatter)
name shares price
---------- ---------- ----------
AA 100 32.20
IBM 50 91.10
CAT 150 83.44
MSFT 200 51.23
GE 95 40.37
MSFT 50 65.10
IBM 100 70.44
>>>
```
Yes, you could modify `print_table()` like this, but is that the right
place to do it? The whole idea of all of the `TableFormatter` classes
is that they could be used in different kinds of applications. Column
formatting is something that could be useful elsewhere, not just
in the `print_table()` function.
Another possible approach might be to change the interface to the
`TableFormatter` class in some way. For example, maybe adding a third
method to apply formatting.
```python
class TableFormatter:
def headings(self, headers):
...
def format(self, rowdata):
...
def row(self, rowdata):
...
```
The problem here is that any time you change the interface on a class,
you're going to have to refactor all of the existing code to work with
it. Specifically, you'd have to modify all of the already written
`TableFormatter` subclasses and all of the code written to use them.
Let's not do that.
As an alternative, a user could use inheritance to customize a
specific formatter in order to inject some formatting into it. For
example, try this experiment:
```python
>>> from tableformat import TextTableFormatter, print_table
>>> class PortfolioFormatter(TextTableFormatter):
def row(self, rowdata):
formats = ['%s','%d','%0.2f']
rowdata = [(fmt % d) for fmt, d in zip(formats, rowdata)]
super().row(rowdata)
>>> formatter = PortfolioFormatter()
>>> print_table(portfolio, ['name','shares','price'], formatter)
name shares price
---------- ---------- ----------
AA 100 32.20
IBM 50 91.10
CAT 150 83.44
MSFT 200 51.23
GE 95 40.37
MSFT 50 65.10
IBM 100 70.44
>>>
```
Yes, that works, but it's also a bit clumsy and weird. The user has
to pick a specific formatter to customize. On top of that, they have
to implement the actual column formatting code themselves. Surely
there is a different way to do this.
## (b) Going Sideways
In the `tableformat.py` file, add the following class definition:
```python
class ColumnFormatMixin:
formats = []
def row(self, rowdata):
rowdata = [(fmt % d) for fmt, d in zip(self.formats, rowdata)]
super().row(rowdata)
```
This class contains a single method `row()` that applies formatting to
the row contents. A class variable `formats` is used to hold the format
codes. This class is used via multiple inheritance. For example:
```python
>>> import stock, reader
>>> portfolio = reader.read_csv_as_instances('Data/portfolio.csv', stock.Stock)
>>> from tableformat import TextTableFormatter, ColumnFormatMixin, print_table
>>> class PortfolioFormatter(ColumnFormatMixin, TextTableFormatter):
formats = ['%s', '%d', '%0.2f']
>>> formatter = PortfolioFormatter()
>>> print_table(portfolio, ['name','shares','price'], formatter)
name shares price
---------- ---------- ----------
AA 100 32.20
IBM 50 91.10
CAT 150 83.44
MSFT 200 51.23
GE 95 40.37
MSFT 50 65.10
IBM 100 70.44
```
This whole approach works because the `ColumnFormatMixin` class is
meant to be mixed together with another class that provides the
required `row()` method.
Make another class that makes a formatter print the table headers in all-caps:
```python
class UpperHeadersMixin:
def headings(self, headers):
super().headings([h.upper() for h in headers])
```
Try it out and notice that the headers are now uppercase:
```python
>>> from tableformat import TextTableFormatter, UpperHeadersMixin
>>> class PortfolioFormatter(UpperHeadersMixin, TextTableFormatter):
pass
>>> formatter = PortfolioFormatter()
>>> print_table(portfolio, ['name','shares','price'], formatter)
NAME SHARES PRICE
---------- ---------- ----------
AA 100 32.2
IBM 50 91.1
CAT 150 83.44
MSFT 200 51.23
GE 95 40.37
MSFT 50 65.1
IBM 100 70.44
>>>
```
This is really the whole idea on "mixins." The creator of a library
can provide a basic set of classes such as `TextTableFormatter`,
`CSVTableFormatter`, and so forth to start. Then, a collection of
add-on classes can be provided to make those classes behave in
different ways.
## (c) Making it Sane
Using mixins can be a useful tool for framework builders for reducing
the amount of code that needs to be written. However, forcing users
to remember how to properly compose classes and use multiple inheritance can
2023-07-17 17:41:16 +02:00
fry their brains. In [Exercise 3.5](ex3_5.md), you wrote a
2023-07-17 03:21:00 +02:00
function `create_formatter()` that made it easier to create a custom
formatter. Take that function and extend it to understand a few optional
arguments related to the mixin classes. For example:
```python
>>> from tableformat import create_formatter
>>> formatter = create_formatter('csv', column_formats=['"%s"','%d','%0.2f'])
>>> print_table(portfolio, ['name','shares','price'], formatter)
name,shares,price
"AA",100,32.20
"IBM",50,91.10
"CAT",150,83.44
"MSFT",200,51.23
"GE",95,40.37
"MSFT",50,65.10
"IBM",100,70.44
>>> formatter = create_formatter('text', upper_headers=True)
>>> print_table(portfolio, ['name','shares','price'], formatter)
NAME SHARES PRICE
---------- ---------- ----------
AA 100 32.2
IBM 50 91.1
CAT 150 83.44
MSFT 200 51.23
GE 95 40.37
MSFT 50 65.1
IBM 100 70.44
>>>
```
Under the covers the `create_formatter()` function will properly compose
the classes and return a proper `TableFormatter` instance.
\[ [Solution](soln3_8.md) | [Index](index.md) | [Exercise 3.7](ex3_7.md) | [Exercise 4.1](ex4_1.md) \]
----
`>>>` Advanced Python Mastery
`...` A course by [dabeaz](https://www.dabeaz.com)
`...` Copyright 2007-2023
![](https://i.creativecommons.org/l/by-sa/4.0/88x31.png). This work is licensed under a [Creative Commons Attribution-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-sa/4.0/)