Eli Courtwright
Eli Courtwright

Reputation: 192921

Why can't I change the __metaclass__ attribute of a class?

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

Answers (3)

jsbueno
jsbueno

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

Raymond Hettinger
Raymond Hettinger

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

Rob Wouters
Rob Wouters

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

Related Questions