为什么我不能更改类的 __metaclass__ 属性?

6 投票
3 回答
694 浏览
提问于 2025-04-17 10:16

我有一个奇怪的需求,想用元类来改变一个基类的 __metaclass__,而且是在这个基类定义之后进行修改,这样它的子类就能自动使用新的 __metaclass__。但是,这样做似乎不太管用:

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)

我知道我这样做可能不太明智、也不被支持,甚至可能是未定义的,但我就是搞不懂为什么旧的元类还会被调用。我想至少弄明白这是怎么回事。

补充:根据 jsbueno 的建议,我把 Foo.__metaclass__ = MetaSub 这一行换成了下面这一行,结果正好达到了我想要的效果:

Foo = type.__new__(MetaSub, "Foo", Foo.__bases__, dict(Foo.__dict__))

3 个回答

1

子类会使用它们父类的 __metaclass__

解决你这个问题的方法是对父类的 __metaclass__ 进行编程,使得它对父类和子类有不同的行为。你可以让它检查类字典中的一个类变量,根据这个变量的值来实现不同的行为。比如,type 就是用这种方法来决定实例是否会有一个字典,这取决于 __slots__ 是否被定义。

3

这个问题是关于__metaclass__属性的,它在继承时并不会像你想的那样被使用。对于Bar类来说,"旧的"元类也不会被调用。文档中提到关于如何找到元类的规则如下:

确定合适的元类遵循以下优先级规则:

  • 如果字典中有__metaclass__,那么就使用它。
  • 否则,如果至少有一个基类,就使用它的元类(这会先查找__class__属性,如果找不到,就使用它的类型)。
  • 否则,如果存在一个名为__metaclass__的全局变量,就使用它。
  • 否则,就使用旧式的经典元类(types.ClassType)。

所以在你的Bar类中,实际上使用的元类是从父类的__class__属性中找到的,而不是从父类的__metaclass__属性中找到的。

更多信息可以在这个StackOverflow的回答中找到。

2

一个类的元类信息在它被创建的时候就会被使用(要么是通过解析类块来创建,要么是通过明确调用元类来动态创建)。这个信息一旦创建就不能再改变,因为元类通常会在类创建时进行一些修改——创建出来的类类型就是这个元类。它的 __metaclass__ 属性在创建后就不再重要了。

不过,你可以创建一个已有类的副本,并让这个副本使用不同的元类。

在你的例子中,如果你不是这样做:

Foo.__metaclass__ = MetaSub

而是这样做:

Foo = Metasub("Foo", Foo.__bases__, dict(Foo.__dict__))

那么你就能达到你想要的效果。新的 Foo 在所有方面都和之前的版本相同,但它有一个不同的元类。

不过,之前已经存在的 Foo 实例不会被视为新 Foo 的实例——如果你需要这样的效果,最好是给这个 Foo 副本起个不同的名字。

撰写回答