Yu Chen
Yu Chen

Reputation: 7500

How to access an Enum's value directly as a class attribute?

I'm learning how to use Enum classes in Python, and have found that whenever I need to access the actual value of the enum, I need to append the .value property:

from enum import Enum
class Pets(Enum):
    DOG = "Fido"
    CAT = "Kitty"

Pets.DOG # yields Pets.DOG
Pets.DOG.value # yields Fido

As an exercise, I'm trying configure my Enum class so that I do not need to continually access that value property. My desired behavior is that when I call Pets.DOG, I get Fido as my value.

I'm tried to implement this with __getattr_(cls, item):

class Pets(Enum):

    def __getattr__(self, item):
        print(f"__getattr__ called with {item}")
        return getattr(self, item).value

    DOG = "Fido"
    CAT = "Kitty"


if __name__ == "__main__":

    pets = Pets()
    pets.DOG

However, I receive a RecursionError: maximum recursion depth exceeded while calling a Python object, and item is a string value of _value_. I'm not quite understanding why this behavior is happening - is this built in Python behavior, or because I am using a special class Enum?

I did take a look at a similar SO post, but the solutions there were to use another module (inspect), or access the __dict__ or dir() and parse it yourself with a combination of conditionals or regex. Is there a better way to access the underlying Enum's value?

Upvotes: 9

Views: 10427

Answers (3)

nog642
nog642

Reputation: 619

You can inherit from str, and the enum values will behave as strings in the same way that bools behave as ints (essentially every way except type() and repr()).

class Pets(str, Enum):
    DOG = "Fido"
    CAT = "Kitty"
>>> Pets.DOG
<Pets.DOG: 'Fido'>
>>> Pets.DOG == 'Fido'
True
>>> {'Fido': 5}[Pets.DOG]
5
>>> {Pets.DOG: 5}['Fido']
5

Upvotes: 1

Chris Santos-Lang
Chris Santos-Lang

Reputation: 71

You said, "My desired behavior is that when I call Pets.DOG, I get Fido as my value." Calling Pets.DOG really calls print(repr(Pets.DOG)), so my proposal is this:

class Pets(enum.Enum):
    DOG = "Fido"
    CAT = "Kitty"
    
    def __repr__(self):
        return self.value

One advantage to this approach is that you can still access the other features of Enum, such as Pets.DOG.name

That said, I would prefer to override __str__ rather than __repr__ because that achieves your goal of avoid typing .value, but leaves intact the ability to see all information about the Enum member when using repr().

I bumped into your question when trying to use namedtuples for my values instead of simple strings. In that case, I think __getattr__ is helpful, and just in case others find this post for a similar use, I am including what worked for me:

import enum
import collections

_ = lambda x : x

class XlateEnum(enum.Enum):
    """Enum whose members inherit the attributes of their values, 
        and which applies the assumed function _() for translations. 
    """
    
    def __getattr__(self, name):
        if name == "_value_":
            return enum.Enum.__getattribute__(self, name)
        elif hasattr(self, "_value_") and hasattr(self._value_, name):
                return _(getattr(self._value_, name))
        return enum.Enum.__getattribute__(self, name)
        
    def __setattr__(self, name, new_value):
        if name == "_value_":
            enum.Enum.__setattr__(self, name, new_value)
        elif hasattr(self, "_value_") and hasattr(self._value_, name):
            raise AttributeError("can't set attribute")
        else:
            enum.Enum.__setattr__(self, name, new_value)
            
    def __delattr__(self, name):
        if hasattr(self, "_value_") and hasattr(self._value_, name):
            raise AttributeError("can't delete attribute")
        else:
            enum.Enum.__delattr__(self, name)
            
    def __str__(self):
        return self.str if hasattr(self, "str") else _(enum.Enum.__str__(self.value))
    
class Pet(XlateEnum):
    """My pets. 

    Attributes:
        str: A localized str to name the Pet. How the Pet prints.
        my: A string representing what I call instances of this Pet.
        legs: The int number of legs of normal instances of this Pet.
    """
              
    DOG = collections.namedtuple("PetValue", "str my legs")(_("Dog"), "Fido", 4)
    CAT = collections.namedtuple("PetValue", "str my legs")(_("Cat"), "Kitty", 4)  
    
print(Pet.DOG)    # yields "Dog" (or translated string)
print(Pet.DOG.my) # yields "Fido" (which is not designated for translation)

Of course, you can remove the _() feature. I find Enum and namedtuple to very useful for constants because it keeps them in proper relationship to each other, and I like to build all of my translation functionality into the constants themselves, so code like this just works:

import ipywidgets
ipywidgets.Dropdown(options=Pet) 

Upvotes: 2

Martijn Pieters
Martijn Pieters

Reputation: 1124948

Don't use the enum class if you want to map attributes to strings. The whole point of the enum module is to produce a set of singleton objects that represent an enumeration, not strings. From the module documentation:

An enumeration is a set of symbolic names (members) bound to unique, constant values. Within an enumeration, the members can be compared by identity, and the enumeration itself can be iterated over.)

Bold emphasis mine. Strings are not unique, constant values (I can create more "Fido" strings at will) and are not designed to be compared by identity (even though sometimes, for a subset of strings, you can).

Just define your own class with attributes that are strings, directly:

class Pets:
    DOG = "Fido"
    CAT = "Kitty"

Your infinite recursion error is caused by a misunderstanding on your part as to what that method is used for. Like all special methods, object.attr looks up __getattr__ on the object type, meaning here that your method applies to instances of your Enum subclass, the DOG and CAT attributes here, not to the class itself, and interferes with the EnumMeta metaclass trying to test for the _value_ attibute, which is handled by your __getattr__ method with self being the newly-minted Pets.DOG instance, and item set to '_value_', which then calls getattr(Pets.DOG, '_value_'), which calls __getattr__, etc.

For your approach to work, you'd have to subclass EnumMeta and implement __getattribute__ on that subclass (__getattr__ is only ever called for missing attributes). However, take into account that __getattribute__ is used for all attribute access, so you have to take care to check for instances of the current class first:

class EnumDirectValueMeta(EnumMeta):
    def __getattribute__(cls, name):
        value = super().__getattribute__(name)
        if isinstance(value, cls):
            value = value.value
        return value

class Pets(Enum, metaclass=EnumDirectValueMeta):
    DOG = "Fido"
    CAT = "Kitty"

at which point Pets.DOG produces 'Fido'.

Upvotes: 11

Related Questions