Introduction
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.
Objectives:
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('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.
Congratulations! You have completed the Learn About Class Decorators lab. You can practice more labs in LabEx to improve your skills.