UPDATE: One other thing to consider, which I think I like better, on balance, is to define subset membership as an attribute of each Enum member, like:
fruit = FRUIT.ORANGE # ---or whatever, probably in far away code---
...
if fruit.is_citrus:
make_juice()
These can be defined as @property
s on the class and don't suffer from the mutability problems mentioned below.
class FRUIT(Enum):
APPLE = 1
BANANA = 2
LEMON = 3
ORANGE = 4
@property
def is_citrus(self):
return self in frozenset((FRUIT.LEMON, FRUIT.ORANGE))
Thanks to the other respondents who all contributed very useful points of view. Here's what I ended up doing after considering the other answers, followed by my rationale:
from enum import Enum
class FRUIT(Enum):
APPLE = 1
BANANA = 2
LEMON = 3
ORANGE = 4
FRUIT.CITRUS_TYPES = frozenset((FRUIT.LEMON, FRUIT.ORANGE))
This works fine and (suprisingly to me) doesn't break any of the other Enum
behaviors:
# ---CITRUS_TYPES subset has desired behavior---
>>> FRUIT.LEMON in FRUIT.CITRUS_TYPES
True
>>> FRUIT.APPLE in FRUIT.CITRUS_TYPES
False
>>> "foobar" in FRUIT.CITRUS_TYPES
False
# ---CITRUS_TYPES has not become a member of FRUIT enum---
>>> tuple(FRUIT)
(FRUIT.APPLE: 1>, <FRUIT.BANANA: 2>, <FRUIT.LEMON: 3>, <FRUIT.ORANGE: 4>)
>>> FRUIT.APPLE in FRUIT
True
>>> FRUIT.CITRUS_TYPES in FRUIT
DeprecationWarning: using non-Enums in containment checks will raise TypeError in Python 3.8
False
# ---CITRUS_TYPES not reported by dir(FRUIT)---
>>> dir(FRUIT)
['APPLE', 'BANANA', 'LEMON', 'ORANGE', '__class__', '__doc__', '__members__', '__module__']
# ---But it does appear on FRUIT.__dict__---
FRUIT.__dict__ == {
'_generate_next_value_': <function Enum._generate_next_value_ at 0x1010e9268>,
'__module__': '__main__',
'__doc__': 'An enumeration.',
'_member_names_': ['APPLE', 'BANANA', 'LEMON', 'ORANGE'],
'_member_map_': OrderedDict([
('APPLE', <FRUIT.APPLE: 1>),
('BANANA', <FRUIT.BANANA: 2>),
('LEMON', <FRUIT.LEMON: 3>),
('ORANGE', <FRUIT.ORANGE: 4>)
]),
'_member_type_': <class 'object'>,
'_value2member_map_': {
1: <FRUIT.APPLE: 1>,
2: <FRUIT.BANANA: 2>,
3: <FRUIT.LEMON: 3>,
4: <FRUIT.ORANGE: 4>,
},
'APPLE': <FRUIT.APPLE: 1>,
'BANANA': <FRUIT.BANANA: 2>,
'LEMON': <FRUIT.LEMON: 3>,
'ORANGE': <FRUIT.ORANGE: 4>,
'__new__': <function Enum.__new__ at 0x1010e91e0>,
'CITRUS_TYPES': frozenset({<FRUIT.LEMON: 3>, <FRUIT.ORANGE: 4>})
}
So it appears to store CITRUS_TYPES
on the class, but hides it from dir()
for whatever reason.
It does have a vulnerability though, in that the added attribute is mutable, like any other class attribute; if some part of the client code assigns to FRUIT.CITRUS_TYPES
, FRUIT
would not complain and that would of course break things. This behavior is unlike that of an Enum
member, which raises AttributeError
on attempted assignment.
I thought this might be remedied by making it a classproperty
, which I ended up trying, but my early attempts did not prevent mutation. A more sophisticated classproperty implementation described there might work, but I ended up be satisfied with the simple approach above for now.
@blhsing raises an interesting question on whether such an attribute on FRUIT
makes sense. I see his point, and may yet adopt his view, but my current view is that localizing "fruit-related" characteristics to a single imported name is best for me. One could consider FRUIT
a rigorous set of fruit-types and subsets of fruit to therefore be a distinct set. I find that rigor unrewarding for my current purposes and prefer to think of FRUIT
as more of a collection of related constant values, including both members and subsets. YMMV of course. Like I say, I may yet adopt his view.