Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why isn't my metaclass function getting invoked for subclasses?

The Python docs say that the metaclass of a class can be any callable. All the examples I see use a class. Why not use a function? It's callable, and fairly simple to define. But it isn't working, and I don't understand why.

Here's my code:

class Foo(object):
    def __metaclass__(name, base, dict):
        print('inside __metaclass__(%r, ...)' % name)
        return type(name, base, dict)
print(Foo.__metaclass__)
class Bar(Foo):
    pass
print(Bar.__metaclass__)

Here's the output:

inside __metaclass__('Foo', ...)
<unbound method Foo.__metaclass__>
<unbound method Bar.__metaclass__>

The metaclass method is defined for both the parent and child classes. Why is it only getting called for the parent? (Yes, I tried using the classmethod and staticmethod decorators for my metaclass, neither works. Yes, this might seem to be a dup of Metaclass not being called in subclasses but they are a class, not a function, as a metaclass.)

like image 657
samwyse Avatar asked Aug 20 '14 12:08

samwyse


1 Answers

The answer is in the precedence rules for __metaclass__ lookup:

The appropriate metaclass is determined by the following precedence rules:

  • If dict['__metaclass__'] exists, it is used.
  • Otherwise, if there is at least one base class, its metaclass is used (this looks for a __class__ attribute first and if not found, uses its type).
  • Otherwise, if a global variable named __metaclass__ exists, it is used.
  • Otherwise, the old-style, classic metaclass (types.ClassType) is used.

If we examine Foo.__class__ we find that it is <type 'type'>, which is expected as your metaclass function called type to construct Foo.

__class__ is set by type to the first parameter of type.__new__, which is why in class metaclasses we call type.__new__(cls, name, bases, dict) (or super(Metaclass, cls).__new__(cls, ...)). However, we can't do that if the metaclass is a function:

>>> def __metaclass__(name, base, dict):
>>>     print('inside __metaclass__(%r, %r, %r)' % (name, base, dict))
>>>     return type.__new__(__metaclass__, name, base, dict)
>>> class Foo(object):
>>>     __metaclass__ = __metaclass__
TypeError: Error when calling the metaclass bases
    type.__new__(X): X is not a type object (function)

Similarly, if we try to set Foo.__class__ to your __metaclass__ it fails, as the __class__ attribute must be a class:

>>> Foo.__class__ = Foo.__metaclass__.__func__
TypeError: __class__ must be set to new-style class, not 'function' object

So, the reason to make metaclasses classes inheriting type, as opposed to just callables, is to make them inheritable.

like image 148
ecatmur Avatar answered Sep 28 '22 08:09

ecatmur