Published 2022-02-10.
Last modified 2022-03-10.
Time to read: 3 minutes.
This article demonstrates how to define additional properties for Python 3 enums. Defining an additional property in a Python enum can provide a simple way to provide string values. The concept is then expanded to demonstrate composition, an important concept for functional programming. This article concludes with a demonstration of dynamic dispatch in Python, by further extending an enum.
Adding Properties to Python Enums
Searching for python enum string value
yields some complex and arcane ways to approach the problem.
Below is a short example of a Python enum that demonstrates a simple way to provide lower-case string values for enum constants:
a new property, to_s
, is defined.
This property provides the string representation that is required.
You could define other properties and methods to suit the needs of other projects.
"""Defines enums"""
from enum import Enum, auto
class EntityType(Enum): """Types of entities""" SITE = auto() GROUP = auto() COURSE = auto() SECTION = auto() LECTURE = auto()
@property def to_s(self) -> str: """:return: lower-case name of this instance""" return self.name.lower()
Adding the following to the bottom of the program allows us to demonstrate it:
if __name__ == "__main__": # Just for demonstration print("Specifying individual values:") print(f" {EntityType.SITE.value}: {EntityType.SITE.to_s}") print(f" {EntityType.GROUP.value}: {EntityType.GROUP.to_s}") print(f" {EntityType.COURSE.value}: {EntityType.COURSE.to_s}") print(f" {EntityType.SECTION.value}: {EntityType.SECTION.to_s}") print(f" {EntityType.LECTURE.value}: {EntityType.LECTURE.to_s}")
print("\nIterating through all values:") for entity_type in EntityType: print(f" {entity_type.value}: {entity_type.to_s}")
Running the program produces this output:
$ cad_enums.py Specifying individual values: 1: site 2: group 3: course 4: section 5: lecture
Iterating through all values: 1: site 2: group 3: course 4: section 5: lecture
Easy!
Constructing Enums
Enum constructors work the same as other Python class constructors. There are several ways to make a new instance of a Python enum. Let's try two ways by using the Python interpreter. Throughout this article I've inserted a blank line between Python interpreter prompts for readability.
$ python Python 3.9.7 (default, Sep 10 2021, 14:59:43) [GCC 11.2.0] on linux Type "help", "copyright", "credits" or "license" for more information.
>>> from cad_enums import EntityType
>>> # Specify the desired enum constant value symbolically >>> gtype = EntityType.GROUP >>> print(gtype) EntityType.GROUP
>>> # Specify the desired enum constant value numerically >>> stype = EntityType(1) >>> print(stype) EntityType.SITE
Enum Ordering
A program I am working on needs to obtain the parent EntityType
.
By 'parent' I mean the EntityType
with the next lowest numeric value.
For example, the parent of EntityType.GROUP
is EntityType.SITE
.
We can obtain a parent enum by computing its numeric value
by adding the following method to the EntityType
class definition.
@property
def parent(self) -> 'EntityType':
""":return: entity type of parent; site has no parent"""
return EntityType(max(self.value - 1, 1))
The return type above is enclosed in quotes
('EntityType'
) to keep Python's type checker happy,
because this is a forward reference.
This is a forward reference because the type is referenced before it is fully compiled.
The complete enum class definition is now:
"""Defines enums"""
from enum import Enum, auto
class EntityType(Enum): """Types of entities""" SITE = auto() GROUP = auto() COURSE = auto() SECTION = auto() LECTURE = auto()
@property def to_s(self) -> str: """:return: lower-case name of this instance""" return self.name.lower()
@property def parent(self) -> 'EntityType': """:return: entity type of parent; site has no parent""" return EntityType(max(self.value - 1, 1))
Lets try out the new parent
property in the Python interpreter.
>>> EntityType.LECTURE.parent <EntityType.SECTION: 4>
>>> EntityType.SECTION.parent <EntityType.COURSE: 3>
>>> EntityType.COURSE.parent <EntityType.GROUP: 2>
>>> EntityType.GROUP.parent <EntityType.SITE: 1>
>>> EntityType.SITE.parent <EntityType.SITE: 1>
Enum Composition
Like methods and properties in all other Python classes, enum methods and properties compose if they return an instance of the class. Composition is also known as method chaining, and also can apply to class properties. Composition is an essential practice of a functional programming style.
The parent
property returns an instance of the EntityType
enum class,
so it can be composed with any other property or method of that class,
for example the to_s
property shown earlier.
>>> EntityType.LECTURE.parent.to_s 'section'
>>> EntityType.SECTION.parent.to_s 'course'
>>> EntityType.COURSE.parent.to_s 'group'
>>> EntityType.GROUP.parent.to_s 'site'
>>> EntityType.SITE.parent.to_s 'site'
Dynamic Dispatch
The Python documentation might lead someone to assume that writing dynamic dispatch code is more complex than it actually is.
To summarize the documentation, all Python classes, methods and instances are callable.
Callable
functions
have type Callable[[InputArg1Type, InputArg2Type], ReturnType]
.
If you do not want any type checking, write Callable[..., Any]
.
However, this is not very helpful information for dynamic dispatch.
Fortunately, working with Callable
is very simple.
You can pass around any Python class, constructor, function or method, and later provide it with the usual arguments. Invocation just works.
Let me show you how easy it is to write dynamic dispatch code in Python, let's construct one of five classes, depending on the value of an enum. First, we need a class definition for each enum value:
# pylint: disable=too-few-public-methods
class BaseClass(): """Demo only"""
class TestLecture(BaseClass): """This constructor has type Callable[[int, str], TestLecture]""" def __init__(self, id_: int, action: str): print(f"CadLecture constructor called with id {id_} and action {action}")
class TestSection(BaseClass): """This constructor has type Callable[[int, str], TestSection]""" def __init__(self, id_: int, action: str): print(f"CadSection constructor called with id {id_} and action {action}")
class TestCourse(BaseClass): """This constructor has type Callable[[int, str], TestCourse]""" def __init__(self, id_: int, action: str): print(f"CadCourse constructor called with id {id_} and action {action}")
class TestGroup(BaseClass): """This constructor has type Callable[[int, str], TestGroup]""" def __init__(self, id_: int, action: str): print(f"CadGroup constructor called with id {id_} and action {action}")
class TestSite(BaseClass): """This constructor has type Callable[[int, str], TestSite]""" def __init__(self, id_: int, action: str): print(f"CadSite constructor called with id {id_} and action {action}")
Now lets add another method, called construct
, to EntityType
that invokes
the appropriate constructor according to the value of an EntityType
instance:
@property def construct(self) -> Callable: """:return: the appropriate Callable for each enum value""" if self == EntityType.LECTURE: return TestLecture if self == EntityType.SECTION: return TestSection if self == EntityType.COURSE: return TestCourse if self == EntityType.GROUP: return TestGroup return TestSite
Using named arguments makes your code resistant to problems that might sneak in due to parameters changing over time.
I favor using named arguments at all times; it avoids many problems. As code evolves, arguments might be added or removed, or even reordered.
Let's test out dynamic dispatch in the Python interpreter.
A class specific to each EntityType
value is constructed by invoking the appropriate Callable
and passing it named arguments id_
and action
.
>>> EntityType.LECTURE.construct(id_=55, action="gimme_lecture") TestLecture constructor called with id 55 and action gimme_lecture <entity_types.TestLecture object at 0x7f9aac690070>
>>> EntityType.SECTION.construct(id_=13, action="gimme_section") TestSection constructor called with id 13 and action gimme_section <entity_types.TestSection object at 0x7f9aac5c1730>
>>> EntityType.COURSE.construct(id_=40, action="gimme_course") TestCourse constructor called with id 40 and action gimme_course <entity_types.TestCourse object at 0x7f9aac6900a0>
>>> EntityType.GROUP.construct(id_=103, action="gimme_group") TestGroup constructor called with id 103 and action gimme_group <entity_types.TestGroup object at 0x7f9aac4c6b20>
>>> EntityType.SITE.construct(id_=1, action="gimme_site") TestSite constructor called with id 1 and action gimme_site <entity_types.TestSite object at 0x7f9aac5c1730>
Because these factory methods return the newly created instance of the desired type, the string representation is printed on the console after the method finishes outputting its processing results, for example:
<entity_types.TestLecture object at 0x7f9aac690070>
.
Using enums to construct class instances and/or invoke methods (aka dynamic dispatch) is super powerful. It rather resembles generics, actually, even though Python’s support for generics is still in its infancy.
The complete Python program discussed in this article is here.