[英]How to extend Python Enum?
是否可以扩展使用 Python 3.4 中的新Enum
功能创建的类? 如何?
简单的子类化似乎不起作用。 一个例子像
from enum import Enum
class EventStatus(Enum):
success = 0
failure = 1
class BookingStatus(EventStatus):
duplicate = 2
unknown = 3
将给出类似TypeError: Cannot extend enumerations
或(在更新版本中) TypeError: BookingStatus: cannot extend enumeration 'EventStatus'
。
我怎样才能让BookingStatus
重用EventStatus
的枚举值并添加更多?
仅当枚举未定义任何成员时,才允许对枚举进行子类化。
允许定义成员的枚举的子类化将导致违反类型和实例的一些重要不变量。
https://docs.python.org/3/library/enum.html#restricted-enum-subclassing
所以不,这不是直接可能的。
虽然不常见,但有时从许多模块创建枚举很有用。 aenum
1库通过extend_enum
函数支持这一点:
from aenum import Enum, extend_enum
class Index(Enum):
DeviceType = 0x1000
ErrorRegister = 0x1001
for name, value in (
('ControlWord', 0x6040),
('StatusWord', 0x6041),
('OperationMode', 0x6060),
):
extend_enum(Index, name, value)
assert len(Index) == 5
assert list(Index) == [Index.DeviceType, Index.ErrorRegister, Index.ControlWord, Index.StatusWord, Index.OperationMode]
assert Index.DeviceType.value == 0x1000
assert Index.StatusWord.value == 0x6041
1披露:我是Python stdlib Enum
、 enum34
backport和Advanced Enumeration ( aenum
)库的作者。
直接调用 Enum 类并使用链允许扩展(连接)现有枚举。
我在处理 CANopen 实现时遇到了扩展枚举的问题。 从 0x1000 到 0x2000 范围内的参数索引对所有 CANopen 节点都是通用的,而例如从 0x6000 开始的范围取决于节点是否是驱动器、io 模块等。
节点.py:
from enum import IntEnum
class IndexGeneric(IntEnum):
""" This enum holds the index value of genric object entrys
"""
DeviceType = 0x1000
ErrorRegister = 0x1001
Idx = IndexGeneric
驱动器.py:
from itertools import chain
from enum import IntEnum
from nodes import IndexGeneric
class IndexDrives(IntEnum):
""" This enum holds the index value of drive object entrys
"""
ControlWord = 0x6040
StatusWord = 0x6041
OperationMode = 0x6060
Idx= IntEnum('Idx', [(i.name, i.value) for i in chain(IndexGeneric,IndexDrives)])
我在 3.8 上测试过这种方式。 我们可以继承现有的枚举,但我们也需要从基类(在最后一个位置)进行。
一个新的 Enum 类必须有一个基本 Enum 类,最多一种具体数据类型,以及所需数量的基于对象的 mixin 类。 这些基类的顺序是:
class EnumName([mix-in, ...,] [data-type,] base-enum):
pass
class Cats(Enum):
SIBERIAN = "siberian"
SPHINX = "sphinx"
class Animals(Cats, Enum):
LABRADOR = "labrador"
CORGI = "corgi"
之后,您可以访问 Cats from Animals:
>>> Animals.SIBERIAN
<Cats.SIBERIAN: 'siberian'>
但是,如果您想遍历此枚举,则只能访问新成员:
>>> list(Animals)
[<Animals.LABRADOR: 'labrador'>, <Animals.CORGI: 'corgi'>]
实际上这种方式是为了从基类继承方法,但您可以将它用于具有这些限制的成员。
如上所述,编写一些函数将两个枚举合二为一。 我写了那个例子:
def extend_enum(inherited_enum):
def wrapper(added_enum):
joined = {}
for item in inherited_enum:
joined[item.name] = item.value
for item in added_enum:
joined[item.name] = item.value
return Enum(added_enum.__name__, joined)
return wrapper
class Cats(Enum):
SIBERIAN = "siberian"
SPHINX = "sphinx"
@extend_enum(Cats)
class Animals(Enum):
LABRADOR = "labrador"
CORGI = "corgi"
但在这里我们遇到了另一个问题。 如果我们想比较成员,它会失败:
>>> Animals.SIBERIAN == Cats.SIBERIAN
False
在这里,我们可能只比较新创建成员的名称和值:
>>> Animals.SIBERIAN.value == Cats.SIBERIAN.value
True
但是如果我们需要对新枚举进行迭代,它可以正常工作:
>>> list(Animals)
[<Animals.SIBERIAN: 'siberian'>, <Animals.SPHINX: 'sphinx'>, <Animals.LABRADOR: 'labrador'>, <Animals.CORGI: 'corgi'>]
所以选择你的方式:简单的继承,用装饰器模拟继承(实际上是重新创建),或者添加一个新的依赖,比如 aenum(我没有测试过,但我希望它支持我描述的所有特性)。
对于正确的类型规范,您可以使用Union
运算符:
from enum import Enum
from typing import Union
class EventStatus(Enum):
success = 0
failure = 1
class BookingSpecificStatus(Enum):
duplicate = 2
unknown = 3
BookingStatus = Union[EventStatus, BookingSpecificStatus]
example_status: BookingStatus
example_status = BookingSpecificStatus.duplicate
example_status = EventStatus.success
我选择使用元类方法来解决这个问题。
from enum import EnumMeta
class MetaClsEnumJoin(EnumMeta):
"""
Metaclass that creates a new `enum.Enum` from multiple existing Enums.
@code
from enum import Enum
ENUMA = Enum('ENUMA', {'a': 1, 'b': 2})
ENUMB = Enum('ENUMB', {'c': 3, 'd': 4})
class ENUMJOINED(Enum, metaclass=MetaClsEnumJoin, enums=(ENUMA, ENUMB)):
pass
print(ENUMJOINED.a)
print(ENUMJOINED.b)
print(ENUMJOINED.c)
print(ENUMJOINED.d)
@endcode
"""
@classmethod
def __prepare__(metacls, name, bases, enums=None, **kargs):
"""
Generates the class's namespace.
@param enums Iterable of `enum.Enum` classes to include in the new class. Conflicts will
be resolved by overriding existing values defined by Enums earlier in the iterable with
values defined by Enums later in the iterable.
"""
#kargs = {"myArg1": 1, "myArg2": 2}
if enums is None:
raise ValueError('Class keyword argument `enums` must be defined to use this metaclass.')
ret = super().__prepare__(name, bases, **kargs)
for enm in enums:
for item in enm:
ret[item.name] = item.value #Throws `TypeError` if conflict.
return ret
def __new__(metacls, name, bases, namespace, **kargs):
return super().__new__(metacls, name, bases, namespace)
#DO NOT send "**kargs" to "type.__new__". It won't catch them and
#you'll get a "TypeError: type() takes 1 or 3 arguments" exception.
def __init__(cls, name, bases, namespace, **kargs):
super().__init__(name, bases, namespace)
#DO NOT send "**kargs" to "type.__init__" in Python 3.5 and older. You'll get a
#"TypeError: type.__init__() takes no keyword arguments" exception.
这个元类可以像这样使用:
>>> from enum import Enum
>>>
>>> ENUMA = Enum('ENUMA', {'a': 1, 'b': 2})
>>> ENUMB = Enum('ENUMB', {'c': 3, 'd': 4})
>>> class ENUMJOINED(Enum, metaclass=MetaClsEnumJoin, enums=(ENUMA, ENUMB)):
... e = 5
... f = 6
...
>>> print(repr(ENUMJOINED.a))
<ENUMJOINED.a: 1>
>>> print(repr(ENUMJOINED.b))
<ENUMJOINED.b: 2>
>>> print(repr(ENUMJOINED.c))
<ENUMJOINED.c: 3>
>>> print(repr(ENUMJOINED.d))
<ENUMJOINED.d: 4>
>>> print(repr(ENUMJOINED.e))
<ENUMJOINED.e: 5>
>>> print(repr(ENUMJOINED.f))
<ENUMJOINED.f: 6>
这种方法使用与源Enum
相同的名称-值对创建一个新的Enum
,但生成的Enum
成员仍然是唯一的。 名称和值将是相同的,但它们将无法按照 Python 的Enum
类设计的精神与其起源进行直接比较:
>>> ENUMA.b.name == ENUMJOINED.b.name
True
>>> ENUMA.b.value == ENUMJOINED.b.value
True
>>> ENUMA.b == ENUMJOINED.b
False
>>> ENUMA.b is ENUMJOINED.b
False
>>>
注意在命名空间冲突的情况下会发生什么:
>>> ENUMC = Enum('ENUMA', {'a': 1, 'b': 2})
>>> ENUMD = Enum('ENUMB', {'a': 3})
>>> class ENUMJOINEDCONFLICT(Enum, metaclass=MetaClsEnumJoin, enums=(ENUMC, ENUMD)):
... pass
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 19, in __prepare__
File "C:\Users\jcrwfrd\AppData\Local\Programs\Python\Python37\lib\enum.py", line 100, in __setitem__
raise TypeError('Attempted to reuse key: %r' % key)
TypeError: Attempted to reuse key: 'a'
>>>
这是由于基本enum.EnumMeta.__prepare__
返回一个特殊的enum._EnumDict
而不是在键分配时表现不同的典型dict
对象。 您可能希望通过使用try
- except TypeError
包围它来抑制此错误消息,或者可能有一种方法可以在调用super().__prepare__(...)
之前修改命名空间。
这里已经有很多很好的答案,但这里有另一个纯粹使用Enum 的 Functional API的答案。
可能不是最漂亮的解决方案,但它避免了代码重复,开箱即用,不需要额外的包/库,它应该足以涵盖大多数用例:
from enum import Enum
class EventStatus(Enum):
success = 0
failure = 1
BookingStatus = Enum(
"BookingStatus",
[es.name for es in EventStatus] + ["duplicate", "unknown"],
start=0,
)
for bs in BookingStatus:
print(bs.name, bs.value)
# success 0
# failure 1
# duplicate 2
# unknown 3
如果您想明确分配的值,可以使用:
BookingStatus = Enum(
"BookingStatus",
[(es.name, es.value) for es in EventStatus] + [("duplicate", 6), ("unknown", 7)],
)
for bs in BookingStatus:
print(bs.name, bs.value)
# success 0
# failure 1
# duplicate 6
# unknown 7
我认为你可以这样做:
from typing import List
from enum import Enum
def extend_enum(current_enum, names: List[str], values: List = None):
if not values:
values = names
for item in current_enum:
names.append(item.name)
values.append(item.value)
return Enum(current_enum.__name__, dict(zip(names, values)))
class EventStatus(Enum):
success = 0
failure = 1
class BookingStatus(object):
duplicate = 2
unknown = 3
BookingStatus = extend_enum(EventStatus, ['duplicate','unknown'],[2,3])
关键点是:
另一种方式 :
Letter = Enum(value="Letter", names={"A": 0, "B": 1})
LetterExtended = Enum(value="Letter", names=dict({"C": 2, "D": 3}, **{i.name: i.value for i in Letter}))
或者 :
LetterDict = {"A": 0, "B": 1}
Letter = Enum(value="Letter", names=LetterDict)
LetterExtendedDict = dict({"C": 2, "D": 3}, **LetterDict)
LetterExtended = Enum(value="Letter", names=LetterExtendedDict)
输出 :
>>> Letter.A
<Letter.A: 0>
>>> Letter.C
Traceback (most recent call last):
File "<input>", line 1, in <module>
File "D:\jhpx\AppData\Local\Programs\Python\Python36\lib\enum.py", line 324, in __getattr__
raise AttributeError(name) from None
AttributeError: C
>>> LetterExtended.A
<Letter.A: 0>
>>> LetterExtended.C
<Letter.C: 2>
您不能扩展枚举,但可以通过合并它们来创建一个新枚举。
Tested for Python 3.6
from enum import Enum
class DummyEnum(Enum):
a = 1
class AnotherDummyEnum(Enum):
b = 2
def merge_enums(class_name: str, enum1, enum2, result_type=Enum):
if not (issubclass(enum1, Enum) and issubclass(enum2, Enum)):
raise TypeError(
f'{enum1} and {enum2} must be derived from Enum class'
)
attrs = {attr.name: attr.value for attr in set(chain(enum1, enum2))}
return result_type(class_name, attrs, module=__name__)
result_enum = merge_enums(
class_name='DummyResultEnum',
enum1=DummyEnum,
enum2=AnotherDummyEnum,
)
是的,您可以修改Enum
。 下面的示例代码有点 hacky,它显然依赖于Enum
的内部,它没有任何业务可以依赖。 另一方面,它有效。
class ExtIntEnum(IntEnum):
@classmethod
def _add(cls, value, name):
obj = int.__new__(cls, value)
obj._value_ = value
obj._name_ = name
obj.__objclass__ = cls
cls._member_map_[name] = obj
cls._value2member_map_[value] = obj
cls._member_names_.append(name)
class Fubar(ExtIntEnum):
foo = 1
bar = 2
Fubar._add(3,"baz")
Fubar._add(4,"quux")
具体来说,观察obj = int.__new__()
行。 enum
模块跳过了一些循环来为应该枚举的类找到正确的__new__
方法。 我们在这里忽略这些箍,因为我们已经知道整数(或者更确切地说,是int
的子类的实例)是如何创建的。
最好不要在生产代码中使用它。 如果必须,您确实应该添加防止重复值或名称的防护措施。
我想从 Django 的IntegerChoices
继承,由于“无法扩展枚举”的限制,这是不可能的。 我认为这可以通过一个相对简单的元类来完成。
CustomMetaEnum.py
:
class CustomMetaEnum(type):
def __new__(self, name, bases, namespace):
# Create empty dict to hold constants (ex. A = 1)
fields = {}
# Copy constants from the namespace to the fields dict.
fields = {key:value for key, value in namespace.items() if isinstance(value, int)}
# In case we're about to create a subclass, copy all constants from the base classes' _fields.
for base in bases:
fields.update(base._fields)
# Save constants as _fields in the new class' namespace.
namespace['_fields'] = fields
return super().__new__(self, name, bases, namespace)
# The choices property is often used in Django.
# If other methods such as values(), labels() etc. are needed
# they can be implemented below (for inspiration [Django IntegerChoice source][1])
@property
def choices(self):
return [(value,key) for key,value in self._fields.items()]
main.py
:
from CustomMetaEnum import CustomMetaEnum
class States(metaclass=CustomMetaEnum):
A = 1
B = 2
C = 3
print("States: ")
print(States.A)
print(States.B)
print(States.C)
print(States.choices)
print("MoreStates: ")
class MoreStates(States):
D = 22
pass
print(MoreStates.A)
print(MoreStates.B)
print(MoreStates.C)
print(MoreStates.D)
print(MoreStates.choices)
python3.8 main.py
:
States:
1
2
3
[(1, 'A'), (2, 'B'), (3, 'C')]
MoreStates:
1
2
3
22
[(22, 'D'), (1, 'A'), (2, 'B'), (3, 'C')]
Enum
的装饰器为了扩展Mikhail Bulygin 的答案,可以使用装饰器来扩展Enum
(并通过使用自定义Enum
基类来支持相等性)。
Enum
基类from enum import Enum
from typing import Any
class EnumBase(Enum):
def __eq__(self, other: Any) -> bool:
if isinstance(other, Enum):
return self.value == other.value
return False
Enum
类的装饰器from typing import Callable
def extend_enum(parent_enum: EnumBase) -> Callable[[EnumBase], EnumBase]:
"""Decorator function that extends an enum class with values from another enum class."""
def wrapper(extended_enum: EnumBase) -> EnumBase:
joined = {}
for item in parent_enum:
joined[item.name] = item.value
for item in extended_enum:
joined[item.name] = item.value
return EnumBase(extended_enum.__name__, joined)
return wrapper
>>> from enum import Enum
>>> from typing import Any, Callable
>>> class EnumBase(Enum):
def __eq__(self, other: Any) -> bool:
if isinstance(other, Enum):
return self.value == other.value
return False
>>> def extend_enum(parent_enum: EnumBase) -> Callable[[EnumBase], EnumBase]:
def wrapper(extended_enum: EnumBase) -> EnumBase:
joined = {}
for item in parent_enum:
joined[item.name] = item.value
for item in extended_enum:
joined[item.name] = item.value
return EnumBase(extended_enum.__name__, joined)
return wrapper
>>> class Parent(EnumBase):
A = 1
B = 2
>>> @extend_enum(Parent)
class ExtendedEnum(EnumBase):
C = 3
>>> Parent.A == ExtendedEnum.A
True
>>> list(ExtendedEnum)
[<ExtendedEnum.A: 1>, <ExtendedEnum.B: 2>, <ExtendedEnum.C: 3>]
从概念上讲,在这种意义上扩展枚举是没有意义的。 问题在于这违反了 Liskov 替换原则:子类的实例应该可以在任何可以使用基类 class 的实例的地方使用,但是BookingStatus
的实例不能可靠地用于任何需要EventStatus
的地方。 毕竟,如果该实例的值为BookingStatus.duplicate
或BookingStatus.unknown
,那么这将不是EventStatus
的有效枚举值。
我们可以创建一个新的 class,它通过使用功能性 API重用EventStatus
设置。一个基本示例:
event_status_codes = [s.name for s in EventStatus]
BookingStatus = Enum(
'BookingStatus', event_status_codes + ['duplicate', 'unknown']
)
这种方法对枚举值重新编号,忽略它们在EventStatus
中的内容。 我们还可以传递名称-值对以指定枚举值; 这让我们可以做更多的分析,以便重用旧值并自动编号新值:
def extend_enum(result_name, base, *new_names):
base_values = [(v.name, v.value) for v in base]
next_number = max(v.value for v in base) + 1
new_values = [(name, i) for i, name in enumerate(new_names, next_number)]
return Enum(result_name, base_values + new_values)
# Now we can do:
BookingStatus = extend_enum('BookingStatus', EventStatus, 'duplicate', 'unknown')
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.