Как я могу целесообразно связать дополнительную информацию с членами Enum?

#python #enums #idioms

#python #перечисления #идиомы

Вопрос:

Время от времени я ловлю себя на том, что хочу написать что-то вроде этого:

 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 каждого Item из них, обратившись к свойству. Проблема в том, что при написании price метода мне приходится снова перечислять все элементы enum в методе. (Кроме того, использование этого метода для связывания изменяемых объектов с элементами становится немного сложнее.)

Я мог бы вместо этого написать перечисление следующим образом:

 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= ключевого слова argparse.ArgumentParser.add_argument .

Есть ли способ связать дополнительные значения с членами enum, не повторяясь, сохраняя при этом возможность создавать элементы из их «первичных» значений?

Ответ №1:

Используя 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 библиотеку 1.

 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 , enum34 backport и библиотеки Advanced Enumeration ( aenum ).

Ответ №2:

вы можете использовать __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__ что не поддерживает его. Ниже приведен источник из 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('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
 

Комментарии:

1. Item('spam') поднимает кот TypeError .

2. Хорошо, я должен сказать, что это странный синтаксис для того, чего вы хотите достичь, я действительно не знаю, возможно ли это на самом деле, и я рекомендую не заниматься этим. Вместо этого я думаю, что у меня есть обходной путь, который может работать так же хорошо, не выглядя «выключенным» семантически. Поскольку это было то, что вы набрали, вызывайте впечатление, которое вы пытаетесь создать Item объект с spam параметром, вместо того, чтобы пытаться получить доступ Item.SPAM .