[ Index | Exercise 7.5 | Exercise 8.1 ]
Objectives:
- Metaclasses in action
- Explode your brain
Files Modified: structure.py
, validate.py
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
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('Data/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.
[ Solution | Index | Exercise 7.5 | Exercise 8.1 ]
>>>
Advanced Python Mastery
...
A course by dabeaz
...
Copyright 2007-2023
. This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License