\[ [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 [Exercise 4.3](ex4_3.md) 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 [Exercise 6.4](ex6_4.md)):

```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/)
