python-mastery/Exercises/ex9_4.md

194 lines
5.3 KiB
Markdown
Raw Normal View History

2023-07-17 03:21:00 +02:00
\[ [Index](index.md) | [Exercise 9.3](ex9_3.md) | []() \]
# Exercise 9.4
*Objectives:*
- Explore circular imports
- Dynamic module imports
In the last exercise, you split the `tableformat.py` file up into submodules.
The last part of the resulting `tableformat/formatter.py` file has turned into a mess of imports.
```python
# tableformat.py
...
class TableFormatter(ABC):
@abstractmethod
def headings(self, headers):
pass
@abstractmethod
def row(self, rowdata):
pass
from .formats.text import TextTableFormatter
from .formats.csv import CSVTableFormatter
from .formats.html import HTMLTableFormatter
...
def create_formatter(name, column_formats=None, upper_headers=False):
if name == 'text':
formatter_cls = TextTableFormatter
elif name == 'csv':
formatter_cls = CSVTableFormatter
elif name == 'html':
formatter_cls = HTMLTableFormatter
else:
raise RuntimeError('Unknown format %s' % name)
if column_formats:
class formatter_cls(ColumnFormatMixin, formatter_cls):
formats = column_formats
if upper_headers:
class formatter_cls(UpperHeadersMixin, formatter_cls):
pass
return formatter_cls()
```
The imports in the middle of the file are required because the `create_formatter()`
function needs them to find the appropriate classes. Really, the whole thing is a mess.
## (a) Circular Imports
Try moving the following import statements to the top of the `formatter.py` file:
```python
# formatter.py
from .formats.text import TextTableFormatter
from .formats.csv import CSVTableFormatter
from .formats.html import HTMLTableFormatter
class TableFormatter(ABC):
@abstractmethod
def headings(self, headers):
pass
@abstractmethod
def row(self, rowdata):
pass
...
```
Observe that nothing works anymore. Try running the `stock.py` program and
notice the error about `TableFormatter` not being defined. The order
of import statements matters and you can't just move the imports anywhere
you want.
Move the import statements back where they were. Sigh.
## (b) Subclass Registration
Try the following experiment and observe:
```python
>>> from structly.tableformat.formats.text import TextTableFormatter
>>> TextTableFormatter.__module__
'structly.tableformat.formats.text'
>>> TextTableFormatter.__module__.split('.')[-1]
'text'
>>>
```
Modify the `TableFormatter` base class by adding a dictionary and an
`__init_subclass__()` method:
```python
class TableFormatter(ABC):
_formats = { }
@classmethod
def __init_subclass__(cls):
name = cls.__module__.split('.')[-1]
TableFormatter._formats[name] = cls
@abstractmethod
def headings(self, headers):
pass
@abstractmethod
def row(self, rowdata):
pass
```
This makes the parent class track all of its subclasses. Check it out:
```python
>>> from structly.tableformat.formatter import TableFormatter
>>> TableFormatter._formats
{'text': <class 'structly.tableformat.formats.text.TextTableFormatter'>,
'csv': <class 'structly.tableformat.formats.csv.CSVTableFormatter'>,
'html': <class 'structly.tableformat.formats.html.HTMLTableFormatter'>}
>>>
```
Modify the `create_formatter()` function to look up the class in this dictionary
instead:
```python
def create_formatter(name, column_formats=None, upper_headers=False):
formatter_cls = TableFormatter._formats.get(name)
if not formatter_cls:
raise RuntimeError('Unknown format %s' % name)
if column_formats:
class formatter_cls(ColumnFormatMixin, formatter_cls):
formats = column_formats
if upper_headers:
class formatter_cls(UpperHeadersMixin, formatter_cls):
pass
return formatter_cls()
```
Run the `stock.py` program. Make sure it still works after you've made these changes.
Just a note that all of the import statements are still there. You've mainly
just cleaned up the code a bit and eliminated the hard-wired class names.
## (c) Dynamic Imports
You're now ready for the final frontier. Delete the following import statements
altogether:
```python
# formatter.py
...
from .formats.text import TextTableFormatter # DELETE
from .formats.csv import CSVTableFormatter # DELETE
from .formats.html import HTMLTableFormatter # DELETE
...
```
Run your `stock.py` code again--it should fail with an error. It knows nothing about the
text formatter. Fix it by adding this tiny fragment of code to `create_formatter()`:
```python
def create_formatter(name, column_formats=None, upper_headers=False):
if name not in TableFormatter._formats:
__import__(f'{__package__}.formats.{name}')
...
```
This code attempts a dynamic import of a formatter module if nothing is known about the
name. The import alone (if it works) will register the class with the `_formats`
dictionary and everything will just work. Magic!
Try running the `stock.py` code and make sure it works afterwards.
\[ [Solution](soln9_4.md) | [Index](index.md) | [Exercise 9.3](ex9_3.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/)