使用ABCMeta和EnumMeta的抽象Enum类

时间:2019-02-26 20:30:06

标签: python python-3.x metaclass

简单示例

目标是通过从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

现在,在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 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派生的元类的类有效地忽略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)

我很困惑,因为这似乎在面对元类是什么的时候:元类应该定义该类的定义和行为方式,在这种情况下,使用一个既抽象又抽象的元类来定义一个类枚举创建一个类,该类静默忽略抽象组件。这是错误,是有目的的,还是我不了解的更多地方?

2 个答案:

答案 0 :(得分:3)

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

B(1).foo()之所以有效,是因为一旦有了实例,该方法是否被标记为抽象就没有关系。它仍然是一个真实的方法,可以这样称呼。

答案 1 :(得分:2)

如@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)

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

但是,请注意,Enum类无论如何都不能进一步继承,并且仅创建它而没有缺少的抽象方法可能已经违反了您要检查的内容。也就是说:在上面的示例中,即使缺少了class B方法,也可以声明B.x并且foo将起作用。如果要防止这种情况,只需将相同的检查放入元类的__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__方法之外的本机代码中执行的-否则,除了模仿错误外,我们可以调用{{1} }显式覆盖ABCMeta.__call__的行为,而不是在那里对super进行硬编码。)