6.6 KiB
[ Index | Exercise 7.2 | Exercise 7.4 ]
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:
from validate import String, PositiveInteger, PositiveFloat
class Stock:
= String()
name = PositiveInteger()
shares = PositiveFloat()
price ...
Modify your Stock
class so that it includes the above
descriptors and now looks like this (see link:ex6_4.html[Exercise
6.4]):
# stock.py
from structure import Structure
from validate import String, PositiveInteger, PositiveFloat
class Stock(Structure):
= ('name', 'shares', 'price')
_fields = String()
name = PositiveInteger()
shares = PositiveFloat()
price
@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:
# 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)= [val.name for val in validators]
cls._fields 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):
= String()
name = PositiveInteger()
shares = PositiveFloat()
price
@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):
= String()
name = PositiveInteger()
shares = PositiveFloat()
price
@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:
# 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):
= String()
name = PositiveInteger()
shares = PositiveFloat()
price
@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:
# structure.py
class Structure:
= ()
_types
...@classmethod
def from_row(cls, row):
= [ func(val) for func, val in zip(cls._types, row) ]
rowdata 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
'GOOG', 100, 490.1)
Stock(>>> 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:
# stock.py
from structure import Structure
from validate import String, PositiveInteger, PositiveFloat
class Stock(Structure):
= String()
name = PositiveInteger()
shares = PositiveFloat()
price
@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
>= 0
nshares: must be >>>
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