Be careful with metaclasses
May 13th 2009 23:40:14
Python metaclasses can be really intense. SQLObject's declarative metaclass base, popularized by Django's ORM, has been as useful a DSL as the python world has had in a long time. But please be careful when using metaclasses in a way that makes it difficult to extend via subclassing!
I've been playing around with sqlalchemy and Elixir lately and other than the unfamiliarity I kinda like what I see. However, coming from the Django world where all the rough edges are smoothed out, I felt inclined to clean up the API of some things to make it a bit easier on myself. In particular, I noticed that I got a warning when passing a string to a field specified as unicode:
/var/lib/python-support/python2.5/sqlalchemy/engine/default.py:241: SAWarning: Unicode type received non-unicode bind param value 'Dream'
My first impression was: Nice! I like it when libraries keep me honest, and the closer I keep an eye on encoding issues the easier it will be to avoid decode errors in the future. However, this is just a little toy/exploration app, and although I don't want to kill the warnings I would like it if I could lessen the type-coercion burden on business logic. Elixir's main ORM object, Entity, initializes mostly through another function called set, so I thought I could just override that and be on my way...
Except that Entity actually allows you to declare table base classes in order to share the same partial table definition across many models. This is actually really clean, pythonic, and wonderful, except in my case it was also wrong. After playing with it a bit, I realized that my version of Elixir did not separate the EntityBase meat from a class poisoned by EntityMeta. The solution at this point, of course, is unfortunately a monkey patch. Here it is, untested in Elixir 0.7.x:
import elixir from elixir import * unicode_test = lambda col: isinstance(col.type, (Unicode, UnicodeText)) def _set(self, **kwargs): unicode_fields = (col.name for col in self.c if unicode_test(col)) for name in unicode_fields: if name in kwargs and isinstance(kwargs[name], str): kwargs[name] = kwargs[name].encode('latin-1').decode('utf-8') for key,val in kwargs.iteritems(): setattr(self, key, val) if elixir.__version__.startswith('0.7'): # if we are in 0.7, we can do this nicely... from elixir.entity import EntityMeta, EntityBase class SmartEntity(EntityBase): __metaclass__ = EntityMeta def set(self, **kwargs): _set(self, **kwargs) else: SmartEntity = Entity SmartEntity.set = _set
The moral of this story? What Elixir did in 0.7 is good! If you have some magic metaclass that is intrusive (like a declarative metaclass that performs some backend central registry magic), you should separate the overlay logic from the 'inheritance' class to allow other people to extend or tweak your code: letting people tweak your classes is pythonic!

public domain
comments