Могут ли абстрактные базовые классы python наследовать от C-расширений?

Кажется, что когда у меня есть абстрактный базовый класс, который наследуется от gevent.Greenlet(который наследуется от окуляра модуля расширения C: https://github.com/python-greenlet/greenlet), то классы, которые его реализуют, не поднимают никаких ошибок abc в отношении нереализованных методов.

class ActorBase(gevent.Greenlet):
    __metaclass__ = abc.ABCMeta

    @abc.abstractmethod
    def foo(self):
        print "foo"

class ActorBaseTest(ActorBase):
    def bar(self):
        print "bar"

abt = ActorBaseTest()  # no errors!

Если я наследую от object, он терпит неудачу, как ожидалось:

class ActorBase(object):
    __metaclass__ = abc.ABCMeta

    @abc.abstractmethod
    def foo(self):
        print "foo"

class ActorBaseTest(ActorBase):
    def bar(self):
        print "bar"

>>> abt = ActorBaseTest()
Traceback (most recent call last):
  File "/home/dw/.virtualenvs/prj/local/lib/python2.7/site-packages/IPython/core/interactiveshell.py", line 2827, in run_code
exec code_obj in self.user_global_ns, self.user_ns
  File "<ipython-input-6-d67a142e7297>", line 1, in <module>
    abt = ActorBaseTest()
TypeError: Can't instantiate abstract class ActorBaseTest with abstract methods foo

Каков правильный способ реализовать эту функциональность?

Ответ 1

Причиной вашей проблемы является то, что это метод object.__new__, который выполняет проверку на создание абстрактного класса, и в этом случае object.__new__ не вызывается: gevent.Greenlet наследует от greenlet.greenlet, а greenlet.greenlet - это тип расширения C, реализация которого __new__ не вызывает object.__new__ в любой точке (см. green_new в источнике greenlet C).

Вы можете увидеть тот же эффект путем подклассификации некоторых других встроенных типов, которые реализуют свой собственный метод __new__ и не ссылаются на object.__new__ (например, тип float). Однако проблема не относится к типам расширений C: вы также можете реплицировать ее с помощью чистых типов Python. Рассмотрим следующий код:

import abc

class A(object):
    def __new__(cls):
        # self = object.__new__(cls)
        return 42

class B(A):
    __metaclass__ = abc.ABCMeta

    @abc.abstractmethod
    def foo(self):
        pass

b = B()  # No exception.

Класс B правильно зарегистрирован как абстрактный класс (внутри его бит Py_TPFLAGS_IS_ABSTRACT установлен в поле tp_flags), но object.__new__ никогда не вызывается, поэтому нет ошибки, если B инстанцирован. Однако, если вы раскомментируете вызов метода self = object.__new__(cls) в A, вы увидите ожидаемую ошибку при создании экземпляра.

Что касается "правильного пути" для реализации этого, к сожалению, я думаю, что правильный способ - исправить тип greenlet, чтобы его метод __new__ вызывал object.__new__. Я думаю, вы могли бы добавить метод __new__ к ActorBase, который явно вызывает как базовый класс __new__, так и object.__new__ (и отбрасывает результат последнего), но я считаю, что уродливое обходное решение, а не правильный путь'. (EDIT: И кроме того, это не сработает. Я получаю TypeError: object.__new__(ActorBase) is not safe, use greenlet.greenlet.__new__() от вызова object.__new__.) Я открыл issue в диспетчере маршрутизации.


EDIT: эта проблема показалась несколько знакомой, и я просто немного поработал в источнике Enthought Traits, который определяет класс CHasTraits, реализованный в C, который отлично играет с ABC. И его метод __new__ начинается следующим образом (комментарии взяты из исходного источника, а не моего):

PyObject *
has_traits_new ( PyTypeObject * type, PyObject * args, PyObject * kwds ) {

    // Call PyBaseObject_Type.tp_new to do the actual construction.
    // This allows things like ABCMeta machinery to work correctly
    // which is implemented at the C level.
    has_traits_object * obj = (has_traits_object *) PyBaseObject_Type.tp_new(type, empty_tuple, empty_dict);

Итак, возможно, долгосрочное решение состоит в том, чтобы убедить людей greenlet сделать что-то подобное.