Cross-Python metaclasses
Using a class decorator for applying a metaclass in both Python 2 and 3
When you want to create a class including a metaclass, making it compatible with both Python 2 and 3 can be a little tricky.
The excellent six
library provides you with a six.with_metaclass()
factory function that’ll generate a base class for you from a given metaclass:
from six import with_metaclass
class Meta(type):
pass
class Base(object):
pass
class MyClass(with_metaclass(Meta, Base)):
pass
The basic trick is that you can call any metaclass to produce a class for you, given a name, a sequence of baseclasses and the class body. six
produces a new, intermediary base class for you:
>>> type(MyClass)
<class '__main__.Meta'>
>>> MyClass.__mro__
(<class '__main__.MyClass'>, <class 'six.NewBase'>, <class '__main__.Base'>, <type 'object'>)
This can complicate your code as for some usecases you now have to account for the extra six.NewBase
baseclass present.
Rather than creating a base class, I’ve come up with a class decorator that replaces any class with one produced from the metaclass, instead:
def with_metaclass(mcls):
def decorator(cls):
body = vars(cls).copy()
# clean out class body
body.pop('__dict__', None)
body.pop('__weakref__', None)
return mcls(cls.__name__, cls.__bases__, body)
return decorator
which you’d use as:
class Meta(type):
pass
class Base(object):
pass
@with_metaclass(Meta)
class MyClass(Base):
pass
which results in a cleaner MRO:
>>> type(MyClass)
<class '__main__.Meta'>
>>> MyClass.__mro__
(<class '__main__.MyClass'>, <class '__main__.Base'>, <type 'object'>)
Update
As it turns out, Jason Coombs took Guido’s time machine and added the same functionality to the six
library last summer. Not only that, he included support for classes with __slots__
in his version. Thanks to Mikhail Korobov for pointing this out.
The six
decorator is called @six.add_metaclass()
:
@six.add_metaclass(Meta)
class MyClass(Base):
pass
Leave a comment