262 lines
6.6 KiB
Markdown
262 lines
6.6 KiB
Markdown
\[ [Index](index.md) | [Exercise 7.2](ex7_2.md) | [Exercise 7.4](ex7_4.md) \]
|
|
|
|
# Exercise 7.3
|
|
|
|
*Objectives:*
|
|
|
|
- Learn about class decorators
|
|
- Descriptors revisited
|
|
|
|
*Files Modified:* `validate.py`, `structure.py`
|
|
|
|
This exercise is going to pull together a bunch of topics we've
|
|
developed over the last few days. Hang on to your hat.
|
|
|
|
## (a) Descriptors Revisited
|
|
|
|
In [Exercise 4.3](ex4_3.md) you defined some descriptors that
|
|
allowed a user to define classes with type-checked attributes like
|
|
this:
|
|
|
|
```python
|
|
from validate import String, PositiveInteger, PositiveFloat
|
|
|
|
class Stock:
|
|
name = String()
|
|
shares = PositiveInteger()
|
|
price = PositiveFloat()
|
|
...
|
|
```
|
|
|
|
Modify your `Stock` class so that it includes the above descriptors
|
|
and now looks like this (see [Exercise 6.4](ex6_4.md)):
|
|
|
|
```python
|
|
# stock.py
|
|
|
|
from structure import Structure
|
|
from validate import String, PositiveInteger, PositiveFloat
|
|
|
|
class Stock(Structure):
|
|
_fields = ('name', 'shares', 'price')
|
|
name = String()
|
|
shares = PositiveInteger()
|
|
price = PositiveFloat()
|
|
|
|
@property
|
|
def cost(self):
|
|
return self.shares * self.price
|
|
|
|
def sell(self, nshares):
|
|
self.shares -= nshares
|
|
|
|
Stock.create_init()
|
|
```
|
|
|
|
Run the unit tests in `teststock.py`. You should see a significant
|
|
number of tests passing with the addition of type checking.
|
|
Excellent.
|
|
|
|
## (b) Using Class Decorators to Fill in Details
|
|
|
|
An annoying aspect of the above code is there are extra details such as
|
|
`_fields` variable and the final step of `Stock.create_init()`. A lot
|
|
of this could be packaged into a class decorator instead.
|
|
|
|
In the file `structure.py`, make a class decorator `@validate_attributes`
|
|
that examines the class body for instances of Validators and fills in
|
|
the `_fields` variable. For example:
|
|
|
|
```python
|
|
# structure.py
|
|
|
|
from validate import Validator
|
|
|
|
def validate_attributes(cls):
|
|
validators = []
|
|
for name, val in vars(cls).items():
|
|
if isinstance(val, Validator):
|
|
validators.append(val)
|
|
cls._fields = [val.name for val in validators]
|
|
return cls
|
|
```
|
|
|
|
This code relies on the fact that class dictionaries are ordered
|
|
starting in Python 3.6. Thus, it will encounter the different
|
|
`Validator` descriptors in the order that they're listed. Using this
|
|
order, you can then fill in the `_fields` variable. This allows
|
|
you to write code like this:
|
|
|
|
```python
|
|
# stock.py
|
|
|
|
from structure import Structure, validate_attributes
|
|
from validate import String, PositiveInteger, PositiveFloat
|
|
|
|
@validate_attributes
|
|
class Stock(Structure):
|
|
name = String()
|
|
shares = PositiveInteger()
|
|
price = PositiveFloat()
|
|
|
|
@property
|
|
def cost(self):
|
|
return self.shares * self.price
|
|
|
|
def sell(self, nshares):
|
|
self.shares -= nshares
|
|
|
|
Stock.create_init()
|
|
```
|
|
|
|
Once you've got this working, modify the `@validate_attributes`
|
|
decorator to additionally perform the final step of calling
|
|
`Stock.create_init()`. This will reduce the class to the
|
|
following:
|
|
|
|
```python
|
|
# stock.py
|
|
|
|
from structure import Structure, validate_attributes
|
|
from validate import String, PositiveInteger, PositiveFloat
|
|
|
|
@validate_attributes
|
|
class Stock(Structure):
|
|
name = String()
|
|
shares = PositiveInteger()
|
|
price = PositiveFloat()
|
|
|
|
@property
|
|
def cost(self):
|
|
return self.shares * self.price
|
|
|
|
def sell(self, nshares):
|
|
self.shares -= nshares
|
|
```
|
|
|
|
## (c) Applying Decorators via Inheritance
|
|
|
|
Having to specify the class decorator itself is kind of annoying. Modify the
|
|
`Structure` class with the following `__init_subclass__()` method:
|
|
|
|
```python
|
|
# structure.py
|
|
|
|
class Structure:
|
|
...
|
|
@classmethod
|
|
def __init_subclass__(cls):
|
|
validate_attributes(cls)
|
|
```
|
|
|
|
Once you've made this change, you should be able to drop the decorator entirely and
|
|
solely rely on inheritance. It's inheritance plus some hidden magic!
|
|
|
|
```python
|
|
# stock.py
|
|
|
|
from structure import Structure
|
|
from validate import String, PositiveInteger, PositiveFloat
|
|
|
|
class Stock(Structure):
|
|
name = String()
|
|
shares = PositiveInteger()
|
|
price = PositiveFloat()
|
|
|
|
@property
|
|
def cost(self):
|
|
return self.shares * self.price
|
|
|
|
def sell(self, nshares):
|
|
self.shares -= nshares
|
|
```
|
|
|
|
Now, the code is really starting to go places. In fact, it almost
|
|
looks normal. Let's keep pushing it.
|
|
|
|
## (d) Row Conversion
|
|
|
|
One missing feature from the `Structure` class is a `from_row()` method that
|
|
allows it to work with earlier CSV reading code. Let's fix that. Give the
|
|
`Structure` class a `_types` class variable and the following class method:
|
|
|
|
```python
|
|
# structure.py
|
|
|
|
class Structure:
|
|
_types = ()
|
|
...
|
|
@classmethod
|
|
def from_row(cls, row):
|
|
rowdata = [ func(val) for func, val in zip(cls._types, row) ]
|
|
return cls(*rowdata)
|
|
...
|
|
```
|
|
|
|
Modify the `@validate_attributes` decorator so that it examines the
|
|
various validators for an `expected_type` attribute and uses it to
|
|
fill in the `_types` variable above.
|
|
|
|
Once you've done this, you should be able to do things like this:
|
|
|
|
```python
|
|
>>> s = Stock.from_row(['GOOG', '100', '490.1'])
|
|
>>> s
|
|
Stock('GOOG', 100, 490.1)
|
|
>>> import reader
|
|
>>> port = reader.read_csv_as_instances('Data/portfolio.csv', Stock)
|
|
>>>
|
|
```
|
|
|
|
## (e) Method Argument Checking
|
|
|
|
Remember that `@validated` decorator you wrote in the last part?
|
|
Let's modify the `@validate_attributes` decorator so that any method
|
|
in the class with annotations gets wrapped by `@validated`
|
|
automatically. This allows you to put enforced annotations on methods
|
|
such as the `sell()` method:
|
|
|
|
```python
|
|
# stock.py
|
|
|
|
from structure import Structure
|
|
from validate import String, PositiveInteger, PositiveFloat
|
|
|
|
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
|
|
```
|
|
|
|
You'll find that `sell()` now enforces the argument.
|
|
|
|
```python
|
|
>>> s = Stock('GOOG', 100, 490.1)
|
|
>>> s.sell(25)
|
|
>>> s.sell(-25)
|
|
Traceback (most recent call last):
|
|
...
|
|
TypeError: Bad Arguments
|
|
nshares: must be >= 0
|
|
>>>
|
|
```
|
|
|
|
Yes, this starting to get very interesting now. The combination of a class decorator and
|
|
inheritance is a powerful force.
|
|
|
|
\[ [Solution](soln7_3.md) | [Index](index.md) | [Exercise 7.2](ex7_2.md) | [Exercise 7.4](ex7_4.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/)
|