使用 ABCMeta 和 EnumMeta 的抽象枚举 Class

Abstract Enum Class using ABCMeta and EnumMeta

简单示例

目标是通过派生自 abc.ABCMetaenum.EnumMeta 的元 class 创建抽象枚举 class。例如:

import abc
import enum

class ABCEnumMeta(abc.ABCMeta, enum.EnumMeta):
    pass

class A(abc.ABC):
    @abc.abstractmethod
    def foo(self):
        pass

class B(A, enum.IntEnum, metaclass=ABCEnumMeta):
    X = 1

class C(A):
    pass

现在,在 Python3.7 上,这段代码将被无误地解释(在 3.6.x 上,大概在下面,它不会)。事实上,一切看起来都很棒,我们的 MRO 显示 B 来自 AIntEnum

>>> B.__mro__
(<enum 'B'>, __main__.A, abc.ABC, <enum 'IntEnum'>, int, <enum 'Enum'>, object)

抽象枚举不是抽象

然而,即使B.foo没有被定义,我们仍然可以毫无问题地实例化B,并调用foo()

>>> B.X
<B.X: 1>
>>> B(1)
<B.X: 1>
>>> B(1).foo() 

这看起来很奇怪,因为无法实例化从 ABCMeta 派生的任何其他 class,即使我使用自定义元class。

>>> class NewMeta(type): 
...     pass
... 
... class AbcNewMeta(abc.ABCMeta, NewMeta):
...     pass
... 
... class D(metaclass=NewMeta):
...     pass
... 
... class E(A, D, metaclass=AbcNewMeta):
...     pass
...
>>> E()
TypeError: Can't instantiate abstract class E with abstract methods foo

问题

为什么使用从 EnumMetaABCMeta 派生的元 class 的 class 会有效地忽略 ABCMeta,而任何其他 class使用从 ABCMeta 派生的 metaclass 使用它?即使我自定义定义 __new__ 运算符也是如此。

class ABCEnumMeta(abc.ABCMeta, enum.EnumMeta):
    def __new__(cls, name, bases, dct):
        # Commented out lines reflect other variants that don't work
        #return abc.ABCMeta.__new__(cls, name, bases, dct)
        #return enum.EnumMeta.__new__(cls, name, bases, dct)
        return super().__new__(cls, name, bases, dct)

我很困惑,因为这似乎与 metaclass 的含义背道而驰:metaclass 应该定义 class 的定义和行为方式,在这种情况下,使用既是抽象又是枚举的元class定义class会创建一个class,它会默默地忽略抽象组件。这是一个错误,这是故意的,还是我不理解的更重要的事情?

调用枚举类型不会创建新实例。枚举类型的成员由元 class 在 class 创建时立即创建。 __new__ 方法仅执行查找,这意味着永远不会调用 ABCMeta 以防止实例化。

B(1).foo() 之所以有效,是因为一旦您拥有一个实例,该方法是否被标记为抽象就无关紧要了。它仍然是一个真正的方法,可以这样调用。

如@chepner 的回答所述,发生的事情是 Enum metaclass 覆盖了正常的 metaclass' __call__ 方法,因此 Enum class 永远不会通过普通方法实例化,因此,ABCMeta 检查不会触发其抽象方法检查。

然而,在 class 创建时,Metaclass 的 __new__ 通常是 运行,而抽象使用的属性-class将 class 标记为抽象的机制会在创建的 class.

上创建属性 ___abstractmethods__

因此,您要做的就是进一步自定义您的元class以在代码中执行摘要检查 __call__:

import abc
import enum

class ABCEnumMeta(abc.ABCMeta, enum.EnumMeta):

    def __call__(cls, *args, **kw):
        if getattr(cls, "__abstractmethods__", None):
            raise TypeError(f"Can't instantiate abstract class {cls.__name__} "
                            f"with frozen methods {set(cls.__abstractmethods__)}")
        return super().__call__(*args, **kw)

这将使 B(1) 表达式失败并出现与 abstractclass 实例化相同的错误。

但是请注意,Enum class 无论如何都不能进一步继承,它只是在没有缺少抽象方法的情况下创建它可能已经违反了您要检查的内容。也就是说:在上面的示例中,可以声明 class B 并且 B.x 将起作用,即使缺少 foo 方法也是如此。如果你想防止这种情况发生,只需在 metaclass' __new__:

中进行相同的检查
import abc
import enum

class ABCEnumMeta(abc.ABCMeta, enum.EnumMeta):

    def __new__(mcls, *args, **kw):
        cls = super().__new__(mcls, *args, **kw)
        if issubclass(cls, enum.Enum) and getattr(cls, "__abstractmethods__", None):
            raise TypeError("...")
        return cls

    def __call__(cls, *args, **kw):
        if getattr(cls, "__abstractmethods__", None):
            raise TypeError(f"Can't instantiate abstract class {cls.__name__} "
                            f"with frozen methods {set(cls.__abstractmethods__)}")
        return super().__call__(*args, **kw)

(不幸的是,CPython 中的 ABC 抽象方法检查似乎是在本机代码中执行的,在 ABCMeta.__call__ 方法之外 - 否则,我们可以调用 ABCMeta.__call__ 显式覆盖 super 的行为,而不是在那里硬编码 TypeError。)