python-mastery/Exercises/ex7_6.md

166 lines
5.3 KiB
Markdown
Raw Normal View History

2023-07-17 03:21:00 +02:00
\[ [Index](index.md) | [Exercise 7.5](ex7_5.md) | [Exercise 8.1](ex8_1.md) \]
# Exercise 7.6
*Objectives:*
- Metaclasses in action
- Explode your brain
*Files Modified:* `structure.py`, `validate.py`
## (a) The Final Frontier
In link:ex7_3.html[Exercise 7.3], we made it possible to define type-checked structures as follows:
```python
from validate import String, PositiveInteger, PositiveFloat
from structure import Structure
class Stock(Structure):
name = String()
shares = PositiveInteger()
price = PositiveFloat()
@property
def cost(self):
return self.shares * self.price
def sell(self, nshares: PositiveInteger):
self.shares -= nshares
```
There are a lot of things going on under the covers. However, one annoyance
concerns all of those type-name imports at the top (e.g., `String`, `PositiveInteger`, etc.).
That's just the kind of thing that might lead to a `from validate import *` statement.
One interesting thing about a metaclass is that it can be used to control
the process by which a class gets defined. This includes managing the
environment of a class definition itself. Let's tackle those imports.
The first step in managing all of the validator names is to collect
them. Go to the file `validate.py` and modify the `Validator` base
class with this extra bit of code involving `__init_subclass__()` again:
```python
# validate.py
class Validator:
...
# Collect all derived classes into a dict
validators = { }
@classmethod
def __init_subclass__(cls):
cls.validators[cls.__name__] = cls
```
That's not much, but it's creating a little namespace of all of the `Validator`
subclasses. Take a look at it:
```python
>>> from validate import Validator
>>> Validator.validators
{'Float': <class 'validate.Float'>,
'Integer': <class 'validate.Integer'>,
'NonEmpty': <class 'validate.NonEmpty'>,
'NonEmptyString': <class 'validate.NonEmptyString'>,
'Positive': <class 'validate.Positive'>,
'PositiveFloat': <class 'validate.PositiveFloat'>,
'PositiveInteger': <class 'validate.PositiveInteger'>,
'String': <class 'validate.String'>,
'Typed': <class 'validate.Typed'>}
>>>
```
Now that you've done that, let's inject this namespace into namespace
of classes defined from `Structure`. Define the following metaclass:
```python
# structure.py
...
from validate import Validator
from collections import ChainMap
class StructureMeta(type):
@classmethod
def __prepare__(meta, clsname, bases):
return ChainMap({}, Validator.validators)
@staticmethod
def __new__(meta, name, bases, methods):
methods = methods.maps[0]
return super().__new__(meta, name, bases, methods)
class Structure(metaclass=StructureMeta):
...
```
In this code, the `__prepare__()` method is making a special `ChainMap` mapping that consists
of an empty dictionary and a dictionary of all of the defined validators. The empty dictionary
that's listed first is going to collect all of the definitions made inside the class body.
The `Validator.validators` dictionary is going to make all of the type definitions available
to for use as descriptors or argument type annotations.
The `__new__()` method discards extra the validator dictionary and
passes the remaining definitions onto the type constructor. It's
ingenious, but it lets you drop the annoying imports:
```python
# stock.py
from structure import Structure
class Stock(Structure):
name = String()
shares = PositiveInteger()
price = PositiveFloat()
@property
def cost(self):
return self.shares * self.price
def sell(self, nshares: PositiveInteger):
self.shares -= nshares
```
## (b) Stare in Amazement
Try running your `teststock.py` unit tests across this new file. Most of them should be
passing now. For kicks, try your `Stock` class with some of the earlier code
for tableformatting and reading data. It should all work.
```python
>>> from stock import Stock
>>> from reader import read_csv_as_instances
>>> portfolio = read_csv_as_instances('Data/portfolio.csv', Stock)
>>> portfolio
[Stock('AA',100,32.2), Stock('IBM',50,91.1), Stock('CAT',150,83.44), Stock('MSFT',200,51.23), Stock('GE',95,40.37), Stock('MSFT',50,65.1), Stock('IBM',100,70.44)]
>>> from tableformat import create_formatter, print_table
>>> formatter = create_formatter('text')
>>> 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
>>>
```
Again, marvel at the final `stock.py` file and observe how clean the
code looks. Just try not think about everything that is happening
under the hood with the `Structure` base class.
\[ [Solution](soln7_6.md) | [Index](index.md) | [Exercise 7.5](ex7_5.md) | [Exercise 8.1](ex8_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/)