【问题标题】:How to create Python Enum class from existing dict with additional methods?如何使用其他方法从现有 dict 创建 Python Enum 类?
【发布时间】:2019-03-18 11:43:03
【问题描述】:

假设我有一个预先存在的映射作为字典:

value_map = {'a': 1, 'b': 2}

我可以这样创建一个枚举类:

from enum import Enum
MyEnum = Enum('MyEnum', value_map)

并像这样使用它

a = MyEnum.a
print(a.value)
>>> 1
print(a.name)
>>> 'a'

然后我想为我的新枚举类定义一些方法:

def double_value(self):
    return self.value * 2

当然,我可以这样做:

class MyEnum(Enum):
    a = 1
    b = 2
    @property
    def double_value(self):
        return self.value * 2

但正如我所说,我必须使用预定义的值映射字典,所以我不能这样做。 如何做到这一点?我尝试从定义此方法的另一个类(如 mixin)继承,但我想不通。

【问题讨论】:

  • 你就不能Enum('MyEnum', {k: v * 2 for k, v in value_map.items()})吗?
  • 我可以,但是这个double_value只是一个虚拟的例子,我希望能够定义任何方法。
  • 我不确定是什么阻止了您定义此类方法?请提供一个您实际尝试实现的示例
  • 我不明白,你怎么能在那个方法上使用self.value。由于没有名为value 的实例变量:/
  • @DeepSpace 阻止我的是,如果我使用这个:MyEnum = Enum('MyEnum', value_map),那么我无法定义方法。但是如果我使用这个class MyEnum(Enum): ...,那么我必须明确地写出所有的值,并且不能使用我预定义的字典。

标签: python enums mixins


【解决方案1】:

您可以使用 type 参数将带有 mixin 方法的基本类型传入函数式 API:

>>> import enum
>>> value_map = {'a': 1, 'b': 2}
>>> class DoubledEnum:
...     @property
...     def double_value(self):
...         return self.value * 2
...
>>> MyEnum = enum.Enum('MyEnum', value_map, type=DoubledEnum)
>>> MyEnum.a.double_value
2

对于从不使用class 语句的全功能方法,您可以使用type() function 创建基本混合:

DoubledEnum = type('DoubledEnum', (), {'double_value': property(double_value)})
MyEnum = enum.Enum('MyEnum', value_map, type=DoubledEnum)

您也可以使用 enum.EnumMeta() 元类,就像 Python 在创建 class MyEnum(enum.Enum): ... 子类时一样:

  1. 使用元类__prepare__ hook 创建类字典
  2. 调用元类,传入类名、基类(此处为(enum.Enum,))和在步骤1中创建的类字典。

enum.EnumMeta 使用的自定义字典子类并不是真正为易于重用而设计的;它实现了一个__setitem__ 挂钩来记录元数据,但不会覆盖dict.update() 方法,因此我们在使用value_map 字典时需要小心:

import enum

def enum_with_extras(name, value_map, bases=enum.Enum, **extras):
    if not isinstance(bases, tuple):
        bases = bases,
    if not any(issubclass(b, enum.Enum) for b in bases):
        bases += enum.Enum,
    classdict = enum.EnumMeta.__prepare__(name, bases)
    for key, value in {**value_map, **extras}.items():
        classdict[key] = value
    return enum.EnumMeta(name, bases, classdict)

然后将double_value=property(double_value) 传递给该函数(连同枚举名称和value_map 字典):

>>> def double_value(self):
...     return self.value * 2
...
>>> MyEnum = enum_with_extras('MyEnum', value_map, double_value=property(double_value))
>>> MyEnum.a
<MyEnum.a: 1>
>>> MyEnum.a.double_value
2

否则,您可以创建没有成员的枚举的子类(任何 descriptor 都不是成员,因此函数、属性、类方法等),因此您可以定义一个没有成员的枚举:

class DoubledEnum(enum.Enum):
    @property
    def double_value(self):
        return self.value * 2

