\[ [Index](index.md) | [Exercise 7.5](ex7_5.md) | [Exercise 8.1](ex8_1.md) \] # Exercise 7.6 *Objectives:* - Metaclasses in action - Explode your brain *Files Modified:* `structure.py`, `validate.py` ## (a) The Final Frontier In [Exercise 7.3](ex7_3.md), we made it possible to define type-checked structures as follows: ```python 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: ```python # 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: ```python >>> 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: ```python # 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: ```python # 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 ``` ## (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. ```python >>> 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](soln7_6.md) | [Index](index.md) | [Exercise 7.5](ex7_5.md) | [Exercise 8.1](ex8_1.md) \] ---- `>>>` Advanced Python Mastery `...` A course by [dabeaz](https://www.dabeaz.com) `...` Copyright 2007-2023 . This work is licensed under a [Creative Commons Attribution-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-sa/4.0/)