小能豆

当元类从继承类调用多个 super().__new__ 时,__classcell__ 在 Python 3.6 中生成错误

py

以下是一个可执行代码,它在 Python 2.7 中可以运行,但在 Python 3.6 中会出现错误:

import six

class AMeta(type):

    def __new__(cls, name, bases, attrs):
        module = attrs.pop('__module__')
        new_attrs = {'__module__': module}
        classcell = attrs.pop('__classcell__', None)
        if classcell is not None:
            new_attrs['__classcell__'] = classcell
        new = super(AMeta, cls).__new__(
            cls, name, bases, new_attrs)
        new.duplicate = False
        legacy = super(AMeta, cls).__new__(
            cls, 'Legacy' + name, (new,), new_attrs)
        legacy.duplicate = True
        return new

@six.add_metaclass(AMeta)
class A():
    def pr(cls):
        print('a')

class B():
    def pr(cls):
        print('b')

class C(A,B):
    def pr(cls):
        super(C, cls).pr() # not shown with new_attrs
        B.pr(cls)
        print('c') # not shown with new_attrs
c = C()
c.pr()

# Expected result
# a
# b
# c

我收到以下错误:

Traceback (most recent call last):
  File "test.py", line 28, in <module>
    class C(A,B):
TypeError: __class__ set to <class '__main__.LegacyC'> defining 'C' as <class '__main__.C'>

C 继承自使用元类 AMeta 生成的 A。它们是测试类,AMeta 的目标是使用 2 个不同的文件夹执行所有测试:默认文件夹和旧文件夹。

我找到了一种消除此错误的方法,即从attrs 中删除classcell ,然后返回new = super(AMeta, cls)。new ( cls, name, bases, attrs)(而不是new_attrs),但这似乎不正确,如果是这样,我想知道原因。

new_attrs 的目标源自这个SO 问题或文档其中基本上陈述了相反的内容:修改 attrs 时,请确保保留classcell,因为它在 Python 3.6 中已弃用,并将导致 Python 3.8 中的错误。请注意,在这种情况下,它会删除 pr 定义,因为它们未传递给new_attrs,因此会打印“b”而不是“abc”,但与此问题无关。

有没有办法在元类 AMeta 的new中调用多个 super(). new,然后从继承 A 的类 C 中调用它们?

如果没有嵌套继承,则不会出现错误,如下所示:

import six

class AMeta(type):

    def __new__(cls, name, bases, attrs):
        new = super(AMeta, cls).__new__(
            cls, name, bases, attrs)
        new.duplicate = False
        legacy = super(AMeta, cls).__new__(
            cls, 'Duplicate' + name, (new,), attrs)
        legacy.duplicate = True
        return new

@six.add_metaclass(AMeta)
class A():
    def pr(cls):
        print('a')

a = A()
a.pr()

# Result

# a

那么也许 A 的职责就是做些事情来解决这个问题?


阅读 23

收藏
2024-12-25

共1个答案

小能豆

我可以弄清楚你的问题什么,以及如何解决它问题是,当你做你正在做的事情时,你将同一个 cell对象传递给你的类的两个副本:原始副本和遗留副本。

由于它同时存在于两个类中,当尝试使用它时,它会与正在使用它的其他地方发生冲突 -super()调用时会选择错误的祖先类。

cell对象很挑剔,它们是用本机代码创建的,无法在 Python 端创建或配置。我可以想出一种创建类副本的方法,即使用一个方法返回一个新的单元格对象,并将其作为传递__classcell__

(在诉诸我的吼叫之前,我也尝试过在对象上运行copy.copy/ - 但是它不起作用)copy.deepcopy``classcell``cellfactory

为了重现该问题并找出解决方案,我制作了一个更简单的元类版本,仅限 Python3。

from types import FunctionType
legacies = []

def classcellfactory():
    class M1(type):
        def __new__(mcls, name, bases, attrs, classcellcontainer=None):
            if isinstance(classcellcontainer, list):
                classcellcontainer.append(attrs.get("__classcell__", None))

    container = []

    class T1(metaclass=M1, classcellcontainer=container):
        def __init__(self):
            super().__init__()
    return container[0]


def cellfactory():
    x = None
    def helper():
        nonlocal x
    return helper.__closure__[0]

class M(type):
    def __new__(mcls, name, bases, attrs):
        cls1 = super().__new__(mcls, name + "1", bases, attrs)
        new_attrs = attrs.copy()
        if "__classcell__" in new_attrs:
            new_attrs["__classcell__"] = cellclass = cellfactory()

            for name, obj in new_attrs.items():
                if isinstance(obj, FunctionType) and obj.__closure__:
                    new_method = FunctionType(obj.__code__, obj.__globals__, obj.__name__, obj.__defaults__, (cellclass, ))
                    new_attrs[name] = new_method

        cls2 = super().__new__(mcls, name + "2", bases, new_attrs) 
        legacies.append(cls2)
        return cls1

class A(metaclass=M):
    def meth(self):
        print("at A")

class B(A): 
    pass

class C(B,A): 
    def meth(self):
        super().meth()

C()

因此,我不仅创建了一个嵌套函数以便让 Python 运行时创建一个单独的单元对象,然后在克隆的类中使用该单元对象 - 而且,还必须使用指向__closure__新单元变量的新方法重新创建利用单元类的方法。

如果不重新创建这些方法,它们将无法在克隆的类中工作 - 因为super()克隆的类的方法期望单元格指向克隆的类本身,但它却指向原始类。

幸运的是,Python 3 中的方法是普通函数 - 这使得代码更简单。但是,该代码无法在 Python 2 中运行 - 因此,只需将其括在一个if块中即可不在 Python2 上运行。由于该__cellclass__属性在那里甚至不存在,所以根本没有问题。

将上述代码粘贴到 Python shell 中后,我可以运行这两种方法并super()运行:

In [142]: C().meth()                                                                                                                              
at A

In [143]: legacies[-1]().meth()                                                                                                                   
at A
2024-12-25