5.3 KiB
[ Index | Exercise 7.5 | Exercise 8.1 ]
Exercise 7.6
Objectives:
- Metaclasses in action
- Explode your brain
Files Modified: structure.py
,
validate.py
(a) The Final Frontier
In link:ex7_3.html[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):
= String()
name = PositiveInteger()
shares = PositiveFloat()
price
@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):
__name__] = cls cls.validators[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.maps[0]
methods 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):
= String()
name = PositiveInteger()
shares = PositiveFloat()
price
@property
def cost(self):
return self.shares * self.price
def sell(self, nshares: PositiveInteger):
self.shares -= nshares
(b) 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('Data/portfolio.csv', Stock)
>>> portfolio
'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)]
[Stock(>>> from tableformat import create_formatter, print_table
>>> formatter = create_formatter('text')
>>> print_table(portfolio, ['name','shares','price'], formatter)
name shares price ---------- ---------- ----------
100 32.2
AA 50 91.1
IBM 150 83.44
CAT 200 51.23
MSFT 95 40.37
GE 50 65.1
MSFT 100 70.44
IBM >>>
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