对于功能 API(例如 enum.Enum(..., type=DoubledEnum))和我编码为 enum_with_extras() 的元类方法来说,这是一个可接受的基类。

【讨论】:

  • 很好,谢谢!有没有可能在不必使用 enum_with_extras 函数的情况下做到这一点?我的意思是作为一个类定义?
  • @waszil:我猜测您可能想要什么,并添加了一个选项:添加一个没有成员的基本枚举类。
  • 这个类型参数方法是我想到的,非常感谢!
  • 还有一个问题:如果我想将此模式用于 IntEnum,我会得到:TypeError: object.__new__(MyEnum) is not safe, use int.__new__()
  • 知道了, DoubledEnum 也必须是 enum.IntEnum 的子类。
【解决方案2】:

您可以创建一个派生自 enum.EnumMeta(枚举的元类)的新元类(使用元元类或工厂函数,如下所示),并在创建类之前添加成员

import enum
import collections.abc


def enum_metaclass_with_default(default_members):
    """Creates an Enum metaclass where `default_members` are added"""
    if not isinstance(default_members, collections.abc.Mapping):
        default_members = enum.Enum('', default_members).__members__

    default_members = dict(default_members)

    class EnumMetaWithDefaults(enum.EnumMeta):
        def __new__(mcs, name, bases, classdict):
            """Updates classdict adding the default members and
            creates a new Enum class with these members
            """

            # Update the classdict with default_members
            # if they don't already exist
            for k, v in default_members.items():
                if k not in classdict:
                    classdict[k] = v

            # Add `enum.Enum` as a base class

            # Can't use `enum.Enum` in `bases`, because
            # that uses `==` instead of `is`
            bases = tuple(bases)
            for base in bases:
                if base is enum.Enum:
                    break
            else:
                bases = (enum.Enum,) + bases

            return super(EnumMetaWithDefaults, mcs).__new__(mcs, name, bases, classdict)

    return EnumMetaWithDefaults


value_map = {'a': 1, 'b': 2}


class MyEnum(metaclass=enum_metaclass_with_default(value_map)):
    @property
    def double_value(self):
        return self.value * 2


assert MyEnum.a.double_value == 2

另一种解决方案是直接尝试更新locals(),因为它被替换为在您尝试分配值时创建枚举值的映射。

import enum


value_map = {'a': 1, 'b': 2}


def set_enum_values(locals, value_map):
    # Note that we can't use `locals.update(value_map)`
    # because it's `locals.__setitem__(k, v)` that
    # creates the enum value, and `update` doesn't
    # call `__setitem__`.
    for k, v in value_map:
        locals[k] = v


class MyEnum(enum.Enum):
    set_enum_values(locals(), value_map)

    @property
    def double_value(self):
        return self.value * 2


assert MyEnum.a.double_value == 2

这似乎定义得很好,a = 1 很可能与locals()['a'] = 1 相同,但将来可能会改变。第一个解决方案更健壮且不那么 hacky(我还没有在其他 Python 实现中对其进行测试,但它可能工作方式相同)

【讨论】:

    【解决方案3】:

    PLUS:在@Artyer 的回答中添加更多内容(脏话)。 ?

    请注意,如果您从 dict 创建 Enum,您还可以为它提供“附加”功能,请参阅...

    from enum import Enum
    
    _colors = {"RED": (1, "It's the color of blood."), "BLUE": (2, "It's the color of the sky.")}
    
    
    def _set_members_colors(locals: dict):
        for k, v in colors.items():
            locals[k] = v[0]
    
    
    class Colors(int, Enum):
    
        _set_members_colors(locals())
    
        @property
        def description(self):
            return colors[self.name][1]
    
    print(str(Colors.RED))
    print(str(Colors.RED.value))
    print(str(Colors.RED.description))
    

    输出...

    Colors.RED
    1
    It's the color of blood.
    

    谢谢! ?

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-12-30
      • 2022-11-25
      • 2020-07-29
      • 2020-02-02
      • 1970-01-01
      相关资源
      最近更新 更多