\[ [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 ![](https://i.creativecommons.org/l/by-sa/4.0/88x31.png). This work is licensed under a [Creative Commons Attribution-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-sa/4.0/)