Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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__))
like image 338
Eli Courtwright Avatar asked Jan 15 '12 02:01

Eli Courtwright


People also ask

How do I change metaclass in Python?

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.

How do you set a metaclass from one class to another?

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.

What is the best way to declare a class as metaclass?

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.

What is metaclass in C#?

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.


3 Answers

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.

like image 163
Rob Wouters Avatar answered Sep 25 '22 06:09

Rob Wouters


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.

like image 21
jsbueno Avatar answered Sep 23 '22 06:09

jsbueno


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

like image 30
Raymond Hettinger Avatar answered Sep 26 '22 06:09

Raymond Hettinger