Metaclasses in Action

PythonPythonBeginner
Practice Now

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

Introduction

Objectives:

  • Metaclasses in action
  • Explode your brain

Files Modified: structure.py, validate.py

The Final Frontier

In Exercise 7.3, we made it possible to define type-checked structures as follows:

from validate import String, PositiveInteger, PositiveFloat
from structure import Structure

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

There are a lot of things going on under the covers. However, one annoyance concerns all of those type-name imports at the top (e.g., String, PositiveInteger, etc.). That's just the kind of thing that might lead to a from validate import * statement. One interesting thing about a metaclass is that it can be used to control the process by which a class gets defined. This includes managing the environment of a class definition itself. Let's tackle those imports.

The first step in managing all of the validator names is to collect them. Go to the file validate.py and modify the Validator base class with this extra bit of code involving __init_subclass__() again:

## validate.py

class Validator:
    ...

    ## Collect all derived classes into a dict
    validators = { }
    @classmethod
    def __init_subclass__(cls):
        cls.validators[cls.__name__] = cls

That's not much, but it's creating a little namespace of all of the Validator subclasses. Take a look at it:

>>> from validate import Validator
>>> Validator.validators
{'Float': <class 'validate.Float'>,
 'Integer': <class 'validate.Integer'>,
 'NonEmpty': <class 'validate.NonEmpty'>,
 'NonEmptyString': <class 'validate.NonEmptyString'>,
 'Positive': <class 'validate.Positive'>,
 'PositiveFloat': <class 'validate.PositiveFloat'>,
 'PositiveInteger': <class 'validate.PositiveInteger'>,
 'String': <class 'validate.String'>,
 'Typed': <class 'validate.Typed'>}
>>>

Now that you've done that, let's inject this namespace into namespace of classes defined from Structure. Define the following metaclass:

## structure.py
...

from validate import Validator
from collections import ChainMap

class StructureMeta(type):
    @classmethod
    def __prepare__(meta, clsname, bases):
        return ChainMap({}, Validator.validators)

    @staticmethod
    def __new__(meta, name, bases, methods):
        methods = methods.maps[0]
        return super().__new__(meta, name, bases, methods)

class Structure(metaclass=StructureMeta):
    ...

In this code, the __prepare__() method is making a special ChainMap mapping that consists of an empty dictionary and a dictionary of all of the defined validators. The empty dictionary that's listed first is going to collect all of the definitions made inside the class body. The Validator.validators dictionary is going to make all of the type definitions available to for use as descriptors or argument type annotations.

The __new__() method discards extra the validator dictionary and passes the remaining definitions onto the type constructor. It's ingenious, but it lets you drop the annoying imports:

## stock.py

from structure import Structure

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

Stare in Amazement

Try running your teststock.py unit tests across this new file. Most of them should be passing now. For kicks, try your Stock class with some of the earlier code for tableformatting and reading data. It should all work.

>>> from stock import Stock
>>> from reader import read_csv_as_instances
>>> portfolio = read_csv_as_instances('portfolio.csv', Stock)
>>> portfolio
[Stock('AA',100,32.2), Stock('IBM',50,91.1), Stock('CAT',150,83.44), Stock('MSFT',200,51.23), Stock('GE',95,40.37), Stock('MSFT',50,65.1), Stock('IBM',100,70.44)]
>>> from tableformat import create_formatter, print_table
>>> formatter = create_formatter('text')
>>> print_table(portfolio, ['name','shares','price'], formatter)
      name     shares      price
---------- ---------- ----------
        AA        100       32.2
       IBM         50       91.1
       CAT        150      83.44
      MSFT        200      51.23
        GE         95      40.37
      MSFT         50       65.1
       IBM        100      70.44
>>>

Again, marvel at the final stock.py file and observe how clean the code looks. Just try not think about everything that is happening under the hood with the Structure base class.

Summary

Congratulations! You have completed the Metaclasses in Action lab. You can practice more labs in LabEx to improve your skills.

Other Python Tutorials you may like