The Explorer

Metaclasses in Python 3.0 [2 of 2]

by Michele Simionato

August 9, 2008



Classes are used to model sets of objects; in the same sense, metaclasses are used to model sets of classes. In order to show the power of metaclasses, we will define a set of record classes, all subclasses of a common mother class Record and all instances of the same metaclass MetaRecord , with a sum operator. In mathematical term our set of classes will be a monoid, i.e. a set with an associative composition law (the composition operator will be denoted with + ) and an identity, the base class Record . Since Python tuples are already a monoid - the sum of two tuples is a tuple and the empty tuple works as an identity element - it makes sense to define our set of classes as a set of tuples. The difference between ordinary tuples and the records we will define is that records have the concept of type: every given record field has a given type. Since Python is dynamically typed the types will be specified in terms of casting functions, taking in input one or more types (for instance integers and/or strings) and returning a fixed type in output - or a TypeError if the if the input type is not acceptable. We will consider the following casting functions for records of kind Book :

def varchar(n): """varchar(n) converts an object into a string with less than n characters or raises a TypeError""" def check(x): s = str(x) if len(s) > n: raise TypeError('Entered a string longer than %d chars' % n) return s check.__name__ = 'varchar(%d)' % n return check

def date(x): "Takes a string (or date) and converts it into a date" if isinstance(x, datetime.date): x = x.isoformat()[:10] return datetime.date(*map(int, x.split('-')))

def score(x): "Takes a string and converts it into an integer in the range 1-5" if set(x) != {'*'} or len(x) > 5: raise TypeError('%r is not a valid score!' % x) return len(x)

varchar(N) makes sure that the string input is shorter than N characters; for instance

>>> varchar(128)('a'*129) Traceback (most recent call last): ... TypeError: Entered a string longer than 128 chars

date makes sure that the string in input is a data in ISO format; score converts a string with or or more stars into an integer number: the idea is that a book has a score in stars, for one to five.

>>> score('***') 3

>>> score('') Traceback (most recent call last): ... TypeError: '' is not a valid score!

We will define records like the following, where Record is a subclass of tuple enhanced with a suitable metaclass:

class Book(Record): title_type = varchar(128) author_type = varchar(64) class PubDate(Record): date_type = date class Score(Record): score_type = score

On these records it will be possible to define a sum operator taking two or more classes and returning a new class:

>>> Book + PubDate + Score <class Book+PubDate+Score title:varchar(128), author:varchar(64), date:date, score:score>

It will be possible to verify the associativity:

>>> (Book + PubDate) + Score == Book + (PubDate + Score) True

and the existence of the identity element:

>>> Book + Record == Book True

>>> Record + Book == Book True

These properties at the class level correspond to analogous properties at the instance level. Consider for instance the null record

>>> null = Record() >>> null <Record >

a record ot type Book

>>> b = Book('Putting Metaclasses to Work', 'Ira Forman') >>> b <Book title=Putting Metaclasses to Work, author=Ira Forman>

and a record of type PubDate :

>>> d = PubDate('1998-10-01')

You can see that null is an identity:

>>> b + null == null + b == b True

Here is an example of sum of nontrivial records:

>>> s = b + d >>> s <Book+PubDate title=Putting Metaclasses to Work, author=Ira Forman, date=1998-10-01>

You can access the fields by name

>>> s.title, s.author, s.date ('Putting Metaclasses to Work', 'Ira Forman', datetime.date(1998, 10, 1))

or by index

>>> s[0], s[1], s[2] ('Putting Metaclasses to Work', 'Ira Forman', datetime.date(1998, 10, 1))