Ale*_*agh 6 python metaclass python-3.x
目标是通过从 和 派生的元类创建一个抽象枚举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
Run Code Online (Sandbox Code Playgroud)
现在,在 Python3.7 上,该代码将被正确解释(在 3.6.x 和可能更低的版本上,不会出现错误)。事实上,一切看起来都很棒,我们的 MRO 展示B
源自 A
和IntEnum
。
>>> 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)
我很困惑,因为这似乎违背了元类的含义:元类应该定义类的定义和行为方式,在这种情况下,使用既是抽象又是枚举的元类定义一个类创建一个默默地忽略抽象组件的类。这是一个错误,这是有意为之,还是有什么我不明白的事情?
正如 @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.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)
Run Code Online (Sandbox Code Playgroud)
(不幸的是,ABC
CPython 中的抽象方法检查似乎是在方法外部的本机代码中执行的ABCMeta.__call__
- 否则,我们可以直接调用ABCMeta.__call__
显式重写super
的行为,而不是TypeError
对其进行硬编码,而不是模仿错误。)