为什么类型(enumType)返回EnumMeta在python?


from enum import Enum
class Color(Enum):
RED=1;
>>>type(Color);
<class 'enum.EnumMeta'>

我认为Color的类型应该是"类枚举",但为什么它返回"enum.EnumMeta"?

这个行为不同于一般情况,是什么在引擎盖下?

首先,您创建的类Color是类型Enum的直接子类(来自模块enum),因此EnumColor的关系实际上是Color的父类(又称超类),或者ColorEnum的子类:

>>> from enum import Enum
>>> 
>>> class Color(Enum):
...     RED = 1
... 
>>> issubclass(Color, Enum)
True

现在,type()不仅仅是一个函数,而且是一个函数,尽管起初它可能看起来像一个函数,因为向它传递一个参数会显示传递的参数的类型。通常,它可以确定支持某些对象实例的底层类,例如:

>>> obj = object()
>>> type(obj)
<class 'object'>

同样,如果您像这样实例化Color的实例,并尝试找到结果类型:

>>> c = Color(1)
>>> type(c)
<enum 'Color'>

到目前为止都很好,但是类定义本身的类型是什么,比如object?

>>> type(object)
<class 'type'>
>>> class Demo(object):
...     pass
... 
>>> print(Demo)  # effectively calls `str(Demo)` before printing
<class '__main__.Demo'>
>>> type(Demo)
<class 'type'>

这两个都更接近您对type(Color)所做的事情-您所做的是有效地找到创建类的底层基础。在object及其子类Demo的情况下,该基础是type。这揭示了一些东西-type不只是一个函数,但实际上是一个元类(滚动到"什么是元类(最后)"部分,其中深入了解type实际上是什么)。但简单地说,我们可以通过直接从type而不是object继承子类来创建元类:

>>> class SubType(type):
...     pass

现在我们有一个type的子类,它可以用作metaclass。现在,创建一个新类,并通过以下操作将metaclass指定为SubType

>>> class Demo2(metaclass=SubType):
...     pass
... 
>>> class Demo3(Demo2):
...     pass
... 
>>> print(Demo2)  # effectively calls `str(Demo2)` before printing
<class '__main__.Demo2'>
>>> type(Demo2)
<class '__main__.SubType'>
>>> type(Demo3)
<class '__main__.SubType'>

有了从上面的例子中获得的知识,特别是如何创建类似问题的情况,这表明Enum及其子类将EnumMeta作为底层元类,而type(Enum)type(Color)只是揭示了这一事实。实际上,您还可以创建一个新的类,将EnumMeta作为元类,如下所示:

>>> from enum import EnumMeta
>>> 
>>> class BrokenEnum(metaclass=EnumMeta):
...     pass
... 
>>> print(BrokenEnum)  # effective calls `str(BrokenEnum)` before printing
<enum 'BrokenEnum'>
>>> type(BrokenEnum)
<class 'enum.EnumMeta'>

因此,这是在问题中看到的。虽然这是一个坏的枚举类,只是因为它没有EnumMeta期望这种类型的类正确工作的完整机制,因此应该咨询参考实现Enum,以确保结果类可以像Enum类一样工作。


所以有些人可能想知道type的三个参数调用如何适合这里?由于简单的一行代码可以动态地生成一个类(即type('Cls', (object,), {})实际上是class Cls(object): pass),这可以很容易地为Enum?实际上不是,因为元类的__prepare__类方法可以潜在地用于提供自定义映射,包括任何映射表的子类,所以必须首先使用它来调用以产生一个(因为前面调用type不会调用这个__prepare__类方法-Enum有这个__prepare__类方法来产生enum_EnumDict实例)。因此,要使用type复制第一个定义,可以执行以下操作:

>>> clsdict = EnumMeta.__prepare__('Color2', (Enum,),)
>>> clsdict['RED'] = 1
>>> Color2 = EnumMeta('Color2', (Enum,), clsdict)  # `type` may be used instead of `EnumMeta`, but this is to be consistent
>>> Color2(1)
<Color2.RED: 1>

虽然经过进一步的实验,仍然可以完成一行代码,但要注意,这是怪诞的,可能会随机停止工作,因为1)_EnumDict不被认为是公共接口,因为它以_为前缀;2)这依赖于一个可能在将来被覆盖的dunder操作符-看看3.9.13.9.2中如何没有引入这个新更改,作为一个简单的一行代码如何在后续版本中中断的例子。现在,如果你已经准备好了,那就准备好吧,因为这会滥用:=操作符,any,map,zip,以及操作符链,造成混乱,任何人都不应该写。

>>> Color3 = EnumMeta('Color3', (Enum,), (_ := EnumMeta.__prepare__(
...     'Color3', (Enum,),)) and any(map(_.__setitem__, *(zip(*{
...         'RED': 1,
...         'GREEN': 2,
...         'BLUE': 3,
...     }.items())))) or _
... )
>>> Color3(1)
<Color3.RED: 1>
>>> Color(2)
<Color.GREEN: 2>
>>> Color(3)
<Color.BLUE: 3>

我打破了一个长行可读性,并使它有可能传入一个标准的dict嵌套在类属性中,以创建与结果类的"易用性"。(是的,键和值可以作为单独的列表传递,但我认为这样编辑起来不太友好)。关于如何将它们组合在一起的解释将留给读者作为练习。