关于多重继承:带有另一个基类的python abstractmethod破坏了抽象功能

python abstractmethod with another baseclass breaks abstract functionality

考虑下面的代码示例

1
2
3
4
5
6
7
8
9
10
import abc
class ABCtest(abc.ABC):
    @abc.abstractmethod
    def foo(self):
        raise RuntimeError("Abstract method was called, this should be impossible")

class ABCtest_B(ABCtest):
    pass

test = ABCtest_B()

这正确地引发了错误:

1
2
3
4
Traceback (most recent call last):
  File"/.../test.py", line 10, in <module>
    test = ABCtest_B()
TypeError: Can't instantiate abstract class ABCtest_B with abstract methods foo

但是,当ABCtest的子类也继承自诸如strlist的内置类型时,则没有错误,并且test.foo()调用抽象方法:

1
2
3
4
5
6
7
8
9
10
11
class ABCtest_C(ABCtest, str):
    pass

>>> test = ABCtest_C()
>>> test.foo()
Traceback (most recent call last):
  File"<pyshell#0>", line 1, in <module>
    test.foo()
  File"/.../test.py", line 5, in foo
    raise RuntimeError("Abstract method was called, this should be impossible")
RuntimeError: Abstract method was called, this should be impossible

从C中定义的任何类(包括itertools.chainnumpy.ndarray)继承时,似乎都发生了这种情况,但是仍然正确引发了python中定义的类的错误。 为什么实现内置类型之一会破坏抽象类的功能?


令人惊讶的是,阻止实例化抽象类的测试发生在object.__new__中,而不是abc模块本身定义的任何事情:

1
2
3
4
5
6
7
8
9
10
11
static PyObject *
object_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    ...
    if (type->tp_flags & Py_TPFLAGS_IS_ABSTRACT) {
        ...
        PyErr_Format(PyExc_TypeError,
                    "Can't instantiate abstract class %s"
                    "with abstract methods %U",
                     type->tp_name,
                     joined);

(几乎?)不是object的所有内置类型都提供一个不同的__new__来覆盖object.__new__并且不调用object.__new__。从非object内置类型进行多重继承时,您将继承其__new__方法,从而绕过抽象方法检查。

我没有看到关于__new__abc文档中内置类型的多重继承的任何信息。该文档可以在此处使用增强功能。

他们将一个元类用于ABC实现似乎很奇怪,将其他元类与抽象类一起使用,然后将关键的检查放入与abc无关的核心语言代码中并运行对于抽象类和非抽象类。

自2009年以来,一直存在有关问题跟踪器的此问题的报告。


我问了一个类似的问题,并且基于user2357112支持Monicas链接的bug报告,我想出了以下解决方法(基于来自Xiang Zhang的建议):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from abc import ABC, abstractmethod

class Base(ABC):
    @abstractmethod
    def foo(self):
        pass

    @abstractmethod
    def bar(self):
        pass

    def __new__(cls, *args, **kwargs):
        abstractmethods = getattr(cls, '__abstractmethods__', None)
        if abstractmethods:
            msg ="Can't instantiate abstract class {name} with abstract method{suffix} {methods}"
            suffix = 's' if len(abstractmethods) > 1 else ''
            raise TypeError(msg.format(name=cls.__name__, suffix=suffix, methods=', '.join(abstractmethods)))
        return super().__new__(cls, *args, **kwargs)

class Derived(Base, tuple):
    pass

Derived()

这将引发TypeError: Can't instantiate abstract class Derived with abstract methods bar, foo,这是原始行为。