Sunday, 15 October 2023

Python enums

Every now and then I go through the same basic doubts with enums in Python (how did I do that?, can I do that?) so it sounds sensible to write here those basic things (though I guess won't be more useful than what you can easily find at here). Python enums is another example (like abstract classes) of clever use of Metaclasses to provide features that in other languages have to be managed especifically by the compiler and extra syntax.

We create enums by inheriting from the enum.Enum class, that has EnumType (in older versions it was called EnumMeta) as metaclass. Then, when you define an enum by inheriting from Enum (let's say: class Color(Enum)), EnumType becomes its metaclass, and the __new__ method in the EnumType metaclass magically takes care of traversing the attributes that you have defined in your class (let's say: RED, BLUE...) and assigns to them instances of Color with the corresponding value. You can check the source code and see that it uses for that intermediate instances of the _proto_member class.

From the documentation:

The EnumType metaclass is responsible for providing the __contains__(), __dir__(), __iter__() and other methods that allow one to do things with an Enum class that fail on a typical class, such as list(Color) or some_enum_var in Color. EnumType is responsible for ensuring that various other methods on the final Enum class are correct (such as __new__(), __getnewargs__(), __str__() and __repr__()).

Thanks to having those dunder methods in the metaclass we can iterate an enum class, use the "in" operator and so on.

I guess we can think of an enum as a class with a limited number of instances, each one having a name and a value and where each instance is accessible as a static attribute of the class. Given a Query enum:


class Query(Enum):
    SELECT = "select"
    INSERT = "insert"
    UPDATE = "update"
    DELETE = "delete"
    
my_query = Query.INSERT
print(my_query.name) # INSERT
print(my_query.value) # insert


We can obtain the enum member corresponding to a name like this


# Obtain an enum member from its name:
ins1 = Query["INSERT"]


And the enum member corresponding to a value like this


# Obtain an enum member from its value:
ins2 = Query("insert")

And remember that we have a unique member for each value, so:


print(ins1) # Query.INSERT
print(ins2) # Query.INSERT
print(ins1 == ins2) # True
print(ins1 is ins2) # True

The operations above will cause an error if the name or value do not exist, so we should better write something like this:


try:
    ins1 = Query["INSSSEERRRRT"]
except KeyError as ex:
    print(f"error: {ex}")
    
# or:
name = "INSSSEERRRRT"
ins1 = Query[name] if any(name == query.name for query in Query) else None
print(f"ins1: {ins1}")

# ---------------------------

try:
    ins2 = Query("insssseeerrtt")
except ValueError as ex:
    print(f"error: {ex}")

# or:    
value = "insssseeerrtt"
ins2 = Query(value) if any(value == query.value for query in Query) else None
print(f"ins2: {ins2}")
    

The above makes me wonder why there is not a pair of static methods in the Enum class, something like names() and values() that would return me just that, the possible names and values of an enum.

The standard Python enums are enough in most cases, but if you want something more advanced like the Java/Kotlin enums, where we can have multiple attributes as values and have instance methods (the archetypical Java Planets example), you can use the powerful aenum module (advanced enums), that just implements the Planets example.

No comments:

Post a Comment