如何方便地将额外信息与 Enum 成员相关联?

How can I expediently associate extra information with Enum members?

每隔一段时间,我发现自己想写这样的东西:

import enum

class Item(enum.Enum):
    SPAM = 'spam'
    HAM  = 'ham'
    EGGS = 'eggs'

    @property
    def price(self):
        if self is self.SPAM:
            return 123
        elif self is self.HAM:
            return 456
        elif self is self.EGGS:
            return 789
        assert False

item = Item('spam')
print(item)          # Item.SPAM
print(item.price)    # 123

这正是我想要的:我有一个枚举Item,它的成员可以通过调用带有特定字符串的构造函数来获得,我可以获得每个[=13]的price =] 通过访问 属性。问题是,在编写 price 方法时,我必须再次枚举方法中的所有枚举成员。 (另外,使用这种技术将可变对象与成员相关联变得有点复杂。)

我可以这样写枚举:

import enum

class Item(enum.Enum):
    SPAM = ('spam', 123)
    HAM  = ('ham' , 456)
    EGGS = ('eggs', 789)

    @property
    def value(self):
        return super().value[0]
 
    @property
    def price(self):
        return super().value[1]

item = Item.SPAM
print(item)          # Items.SPAM
print(item.value)    # spam
print(item.price)    # 123

现在我不必在方法中重复成员。问题是,这样做我失去了通过调用 Item('spam'):

获得 Item.SPAM 的能力
>>> Item('spam')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python3.9/enum.py", line 360, in __call__
    return cls.__new__(cls, value)
  File "/usr/lib/python3.9/enum.py", line 677, in __new__
    raise ve_exc
ValueError: 'spam' is not a valid Item

这对我很重要,因为我希望能够将 Item class 作为 type= 关键字参数传递给 argparse.ArgumentParser.add_argument.

有没有办法在不重复的情况下将额外的值与枚举成员相关联,同时保留从“主要”值构造成员的能力?

你可以使用 __init__?

class Item(enum.Enum):
    SPAM = ('spam', 123)
    HAM  = ('ham' , 456)
    EGGS = ('eggs', 789)

    def __init__(self, item_type, price):
        self._type = item_type
        self._price = price

    @property
    def value(self):
        return self._type

    @property
    def price(self):
        return self._price


In []: item = Item.SPAM
In []: item.name, item.value, item.price
Out[]: ('SPAM', 'spam', 123)

In []: Item.SPAM
Out[]: <Item.SPAM: ('spam', 123)>

Item('spam') 不会工作,因为 EnumMeta.__call__ 不支持它。以下来自source from cpython 3.8 enum.py (l.313)

def __call__(cls, value, names=None, *, module=None, qualname=None, type=None, start=1):
    """
    Either returns an existing member, or creates a new enum class.

    This method is used both when an enum class is given a value to match
    to an enumeration member (i.e. Color(3)) and for the functional API
    (i.e. Color = Enum('Color', names='RED GREEN BLUE')).

    When used for the functional API:

    `value` will be the name of the new class.

    `names` should be either a string of white-space/comma delimited names
    (values will start at `start`), or an iterator/mapping of name, value pairs.

    `module` should be set to the module this class is being created in;
    if it is not set, an attempt to find that module will be made, but if
    it fails the class will not be picklable.

    `qualname` should be set to the actual location this class can be found
    at in its module; by default it is set to the global scope.  If this is
    not correct, unpickling will fail in some circumstances.

    `type`, if set, will be mixed in as the first base class.
    """
    if names is None:  # simple value lookup
        return cls.__new__(cls, value)
    # otherwise, functional API: we're creating a new Enum type
    return cls._create_(
            value,
            names,
            module=module,
            qualname=qualname,
            type=type,
            start=start,
            )

def __contains__(cls, member):
    if not isinstance(member, Enum):
        raise TypeError(
            "unsupported operand type(s) for 'in': '%s' and '%s'" % (
                type(member).__qualname__, cls.__class__.__qualname__))
    return isinstance(member, cls) and member._name_ in cls._member_map_ 
...

所以我会说 Item.get('spam')Item('spam') 更适合到达 Item.SPAM。如果查询不存在,它也会优雅地失败

    ...
    @property
    def __items(self):
        return {v.value: cls.__getattr__(k) for k, v in cls.__members__.items()}

    @classmethod
    def get(cls, item):
        return cls.__items.get(item)
    ...

In []: Item.get('spam')
Out[]: <Item.SPAM: ('spam', 123)>    
In []: Item.get('spamm') # returns None

使用标准库 Enum 您需要创建自己的 __new__:

import enum

class Item(enum.Enum):
    #
    SPAM = 'spam', 123
    HAM  = 'ham', 456
    EGGS = 'eggs', 789
    #
    def __new__(cls, value, price):
        obj = object.__new__(cls)
        obj._value_ = value
        obj.price = price
        return obj

如果这是您经常需要做的事情,您可以使用 aenum library1

import aenum

class Item(aenum.Enum):
    #
    _init_ = 'value price'
    #
    SPAM = 'spam', 123
    HAM  = 'ham', 456
    EGGS = 'eggs', 789

无论哪种方式,您都以:

>>> item = Item.SPAM

>>> print(item)          # Items.SPAM
Item.SPAM

>>> print(item.value)    # spam
spam

>>> print(item.price)    # 123
123

>>> Item('spam')
<Item.SPAM: 'spam'>

1 披露:我是 Python stdlib Enum, the enum34 backport, and the Advanced Enumeration (aenum) 库的作者。