Initial commit
This commit is contained in:
249
Exercises/ex3_8.md
Normal file
249
Exercises/ex3_8.md
Normal file
@@ -0,0 +1,249 @@
|
||||
\[ [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
|
||||
|
||||
If you go all the way back to link:ex3_1.txt[Exercise 3.1], you
|
||||
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
|
||||
fry their brains. In link:ex3_5.html[Exercise 3.5], you wrote a
|
||||
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
|
||||
|
||||
. This work is licensed under a [Creative Commons Attribution-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-sa/4.0/)
|
||||
Reference in New Issue
Block a user