__classcell__ генерирует ошибку в Python 3.6, когда метакласс вызывает несколько super () .__ new__ из унаследованного класса - PullRequest
0 голосов
/ 10 апреля 2019

Вот исполняемый код, который работает в 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.Это классы тестов, и цель AMeta состоит в том, чтобы выполнить все тесты с двумя разными папками: одна по умолчанию и старая.

Я нашел способ устранить эту ошибку, удалив classcell от attrs, затем возвращается new = super (AMeta, cls). new (cls, name, base, attrs) (не new_attrs ) но это кажется неправильным, и если это так, я хотел бы знать, почему.

Цель new_attrs возникла в результате этого ТАКого вопроса или документация , где говорится в основном обратное: при изменении атрибутов обязательно сохраняйте classcell , поскольку он устарел в Python 3.6 и приведет к ошибке в Python 3.8.Обратите внимание, что в этом случае он удаляет определение pr, потому что они не были переданы new_attrs , поэтому печатает 'b' вместо 'abc', но это не имеет отношения к этой проблеме.

Есть ли способ вызвать несколько super (). new внутри new метакласса AMeta, а затем вызвать их из класса C, унаследованного от класса, наследующего A?

Без вложенного наследования ошибка не появляется, как это:

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 голосов
/ 11 апреля 2019

В чем ваша проблема это Я могу понять, и как обойти это Проблема в том, что когда вы делаете то, что делаете, вы передаете то же самое 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__, который указывает на новую ячейку var.

Без воссоздания методов они не будут работать в клонированном классе - поскольку super() в методах клонированного класса будет ожидать, что ячейка будет указывать на сам клонированный класс, но она указывает на исходный.

К счастью, методы в Python 3 являются простыми функциями - это делает код проще.Однако этот код не будет работать в Python 2 - поэтому просто заключите его в блок if, чтобы не запускать в Python2.Поскольку атрибут __cellclass__ там даже не существует, проблем вообще нет.

После вставки приведенного выше кода в оболочку Python я могу запустить оба метода, и super() работает:

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

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