#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
.