Reputation: 192921
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__))
Upvotes: 6
Views: 725
Reputation: 110271
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.
Upvotes: 2
Reputation: 226296
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).
Upvotes: 1
Reputation: 16327
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.
Upvotes: 3