Python Ошибка метакласса множественного наследования - PullRequest
2 голосов
/ 12 июля 2020

Я пытаюсь наследовать from ABC и ctypes.Structure, но получаю множественные ошибки наследования и метакласса.

Что я делаю не так?

Мой код выглядит следующим образом:

from ctypes import *
from abc import ABC, abstractmethod, ABCMeta

class FinalMeta(ABCMeta, type(Structure)):
    def __new__(mcs, name, bases, namespace, **kwargs):
        print("FinalMeta.__new__(mcs, name, bases, namespace, **kwargs)")
        print("  mcs =", mcs)
        print("  name =", name)
        print("  bases =", bases)
        print("  namespace =", namespace)
        print("  kwargs =", kwargs)
        cls = super().__new__(mcs, name, bases, namespace, **kwargs)
        print("<-- cls =", cls)
        print()
        return cls


class MessageBase(ABC, Structure, metaclass=FinalMeta):
    def __init__(self, *args, **kwargs):
        super().__init__()
    
    @property
    @abstractmethod
    def format(self):
        pass

Ошибка:

File "D:\Development\messages.py", line 13, in __new__
    cls = super().__new__(mcs, name, bases, namespace, **kwargs)
  File "C:\Users\AppData\Local\Programs\Python\Python37-32\lib\abc.py", line 126, in __new__
    cls = super().__new__(mcls, name, bases, namespace, **kwargs)
TypeError: _ctypes.PyCStructType.__new__(FinalMeta) is not safe, use type.__new__()**

1 Ответ

3 голосов
/ 13 июля 2020

Это не сработает. Ответ yAzou может выглядеть так, как будто он работает, но это не так - он пропускает важную инициализацию и оставляет MessageBase в нерабочем состоянии; наиболее очевидным признаком является то, что даже если вы заполните _fields_ и реализуете абстрактное свойство в конкретном подклассе, , вы по-прежнему не сможете создавать экземпляры .

Попытка выполнить множественное наследование с типами, записанными в C, не поддерживается. Частично поведение может быть ошибкой Python, но я сомневаюсь, что она когда-нибудь будет хорошо работать. Вот что происходит.

_ctypes.PyCStructType - это метакласс ctypes.Structure. Этот метакласс написан в C, что означает, что его __new__ создается как оболочка вокруг своего C -уровня tp_new слота.

Обертка tp_new хочет чтобы убедиться, что вы никогда не пропустите инициализацию уровня C, выполнив что-то вроде object.__new__(tuple) (или, что еще хуже, tuple.__new__(dict)), поскольку это может привести к повреждению памяти. По этой причине он имеет следующую проверку безопасности :

static PyObject *
tp_new_wrapper(PyObject *self, PyObject *args, PyObject *kwds)
{
    ...
    /* Check that the use doesn't do something silly and unsafe like
       object.__new__(dict).  To do this, we check that the
       most derived base that's not a heap type is this type. */
    staticbase = subtype;
    while (staticbase && (staticbase->tp_new == slot_tp_new))
        staticbase = staticbase->tp_base;
    /* If staticbase is NULL now, it is a really weird type.
       In the spirit of backwards compatibility (?), just shut up. */
    if (staticbase && staticbase->tp_new != type->tp_new) {
        PyErr_Format(PyExc_TypeError,
                     "%s.__new__(%s) is not safe, use %s.__new__()",
                     type->tp_name,
                     subtype->tp_name,
                     staticbase->tp_name);
        return NULL;
    }

Это пытается убедиться, что когда вы звоните ClassA.__new__(ClassB, ...) с ClassA, записанным в C, ClassA - это класс C, самый дальний по иерархии наследования ClassB. Однако он следует за цепочкой одиночных базовых классов через tp_base указателей, игнорируя множественное наследование !

Способом инициализации указателей tp_base (см. best_base в Objects/typeobject.c), следующие за tp_base указатели обычно приводят к наиболее производному C классу, но это зависит от каждого C класса, добавляющего поля в свой (единственный) базовый класс макет памяти экземпляра. _ctypes.PyCStructType этого не делает (он заменяет класс dict на странный настраиваемый подкласс dict для хранения своих данных), поэтому цепочка tp_base для FinalMeta переходит в FinalMeta-> ABCMeta-> type-> object.

Поскольку _ctypes.PyCStructType не входит в цепочку tp_base, tp_new_wrapper думает, что type является наиболее производным C предком FinalMeta, и думает, что вам следует вместо этого позвонить type.__new__. Однако если вы вызовете type.__new__, вы пропустите инициализацию, эта проверка должна была остановить вас от пропуска, оставив ваш класс в нерабочем состоянии.

Вы можете подумать, что можете исправить это, переставив основания так, чтобы type(Structure) перед ABCMeta, но это решает только половину проблемы. Вы сможете определить свой MessageBase класс, но ABCMeta.__new__ не будет работать, и он не будет выполнять инициализацию, необходимую для пометки MessageBase аннотации.

Кроме того, есть полностью несвязанная, давняя проблема, когда, даже если MessageBase был помечен как абстрактное, вы все равно могли бы создавать его экземпляры. Проверка, предотвращающая создание экземпляров абстрактных классов, находится в object.__new__, а C классы, отличные от object (и потомки таких классов), не вызывают object.__new__.

...