A basic object relational design
In this article we will explore metaclasses as a tool to link up Python classes and database tables.
Based in part on the information in illustrated in this
Python article.
Metaclasses are a daunting concept at first, yet it is worthwhile to understand the idiom because in some circumstances the are vital in getting hard jobs done. In this short article we'll look into a rough draft of some code that creates database tables at the same time a class is created and alters the class definition in such a way that accessing instance variables will result in calls to some database engine.
The first thing to understand is that Python metaclasses are not magic. Everytime you define a new class you already refer to the built-in metaclass type
. Metaclasses are subclasses of type
and regular classes are instances of a metaclass. This may sound weird but remember that everything in Python is an object, even a class definition. And because every object is an instance of some class, this is just a logical extension of a general concept.
If you want your class definition to be instanced by a different metaclass than type
you will have to use the metaclass
parameter in your class definition:
class MyClass(metaclass=MyMetaClass): ...
Your metaclass must be a subclass from Python's built-in metaclass type
and should at least provide a __new__()
method:
class MyMetaClass(type): def __new__(metaclass, classname, bases, classdict): ... do stuff to classDict ... return type.__new__(metaclass, classname, bases, classdict)
This __new__()
method should return a new class, for example by passing its attribute to the __new__()
method of type
like we do in the example above. The real power is hidden in the arguments passed to the __new__()
method: besides the name of the class we are building and the base classes it will be a subclass of, we also have access to the class dictionary.
The class dictionary holds all class attributes, including class variables and methods (both class methods and instance methods). The beauty is that we can check and/or alter the contents of this class dictionary before we actually create a class. This could be used for example to check whether the class has some mandatory method definitions or overrides specific methods in its bases, something that is used to implement abstract base classes in Python.
Another application of metaclasses is to bridge domains. In the example below there are two domains: the classes and instances of those classes in Python's runtime environment and the tables and records in a relational database stored on disk. If we would like to map those classes to database tables metaclasses provide us with some excellent tooling because they allow us to act upon the class definition before the class is actually made to exist.
This means that we when we define a class our metaclass can check whether there is already a suitable table defined in the database and that class variables are mapped to columns in this table. It is also possible to change class variables into properties in such a way that accessing these properties from instances will result in proper sql statements issues to a database engine.
This may sound a bit abstract, so just take a look at the code below. It will not really interact with a database but just print the sql it would have used. The comments should explain what is happening in a fairly detailed manner:
from functools import partial from random import randint class Attribute: """ Attribute makes it possible to distinguish class variables that should be backed by a columns definition for a table. """ def __init__(self,constraints=''): self.constraints=constraints class DBbackend(type): """ A metaclass that will create a database table that contains columns for each class variable that is an instance of Attribute. It also replaces these class variables with poperties that will retrieve or update the database value when the attribute is accessed. """ def __new__(meta, classname, bases, classDict): attributes = {} # create a suitable table definition. Although we # do not do a complete implementation here, we have # sqlite in mind, so no explicit types. for attr in classDict: if issubclass(Attribute,classDict[attr].__class__): attributes[attr]=classDict[attr] sql = 'create table if not exists %s ( %s )' % ( classname, ",".join(['id integer primary key autoincrement'] +[ name+" "+a.constraints for name,a in attributes.items()])) print(sql) # we alter the __init__() method to create a database record def create(self,**kw): self.id=randint(1,1000000) # just for illustration, normally handled by autoincrement in the db sql='insert into %s (%s) values (%s) [%s]' % (self.__class__.__name__, ",".join(kw.keys()), ",".join(['?']*len(kw.keys())), ",".join(kw.values())) print(sql) classDict['__init__']=create # functions that retrieve/update a column in a database table def get(self,name): print('select %s from %s where id = ? [%s]' % (name, classname,str(self.id))) def set(self,value,name): print('update %s set %s=? [%s] where id = ? [%s]' % (classname, name,str(value),str(self.id))) # change each class var that holds an Attribute object to a # property that get/sets the appropriate column for attr in attributes: fget = partial(get,name=attr) fset = partial(set,name=attr) classDict[attr]=property(fget,fset) return type.__new__(meta, classname, bases, classDict) if __name__ == "__main__": # example, create a class with three attributes/database columns class Car(metaclass=DBbackend): make = Attribute() model= Attribute() license=Attribute('unique') # create an instance mycar = Car(make='Volvo', model='C30', license='1-abc-23') # retrieve various attributes model = mycar.model make = mycar.make lic = mycar.license # set an attribute mycar.model='S40'