Initial commit
This commit is contained in:
261
Exercises/ex7_3.md
Normal file
261
Exercises/ex7_3.md
Normal file
@@ -0,0 +1,261 @@
|
||||
\[ [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 link:ex4_3.html[Exercise 4.3] 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 link:ex6_4.html[Exercise 6.4]):
|
||||
|
||||
```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/)
|
||||
Reference in New Issue
Block a user