6

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__))
Eli Courtwright
  • 186,300
  • 67
  • 213
  • 256

3 Answers3

3

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.

Community
  • 1
  • 1
Rob Wouters
  • 15,797
  • 3
  • 42
  • 36
2

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.

jsbueno
  • 99,910
  • 10
  • 151
  • 209
  • Thanks for the tip, that didn't do exactly what I wanted, since I still want subclasses of `Foo` to have the metaclass, but I've edited my question to include the solution that did what I needed, based on your suggestion. – Eli Courtwright Jan 15 '12 at 06:32
1

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).

Raymond Hettinger
  • 216,523
  • 63
  • 388
  • 485
  • In my actual example, the parent class is from a third-party module, and I want to extend a class from that module, but have my own metaclass which overrides some of the behavior of the third-party module's metaclass. Otherwise I'd definitely do what you suggest. – Eli Courtwright Jan 15 '12 at 04:53
  • @EliCourtwright Perhaps you can roll your own class that delegates to the third-party module rather than inheriting from it. That should give you full control over the class creation process while maximizing code re-use. – Raymond Hettinger Jan 15 '12 at 05:01