【问题标题】:Automatically setting an enum member's value to its name自动将枚举成员的值设置为其名称
【发布时间】:2015-11-19 18:48:59
【问题描述】:

我一直在搞乱 python 的枚举库并且遇到了一个难题。在文档中,他们展示了auto-numbering enum 的示例,其中定义了一些内容:

class Color(AutoNumber):
    red = ()
    green = ()
    ...

我想创建一个类似的类,但值会自动从成员的名称中设置,并保留您通过执行 strenum mixin stuff 获得的功能

比如:

class Animal(MagicStrEnum):
    horse = ()
    dog = ()

Animal.dog == 'dog' # True

我查看了 enum 模块的源代码,并尝试了很多变体来处理 __new__EnumMeta

【问题讨论】:

  • 你的意思是Animal.dog.value == 'dog'
  • 我认为可以通过修改 enum.py (hg.python.org/cpython/file/3.4/Lib/enum.py#l295) 中的 EnumMeta._create_() 来完成,但是由于来自 _is_sunder() 的拆分名称上的 ValueError 无法立即覆盖以保护但是,如果 names = ['red','green','blue'] 并且 Color = Enum('Color', names=zip(names,names)),那么 Color.red.value == 'red' , Color.green.value == 'green' 和 Color.blue.value == 'blue'。
  • 你想在这里做什么?

标签: python enums metaprogramming metaclass


【解决方案1】:

更新:2017-03-01

在 Python 3.6(和 Aenum 2.01)中添加了 FlagIntFlag 类;其中一部分是一个新的auto() helper,它使这变得非常简单:

>>> class AutoName(Enum):
...     def _generate_next_value_(name, start, count, last_values):
...         return name
...
>>> class Ordinal(AutoName):
...     NORTH = auto()
...     SOUTH = auto()
...     EAST = auto()
...     WEST = auto()
...
>>> list(Ordinal)
[<Ordinal.NORTH: 'NORTH'>, <Ordinal.SOUTH: 'SOUTH'>, <Ordinal.EAST: 'EAST'>, <Ordinal.WEST: 'WEST'>]

原答案

AutoStr 类的困难在于枚举成员的名称没有传递到创建它的代码中,因此无法使用。另一个问题是str 是不可变的,因此我们无法在创建这些类型的枚举后更改它们(例如,通过使用class decorator)。

最简单的方法是使用Functional API

Animal = Enum('Animal', [(a, a) for a in ('horse', 'dog')], type=str)

这给了我们:

>>> list(Animal)
[<Animal.horse: 'horse'>, <Animal.dog: 'dog'>]

>>> Animal.dog == 'dog'
True

下一个最简单的事情是,假设您想为将来的枚举使用创建一个基类,就像我的DocEnem

class DocEnum(Enum):
    """
    compares equal to all cased versions of its name
    accepts a doctring for each member
    """
    def __new__(cls, *args):
        """Ignores arguments (will be handled in __init__)"""
        obj = object.__new__(cls)
        obj._value_ = None
        return obj

    def __init__(self, doc=None):
        # first, fix _value_
        self._value_ = self._name_.lower()
        self.__doc__ = doc

    def __eq__(self, other):
        if isinstance(other, basestring):
            return self._value_ == other.lower()
        elif not isinstance(other, self.__class__):
            return NotImplemented
        return self is other

    def __hash__(self):
        # keep DocEnum hashable
        return hash(self._value_)

    def __ne__(self, other):
        return not self == other

并在使用中:

class SpecKind(DocEnum):
    REQUIRED = "required value"
    OPTION = "single value per name"
    MULTI = "multiple values per name (list form)"
    FLAG = "boolean value per name"
    KEYWORD = 'unknown options'

请注意,与第一个选项不同,DocEnum 成员不是 strs。


如果您想以艰难的方式做到这一点:子类EnumMeta 并在创建成员之前修改Enum 的新类字典:

from enum import EnumMeta, Enum, _EnumDict

class StrEnumMeta(EnumMeta):
    def __new__(metacls, cls, bases, oldclassdict):
        """
        Scan through `oldclassdict` and convert any value that is a plain tuple
        into a `str` of the name instead
        """
        newclassdict = _EnumDict()
        for k, v in oldclassdict.items():
            if v == ():
                v = k
            newclassdict[k] = v
        return super().__new__(metacls, cls, bases, newclassdict)

class AutoStrEnum(str, Enum, metaclass=StrEnumMeta):
    "base class for name=value str enums"

class Animal(AutoStrEnum):
    horse = ()
    dog = ()
    whale = ()

print(Animal.horse)
print(Animal.horse == 'horse')
print(Animal.horse.name, Animal.horse.value)

这给了我们:

Animal.horse
True
horse horse

1 披露:我是Python stdlib Enumenum34 backportAdvanced Enumeration (aenum) 库的作者。

【讨论】:

  • @VillasV:感谢您发现该错误!很抱歉,审稿人没有看到这是一个正确的编辑。
  • 绝妙的答案!只有一个问题:虽然是Enum 的子类,DocEnum 似乎没有__hash__ 方法。我必须手动复制 Enum.__hash__ 以使 DocEnum 可散列。这是为什么呢?
  • @ComeOnGetMe:在 Python 3 中,如果定义了 __eq__,则 __hash__ 设置为 None,除非在类中定义了 __hash__(或设置为其他内容)。答案已更新。
  • 这在 3.9 中不再有效,因为私有 _EnumDict 已更改。
  • @gps:我刚刚测试了我的答案中最顶级的解决方案,以及我的答案中的原始解决方案,来自 Pythons 3.6 - 3.11,并且在每个版本中都有效。您到底尝试了什么?
【解决方案2】:

也许您正在寻找由Enum 类自动提供的name 属性

>>> class Animal(Enum):
...     ant = 1
...     bee = 2
...     cat = 3
...     dog = 4
...

>>> Animal.ant.name == "ant"
True

但如果你真的想在脚上开枪。而且我敢肯定,这将引入整个世界的陷阱(我已经消除了最明显的一个)。

from enum import Enum, EnumMeta, _EnumDict

class AutoStrEnumDict(_EnumDict):
    def __setitem__(self, key, value):
        super().__setitem__(key, key)

class AutoStrEnumMeta(EnumMeta):
    @classmethod
    def __prepare__(metacls, cls, bases):
        return AutoStrEnumDict()
    def __init__(self, name, bases, attrs):
        super().__init__(name, bases, attrs)
        # override Enum.__str__
        # can't put these on the class directly otherwise EnumMeta overwrites them
        # should also consider resetting __repr__, __format__ and __reduce_ex__
        if self.__str__ is not str.__str__:
            self.__str__ = str.__str__

class AutoStrNameEnum(str, Enum, metaclass=AutoStrEnumMeta):
    pass

class Animal(AutoStrNameEnum):
    horse = ()
    dog = ()

print(Animal.horse)
assert Animal.horse == "horse"
assert str(Animal.horse) == "horse" 
# and not equal to "Animal.horse" (the gotcha mentioned earlier)

【讨论】:

  • 您对_EnumDict的修改使得无法添加方法,因为它们也会被转换为strs。
  • 交通便利。要么检查该值是否是一个函数。或者为了使其更健壮,创建一个哨兵值,并且仅在该值为哨兵时进行转换。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-11-16
  • 1970-01-01
  • 1970-01-01
  • 2016-03-04
相关资源
最近更新 更多