目标是通过从abc.ABCMeta
和enum.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
来自A
和IntEnum
。
>>> 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
为什么使用从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)
我很困惑,因为这似乎在面对元类是什么的时候:元类应该定义该类的定义和行为方式,在这种情况下,使用一个既抽象又抽象的元类来定义一个类枚举创建一个类,该类静默忽略抽象组件。这是错误,是有目的的,还是我不了解的更多地方?
答案 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
进行硬编码。)