-- recipe: 204197 #34 -- title: Solving Metaclass Conflicts -- credits: Michele Simionato (mis6@pitt.edu) -- comments: +1, hard but crucial, check Eby and Mertz are also credited -- SOLVING THE METACLASS CONFLICT -- problem: You need to need to multiply inherit from several classes that may come from several metaclasses; therefore, you need to generate automatically a custom metaclass to solve any possible metaclass conflicts. -- solution -- p Given a set of metaclasses, adding a further metaclass m to the mix will give no problem if if m is already a superclass of any of them. In particular, there is no problem if m is type, since type is the superclass of all metaclasses, like object is the superclass of all classes. Thus, we first need this easy function: -- code def _problem_m(m, metas): if m is type: return False for M in metas: if issubclass(M, m): return False return True -- !code Given a set of base classes as well as a set of existing metaclasses, then, we can determine the set of metaclasses of those base classes' that need to be added as they might otherwise give conflicts. Since order of inheritance matters in Python, we need to return a sequence of such metas; for convenience, we build and return a list of them. -- code def _metas_to_add(bases, metas): metas = list(metas) must_add = [] for b in bases: m = type(b) if _problem_m(m, metas): must_add.append(m) metas.append(m) return must_add -- !code We now have the tools to obtain (and generate, if needed) a suitable metaclass, given a set of base classes and a set of metaclasses to inject explicitly. Since, again, order of inheritance matters in Python, we also need a boolean parameter to indicate whether the new or injected metaclasses get priority, that is, go to the front of the sequence of metaclasses. It's also important to avoid generating more than one metaclass to solve the same potential conflicts, so we will also keep a memoization dictionary to ensure that: -- code noconflict_metaclass_for_metaclasses_memo = {} def _obtain_noconflict_metaclass(bases, injected_metas, injected_priority): must_add = tuple(_metas_to_add(bases, injected_metas)) # make the tuple of all needed metaclasses in specified priority order if injected_priority: all_metas = must_add + tuple(injected_metas) else: all_metas = tuple(injected_metas) + must_add # return existing confict-solving meta if any if all_metas in noconflict_metaclass_for_metaclasses_memo: return noconflict_metaclass_for_metaclasses_memo[all_metas] # nope: compute, memoize and return needed conflict-solving meta if not all_metas: # wee, a trivial case, happy us meta = type elif len(all_metas) == 1: # another trivial case meta = all_metas[0] else: # nontrivial, gotta work... metaname = '_' + ''.join([m.__name__ for m in all_metas]) meta = classmaker()(metaname, all_metas, {}) noconflict_metaclass_for_metaclasses_memo[all_metas] = meta return meta -- !code If you're reading this code carefully, you will have noticed towards the end of this function _obtain_supermetaclass the appearance of a name that is yet unknown, specifically classmaker, called without arguments. You may have recognized by the pattern of the code where this new name is used that we are generating a class (specifically, a conflict-resolving metaclass that inherits from all needed metas) by calling something and passing as arguments the name, bases and dictionary (here, an empty dictionary). So, why don't we just call the type built-in? Answer: because metaclasses could have custom metaclasses, too — and so on, if not ad infinitum, at least for more than one metalevel, until we do get to the base class of all metaclasses, type. So, we need mutual recursion between the function we just wrote, to obtain (possibly build) a metaclass to solve conflicts, and the factory-function classmaker that uses this conflict-solving metaclass appropriately for class-building purposes. Specifically, classmaker needs to be a closure, and here it is: -- code def classmaker(*metas, **options): injected_priority = options.pop('injected_priority', True) if options: raise TypeError, 'ignored options: %r' % options def make_class(name, bases, adict): metaclass = _obtain_noconflict_metaclass(bases, metas, injected_priority) return metaclass(name, bases, adict) return make_class -- !code In all of our code outside of this noconflict.py module, we will only use noconflict.classmaker, calling it with metaclasses we want to inject, and optionally the priority-indicator flag, to obtain a callable that we can then use just like a metaclass to build new class objects given names, bases and dictionary, but with the assurance that metatype conflicts cannot occur. Phew. Now that was worth it, wasn't it?! -- discuss: -- p Here is the simplest case where we can have a metatype confict: multiply inheriting from two classes with independent metaclasses. In a pedagogically simplified toy-level examples, that could be, say: -- code >>> class Meta_A(type): pass ... >>> class Meta_B(type): pass ... >>> class A: __metaclass__ = Meta_A ... >>> class B: __metaclass__ = Meta_B ... >>> class C(A, B): pass Traceback (most recent call last): File "", line 1, in ? TypeError: Error when calling the metaclass bases metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases >>> -- !code A class normally inherits its metaclass from its bases, but when the bases have distinct metaclasses, the metatype constraint that Python expresses so tersely in this error message applies. So, we need to build a new metaclass, say Meta_C, which inherits from both Meta_A and Meta_B. For a demonstration, see the book that's rightly considered the Bible of metaclasses, Putting Metaclasses to Work: A New Dimension in Object-Oriented Programming, by Ira R. Forman and Scott H. Danforth (Addison Wesley, 1999). -- p Python does not do magic: it does not automatically create Meta_C. Rather, it raises a TypeError to ensure the programmer is aware of the problem. In simple cases, the programmer can solve the metatype conflict by hand, as follows: -- code >>> class Meta_C(Meta_A, Meta_B): pass >>> class C(A, B): __metaclass__ = Meta_C -- !code In this case, everything works smoothly. -- p The key point of this recipe is to show an automatic way to resolve metatype conflicts, rather than having to do it by hand every time. Having saved all the code from this recipe's Solution into module noconflict.py somewhere along your Python's sys.path, you may then make class C with automatic conflict resolution, as follows: -- code >>> import noconflict >>> class C(A, B): __metaclass__ = noconflict.classmaker() -- !code Automating the resolution of the metatype conflict has many pluses, even in simple cases. Thanks to the memoizing technique used in noconflict.py, the same conflict-resolving metaclass will get used for any sequence of conflicting metaclasses. Moreover, with this approach you may also inject other metaclasses explicitly, beyond those you get from your base classes, and again avoid conflicts. Consider: -- code >>> class D(A): __metaclass__ = Meta_B Traceback (most recent call last): File "", line 1, in ? TypeError: Error when calling the metaclass bases metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases -- !code This metatype conflict is resolved just as easily as the former one: -- code >>> class D(A): __metaclass__ = noconflict.classmaker(Meta_B) -- !code If you want the the implicitly inherited Meta_A to take priority over the explicitly injected Meta_B, noconflict.classmaker allows that easily, too: -- code >>> class D(A): __metaclass__ = noconflict.classmaker(Meta_B, injected_priority=False) -- !code -- p The code presented in this recipe's Solution takes pains to avoid any subclassing that is not strictly necessary, and also uses mutual recursion to avoid any meta-level of meta-meta-type conflicts. You might never meet higher-order-meta conflict anyway, but if you adopt the code presented in this recipe you need not even worry about them. -- p I thank David Mertz for help in polishing the original version of the code. This version has largerly benefited from discussions with Phillip J. Eby, and Alex Martelli did his best to try to make the recipe's code as explicit and understandable as he could make it. -- see_also -- p Putting Metaclasses to Work: A New Dimension in Object-Oriented Programming, by Ira R. Forman and Scott H. Danforth (Addison Wesley, 1999).