4

I've inherited code that looks something like this.

class Clients(IntEnum):
    ALICE = 1
    BOB = 2
    PETER = 3
    CHERYL = 4
    LARRY = 5

if client_id == 1:
    client_info = find_info(Clients.ALICE.value)
elif client_id == 2:
    client_info = find_info(Clients.BOB.value)
elif client_id == 3:
    client_info = find_info(Clients.PETER.value)
elif client_id == 4:
    client_info = find_info(Clients.CHERYL.value)
elif client_id == 5:
    client_info = find_info(Clients.LARRY.value)
else:
    raise Exception('Unknown client_id.')

Not having much experience with Python enum, I have a burning desire to simplify this into something like this (pseudocode):

if client_id in dict(Clients).keys():
    client_info = find_info(client_id)
else:
    raise Exception('Unknown client_id.')

I've tried Clients.__members__ and Clients.__dict__, but they don't quite behave as I'd expect, returning something called a mappingproxy.

I can do 'ALICE' in Clients.__members__, but is there an equivalent for the values? If not, is there a more elegant way to write this bit of code?

Josh Friedlander
  • 10,870
  • 5
  • 35
  • 75
  • 1
    Possible duplicate of [How to convert int to Enum in python?](https://stackoverflow.com/questions/23951641/how-to-convert-int-to-enum-in-python) – meowgoesthedog Jan 10 '19 at 10:34
  • 2
    Will `client_id` always be the same as the client's `value`? If so, you don't need a mapping at all, as [enums allow access by value](https://docs.python.org/3.7/library/enum.html#programmatic-access-to-enumeration-members-and-their-attributes). – mportes Jan 10 '19 at 10:50
  • Maybe I should edit my question a bit. I've realised I can do `Clients(client_id).name` I guess my question is primarily how to validate the value. – Josh Friedlander Jan 10 '19 at 10:55

4 Answers4

7

You can store only values in a plain tuple (or list) and use the fact that enums allow access by value:

values = tuple(item.value for item in Clients)

if client_id in values:
    # do something with Clients(client_id)
else:
    # print message or raise exception

Or you can map values to enum members using a dictionary:

mapping = dict((item.value, item) for item in Clients)

if client_id in mapping:
    # do something with mapping[client_id]
else:
    # print message or raise exception
mportes
  • 1,589
  • 5
  • 13
3
try:
    client_info = find_info(Clients(client_id))
except ValueError:
    # customize error message
    raise Exception('Unknown client_id.')
Ethan Furman
  • 63,992
  • 20
  • 159
  • 237
  • I figure you're a good person to ask: in general, is there any Pythonic preference to an `if...else...` over `try...except...` (or vice versa)? – Josh Friedlander Jan 10 '19 at 14:49
  • 1
    @JoshFriedlander: If an exception could be raised, and I wan't to fix it myself or change the exception in some way, then I use `try/except`; `if/else` is for testing conditions which either won't raise exceptions or I don't care about catching them; likewise, just doing it if I don't care about catching anything -- so the above would be `client_info = find_info(Clients(client_id))` with no `try/except` and no `if/else`, and if a wrong value was specified then `ValueError('... is not a valid Clients')` would be raised. – Ethan Furman Jan 10 '19 at 17:29
  • See https://stackoverflow.com/q/11360858/1126841. – chepner Jun 06 '23 at 12:36
1

Here is how you can validate values with helper functions:

For the Enums:

class Customers(IntEnum):
    ABC = 1
    FOO = 2
    JDI = 3

class Clients(IntEnum):
    ALICE = 1
    BOB = 2
    PETER = 3

Implement helper functions:

from enum import IntEnum, EnumMeta
def isEnumClass(o) -> bool:
    return type(o) == EnumMeta

def isEnumMember(o) -> bool:
    return type(o).__class__ == EnumMeta

def enumHasMember(en, o, int_val:bool=False) -> bool:
    assert isEnumClass(en),\
        "param en is not an Enum class. Got %s!" \
            % ('(none)' if en is None else type(o).__name__)
    if type(o) == int:
        if not int_val: return False
        return len([m.value for m in en if m.value == o]) == 1
    else:
        return not o is None and o.__class__ == en

Usage:


print("1: %s" % enumHasMember(Customers, Customers.ABC))
print("2: %s" % enumHasMember(Customers, 1, int_val=False))
print("3: %s" % enumHasMember(Customers, 1, int_val=True))
print("4: %s" % enumHasMember(Customers, 4, int_val=True))
print("5: %s" % enumHasMember(Customers, Clients.ALICE))

> 1: True
> 2: False
> 3: True
> 4: False
> 5: False

Alternatively, if you control all of the code, you can create create a classmethod for a custom IntEnu class:

from enum import IntEnum, EnumMeta
class MyIntEnum(IntEnum):
    @classmethod
    def hasMember(cls, o, strict:bool=True) -> bool:
        if type(o) == int:
            if strict: return False
            return len([m.value for m in cls if m.value == o]) == 1
        else:
            return not o is None and o.__class__ == cls


class Clients(MyIntEnum):
    ALICE = 1
    BOB = 2
    PETER = 3
    CHERYL = 4
    LARRY = 5
    
class Customers(MyIntEnum):
    ABC = 1
    FOO = 2
    JDI = 3

Usage:

print("1: %s" % Customers.hasMember(Customers.ABC))
print("2: %s" % Customers.hasMember(1))
print("3: %s" % Customers.hasMember(1, strict=False))
print("4: %s" % Customers.hasMember(4, strict=False))
print("5: %s" % Customers.hasMember(Clients.ALICE))

> 1: True
> 2: False
> 3: True
> 4: False
> 5: False

Another handy class method would be validate method for a one line hard assertion:

class MyIntEnum(IntEnum):
        ...
    @classmethod
    def validate(cls, alias:str, o, strict:bool=True):
        assertNotBlank('alias', alias)
        assert cls.hasMember(o, strict),\
            f"Argument '{alias}' is not a member of MyIntEnum {cls.__module__}.{type(cls).__name__}. Got: {dump(o, True)}"
        
        return o

Please let me know if there is any issue with the above design. Just started researching this myself.

Timothy C. Quinn
  • 3,739
  • 1
  • 35
  • 47
0

I did something like this:

class Clients(IntEnum):
    ALICE = 1
    BOB = 2
    PETER = 3
    CHERYL = 4
    LARRY = 5

    @classmethod
    def values(self):       
        return [_.value for _ in list(self)]

    @classmethod
    def has(self, value):
        return value in self.values()


2 in Clients.values()  # True
22 in Clients.values() # False

Clients.has(2)  # True
Clients.has(22) # False

I usually implement both, because sometimes it makes sense to me in the code to say if Clients.has(id) and sometimes ( in forms for examples ) I want to test if the value given actually exists and provide an error accordingly so I say: if form_value in Clients.values()

So both do the same thing, just when you read the code afterwards it makes sense in term of context ...

Ricky Levi
  • 7,298
  • 1
  • 57
  • 65