I have a weird and unusual use case for metaclasses where I'd like to change the __metaclass__
of a base class after it's been defined so that its subclasses will automatically use the new __metaclass__
. But that oddly doesn't work:
class MetaBase(type):
def __new__(cls, name, bases, attrs):
attrs["y"] = attrs["x"] + 1
return type.__new__(cls, name, bases, attrs)
class Foo(object):
__metaclass__ = MetaBase
x = 5
print (Foo.x, Foo.y) # prints (5, 6) as expected
class MetaSub(MetaBase):
def __new__(cls, name, bases, attrs):
attrs["x"] = 11
return MetaBase.__new__(cls, name, bases, attrs)
Foo.__metaclass__ = MetaSub
class Bar(Foo):
pass
print(Bar.x, Bar.y) # prints (5, 6) instead of (11, 12)
What I'm doing may very well be unwise/unsupported/undefined, but I can't for the life of me figure out how the old metaclass is being invoked, and I'd like to least understand how that's possible.
EDIT: Based on a suggestion made by jsbueno
, I replaced the line Foo.__metaclass__ = MetaSub
with the following line, which did exactly what I wanted:
Foo = type.__new__(MetaSub, "Foo", Foo.__bases__, dict(Foo.__dict__))
In order to set metaclass of a class, we use the __metaclass__ attribute. Metaclasses are used at the time the class is defined, so setting it explicitly after the class definition has no effect. The best idea I can think of is to re-define whole class and add the __metaclass__ attribute dynamically somehow.
The metaclass is determined by looking at the baseclasses of the class-to-be (metaclasses are inherited), at the __metaclass__ attribute of the class-to-be (if any) or the __metaclass__ global variable. The metaclass is then called with the name, bases and attributes of the class to instantiate it.
When defining a class and no metaclass is defined the default type metaclass will be used. If a metaclass is given and it is not an instance of type() , then it is used directly as the metaclass.
In object-oriented programming, a metaclass is a class whose instances are classes. Just as an ordinary class defines the behavior of certain objects, a metaclass defines the behavior of certain classes and their instances.
The problem is the __metaclass__
attribute is not used when inherited, contrary to what you might expect. The 'old' metaclass isn't called either for Bar
. The docs say the following about how the metaclass is found:
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.
So what is actually used as metaclass in your Bar
class is found in the parent's __class__
attribute and not in the parent's __metaclass__
attribute.
More information can be found on this StackOverflow answer.
The metaclass information for a class is used at the moment it is created (either parsed as a class block, or dynamically, with a explicit call to the metaclass). It can't be changed because the metaclass usually does make changes at class creation time - the created class type is the metaclasse. Its __metaclass__
attribute is irrelevant once it is created.
However, it is possible to create a copy of a given class, and have the copy bear a different metclass than the original class.
On your example, if instead of doing:
Foo.__metaclass__ = MetaSub
you do:
Foo = Metasub("Foo", Foo.__bases__, dict(Foo.__dict__))
You will achieve what you intended. The new Foo
is for all effects equal its predecessor, but with a different metaclass.
However, previously existing instances of Foo
won't be considered an instance of the new Foo
- if you need that, you better create the Foo
copy with a different name instead.
Subclasses use the __metaclass__ of their parent.
The solution to your use-case is to program the parent's __metaclass__ so that it will have different behaviors for the parent than for its subclasses. Perhaps have it inspect the class dictionary for a class variable and implement different behaviors depending on its value (this is the technique type uses to control whether or not instances are given a dictionary depending on the whether or not __slots__ is defined).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With