2023-07-17 03:21:00 +02:00
\[ [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
2023-07-17 17:41:16 +02:00
In [Exercise 4.3 ](ex4_3.md ) you defined some descriptors that
2023-07-17 03:21:00 +02:00
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
2023-07-17 17:41:16 +02:00
and now looks like this (see [Exercise 6.4 ](ex6_4.md )):
2023-07-17 03:21:00 +02:00
```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/ )