[ Index | Exercise 7.2 | Exercise 7.4 ]
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.
In Exercise 4.3 you defined some descriptors that allowed a user to define classes with type-checked attributes like this:
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):
# 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.
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:
# 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:
# 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:
# 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
Having to specify the class decorator itself is kind of annoying. Modify the
Structure
class with the following __init_subclass__()
method:
# 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!
# 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.
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:
# 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:
>>> 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)
>>>
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:
# 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.
>>> 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 | Index | Exercise 7.2 | Exercise 7.4 ]
>>>
Advanced Python Mastery
...
A course by dabeaz
...
Copyright 2007-2023
. This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License