Learn About Class Decorators

PythonPythonBeginner
Practice Now

This tutorial is from open-source community. Access the source code

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.

Descriptors Revisited

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.

âœĻ Check Solution and Practice

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)
    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
âœĻ Check Solution and Practice

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):
    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.

âœĻ Check Solution and Practice

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):
        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)
>>>
âœĻ Check Solution and Practice

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):
    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.

âœĻ Check Solution and Practice

Summary

Congratulations! You have completed the Learn About Class Decorators lab. You can practice more labs in LabEx to improve your skills.

Other Python Tutorials you may like