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

时间:2019-04-10 13:58:20

标签: python python-3.6 metaclass

这里是可执行代码,可在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 < / strong>(cls,名称,碱基,attrs)(不是 new_attrs ),但这似乎不正确,如果正确,我想知道为什么。

new_attrs的目标是由该SO questiondocumentation引起的,它的基本相反:在修改attrs时,请确保保留 classcell ,因为它在Python 3.6中已弃用,并会在Python 3.8中导致错误。 请注意,在这种情况下,它删除了pr定义,因为它们没有传递给 new_attrs ,因此打印了'b'而不是'abc',但是与​​这个问题无关。

是否可以调用元类AMeta的 new 中的多个super()。 new ,然后从继承自该类的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

那么也许做些什么来解决这个问题是

先谢谢了

1 个答案:

答案 0 :(得分:0)

我能弄清你的问题 是什么,以及如何解决 问题在于,当您做自己的工作时,会将同一个 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运行时创建一个单独的单元对象,然后在克隆的类中使用该对象,而且使用该类的方法也必须重新使用指向新单元格var的新__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