使用 ABCMeta 和 EnumMeta 的抽象枚举类

Ale*_*agh 6 python metaclass python-3.x

简单的例子

目标是通过从 和 派生的元类创建一个抽象枚举abc.ABCMetaenum.EnumMeta。例如:

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
Run Code Online (Sandbox Code Playgroud)

现在,在 Python3.7 上,该代码将被正确解释(在 3.6.x 和可能更低的版本上,不会出现错误)。事实上,一切看起来都很棒,我们的 MRO 展示B源自 AIntEnum

>>> B.__mro__
(<enum 'B'>, __main__.A, abc.ABC, <enum 'IntEnum'>, int, <enum 'Enum'>, object)
Run Code Online (Sandbox Code Playgroud)

抽象枚举不是抽象的

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

>>> B.X
<B.X: 1>
>>> B(1)
<B.X: 1>
>>> B(1).foo() 
Run Code Online (Sandbox Code Playgroud)

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

>>> 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
Run Code Online (Sandbox Code Playgroud)

问题

EnumMeta为什么使用从派生的元类的类会ABCMeta有效地忽略ABCMeta,而使用从 派生的元类的任何其他类都ABCMeta使用它?即使我自定义了运算符也是如此__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)
Run Code Online (Sandbox Code Playgroud)

我很困惑,因为这似乎违背了元类的含义:元类应该定义类的定义和行为方式,在这种情况下,使用既是抽象又是枚举的元类定义一个类创建一个默默地忽略抽象组件的类。这是一个错误,这是有意为之,还是有什么我不明白的事情?

jsb*_*eno 4

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

然而,在类创建时,元类__new__正常运行,并且抽象类机制使用的将类标记为抽象的属性确实___abstractmethods__在创建的类上创建了属性。

因此,您要做的就是进一步自定义您的元类以在代码中执行抽象检查__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)
Run Code Online (Sandbox Code Playgroud)

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

但请注意,Enum无论如何都不能进一步继承一个类,并且在不缺少抽象方法的情况下简单地创建它可能已经违反了您想要检查的内容。也就是说:在上面的示例中,即使缺少方法,也class B可以声明并且可以工作。如果你想防止这种情况,只需在元类中进行相同的检查即可:B.xfoo__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)
Run Code Online (Sandbox Code Playgroud)

(不幸的是,ABCCPython 中的抽象方法检查似乎是在方法外部的本机代码中执行的ABCMeta.__call__- 否则,我们可以直接调用ABCMeta.__call__显式重写super的行为,而不是TypeError对其进行硬编码,而不是模仿错误。)