I am writing a class to represent sequential stages of an industrial process, composed of ADMISSION
, PROCESSING
, QC
and DELIVERY
stages.
Each stage has a unique, progressive sequence number, a mnemonic name and a field keeping track of the number of instances going through it:
@dataclass
class Stage:
seq_id: int
name: str
n_instances: int
Since stages are well-known and not supposed to change during execution, I decided to gather them in an Enum.
I need said enum to have the following requirements:
enum members need to:
be subclasses of Stage
, in order to avoid accessing their value
and making them easier to use (akin to IntEnum
or StrEnum
). In particular:
be comparable by their seq_id
(e.g. Stages.DELIVERY > Stages.PROCESSING
is true)
be usable as dictionary keys
use name
as their __str__
representation.
have immutable, sequential seq_id
s from 0 to n based on their declaration order
name
is specified at member declaration. Using auto()
results in the lower-cased member name
I managed to address point 2 and 3 in my implementation (see below).
How can I implement point 1 (and its subpoints)?
(fixed thanks to @EthanFurman's answer)
The idea is to use Stage
instances as the enum members, and use their seq_id
s as member values.
seq_id
is no longer in the Stage
class as it is stored directly
in the enum member's _value_
; I made this choice because I deem
seq_id
s not to have meaning outside the enum.
seq_id
cannot be specified at member declaration. Instead, it is automatically generated based on declaration order (thanks to auto-numbering enum pattern). This prevents invalid seq_ids from being specified
dunder methods __lt__
, __eq__
, __str__
and __hash__
have been implemented
inside the enum rather than the Stage
class because their behavior is tied to the use of the enum members
Hashing is based on the member's _value_
, which is supposedly
immutable (hashing Stage
would've been tricky due to its
mutability)
@dataclass
class Stage:
label: str
n_instances: int = 0
@total_ordering
class Stages(Stage, Enum): #req no. 1 (members are subclasses of stage)
#req no. 1.1
def __lt__(self, other):
if self.__class__ is other.__class__:
return self.value < other.value
return NotImplemented
def __eq__(self, other):
if self.__class__ is other.__class__:
return self.value == other.value
return NotImplemented
#req no. 1.2
def __hash__(self):
return hash(self.value)
#req no. 1.3
def __str__(self):
return self.label
#req no. 2
def __new__(cls, label):
#auto numbering enum pattern for seq_ids
value = len(cls.__members__) + 1
obj = Stage.__new__(cls)
#enum value set to seq_id
obj._value_ = value
return obj
#req no. 3
@override
def _generate_next_value_(name, start, count, last_values):
return name.lower()
ADMISSION = auto()
PROCESSING = auto()
QC = auto()
DELIVERY = auto()
#ordering test
assert(Stages.PROCESSING.__lt__(Stages.DELIVERY))
#dictionary key test
stage_to_color = {
Stages.ADMISSION : "#B10156",
Stages.PROCESSING : "#F4B704",
Stages.QC : "#FD0002",
Stages.DELIVERY : "#7FB857"
}
assert(stage_to_color[Stages.QC] == "#FD0002")
Both dataclass
and Enum
do a lot of work to make things simple for the user -- when you start extending and/or combining them you need to be careful.
Working code:
from dataclasses import dataclass
from enum import Enum, auto, unique
from functools import total_ordering
from typing import override
@dataclass
class Stage:
seq_id: int
label: str # name and value are reserved by Enum
n_instances: int = 0
# req 1.2
def __hash__(self):
return hash(self.seq_id)
@total_ordering #req no. 1 (members are subclasses of stage)
class Stages(Stage, Enum):
#req no. 1.1 (ordering)
def __lt__(self, other):
if self.__class__ is other.__class__:
return self.seq_id < other.seq_id
return NotImplemented
def __eq__(self, other):
if self.__class__ is other.__class__:
return self.seq_id == other.seq_id
return NotImplemented
#req no. 1.3
def __str__(self):
return self.label
#req nos. 2 & 3
@override
def _generate_next_value_(name, start, count, last_values):
return count, name.lower()
ADMISSION = auto()
PROCESSING = auto()
QC = auto()
DELIVERY = auto()