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

问题描述

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

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,它的成员可以通过调用带有某些字符串的构造函数获取,我可以获取每个price的{​​{1}}通过访问属性。问题是在编写 Item 方法时,我必须再次枚举方法中的所有枚举成员。 (此外,使用这种技术将可变对象与成员关联起来会变得有点复杂。)

我可以这样写枚举:

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 关键字参数传递给 type=

有没有一种方法可以将额外的值与枚举成员相关联而不重复我自己,同时保留从它们的“主要”值构造成员的能力?

解决方法

使用 stdlib 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',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 Enumenum34 backportAdvanced Enumeration (aenum) 库的作者。

,

你可以使用__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,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('spam') 最好去 Item.get('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