11

I would like to extend Python Enums to support non-defined values.

My use case: I want to benefit from Enums (e.g., being able to address/compare known entries with speaking names) but I also want it to support unknown values. If there's no name behind the value, str should simply print the str representation of the value and comparisons should fail.

Let me give you a short example what I'd like to have:

from enum import Enum

class Foo(Enum):
    A = 1
    B = 2
    C = 3

    def __str__(self):
        return self.name


print(Foo(1)) # prints 'A'
print(Foo(2)) # print 'B'
print(Foo(3)) # prints 'C'
print(Foo(1) == Foo.A) # prints 'true'

print(Foo(4)) # I'd expect '4'
print(Foo(123)) # I'd expect '123'
print(Foo(123) == Foo.A) # I'd expect False

Of course the last lines fail.

Is there a way to extend Enums, or is there maybe another easy pythonic way? (No third party libraries, please.)

5
  • 5
    This is counter to the use of enums, which are supposed to enumerate a set of known values. Commented May 5, 2019 at 21:35
  • Ok, got this -- is there another simple pythonic way to achieve my goal? Commented May 5, 2019 at 21:40
  • 3
    Please explain what you need this for -- it's very likely there's a more fitting solution. Commented May 5, 2019 at 22:26
  • 3
    Let me try. I parse magic constants (integers). Some of them are well-defined, and there are speaking names for them, some aren't. I need to check for some of those well-defined integers, and I want to use their speaking names during the comparison (for the sake of readability of code). Later on, I want to use type-safe representations of those values to generate config files. If the instance has a speaking name, print it. If not, simply use its raw integer value. Enums fulfil these requirements, as long as I don't have undefined values. But there are some. Hope that helps. Commented May 5, 2019 at 22:49
  • I can see that happening. Check my answer for a solution. Commented May 6, 2019 at 8:27

6 Answers 6

5

To make this work across multiple values of Python, particularly Python 2, you are going to need an external library: aenum1.

from aenum import Enum, extend_enum

class Foo(Enum):
    _order_ = 'A B C'   # because Python 2 does not keep order
    A = 1
    B = 2
    C = 3

    def __str__(self):
        return self.name

    @classmethod
    def _missing_(cls, value):
        extend_enum(cls, str(value), (value, ))
        return list(cls)[-1]

This dynamically adds the unknown value as an enum member and returns it.


1 Disclosure: I am the author of the Python stdlib Enum, the enum34 backport, and the Advanced Enumeration (aenum) library.

Sign up to request clarification or add additional context in comments.

2 Comments

works perfect, but as noted in my initial questions I'd like to avoid third party libraries.
@rralf: And as I noted in my answer, if you're going to support both Python 2 & 3 then you'll need a third-party library... unless you use a non-Enum solution or rewrite the whole thing yourself, of course.
3

Here's another answer that works in Python 3.6, 3.7 and 3.8.

It involves weird metaclass hackery, so be wary of that.

import enum

class TestMeta(enum.EnumMeta):
    def __call__(cls, value, names=None, *, module=None, qualname=None, type=None, start=1):
        if names is not None:
            # the enum is being constructed via the functional syntax
            return super().__call__(value, names=names, module=module, qualname=qualname, type=type, start=start)

        try:
            # attempt to get an enum member
            return super().__call__(value, names=names, module=module, qualname=qualname, type=type, start=start)
        except ValueError:
            # no such member exists, but we don't care
            return value

class Test(enum.Enum, metaclass=TestMeta):
    A = 5
    B = 6    

print(Test(5), Test(6), Test(7))

This version works in Python 2.7, but requires a third-party library for enum (see comments):

class TestMeta(enum.EnumMeta):
    def __call__(cls, value, names=None, module=None, type=None, start=1):
        if names is not None:
            return enum.EnumMeta.__call__(cls, value, names, module, type, start)

        try:    
            return enum.EnumMeta.__call__(cls, value, names, module, type, start)
        except ValueError:
            return value

7 Comments

Looks like a promising solution. In my question I forgot to mention that I also need to support python2, but I will accept this if there are no further solutions.
@rralf, I added a version that seems to work in Python 2
@rralf: There is no enum in Python 2.7, so which third-party library is being used?
@EthanFurman, yeah, it actually doesn't... Yet importing some kind of enum somehow works. Running help(enum) on the site doesn't give any info about the version of the library or any links.
@ForceBru: since EnumMeta is there, it's probably enum34, which is a bare-bones version of the Enum included in Python3.4. The aenum library actually has all the features of Enum present in the latest Python, plus a few extra things (such as extend_enum()).
|
3

how about ?

from enum import Enum


class CraftTypes(Enum):
    wood_work = 0
    welding = 1
    mechanics = 2
    unknown = 3
    # 3 and above is unknown


    @classmethod
    def _missing_(cls, number):
        return cls(cls.unknown)

simple and pythonic...

1 Comment

2

For your use case, a more natural solution would be to:

  • Get a complete list of number values present in the input (from parsing it)
  • Autogenerate a Enum class with Functional API using those values and known identifiers

E.g.:

known_values = {'a':1,'b':2,'c':1}    # only one of 'a' and 'c' will be used
                                      # 'a' guaranteed in 3.7+, implementation-specific before
                                      # (https://stackoverflow.com/q/39980323)
extracted_values = (1,2,3,4,5)

known_values_reverse = {}
for k,v in known_values.items():
    if v not in known_values_reverse:
        known_values_reverse[v]=k
    del k,v    #if placed outside the loop, will error if known_values is empty

for v in extracted_values:
    if v not in known_values_reverse:
        known_values_reverse[v]=str(v)
    del v

AutoFooEnum = enum.Enum(value='AutoFooEnum',names=(k,v for v,k in known_values_reverse.items()))

When producing the output, you'll need to use AutoFooEnum(value).name to output the textual ID, if any, or a number.

Naturally, you won't be able to refer to numbered members with AutoFooEnum.<id> in code since a number is not a valid identifier. But it doesn't look like you need that.

Comments

2

One could try to extend the enum at runtime with a new member following the example in enum.Flag._create_pseudo_member_:

from enum import Enum

class Foo(Enum):
    A = 1
    B = 2
    C = 3

    def __str__(self):
        return self.name

    @classmethod
    def _missing_(cls, value):
        if cls._member_type_ is object:
            # construct a new singleton enum member
            new_member = object.__new__(cls)
            # include the value representation in _name_ because the Enum hash implementation relies on _name_
            new_member._name_ = '%r' % value
            new_member._value_ = value
            # use setdefault in case another thread already created a composite with this value
            new_member = cls._value2member_map_.setdefault(value, new_member)
            return new_member

This simplistic implementation will break the possibility to mix Enum with other types in class definitions, but this could certainly be fixed following the machinery in enum.EnumMeta.__new__.

As already commented by @chepner, this breaks the promise

an Enum class is final once enumeration items have been defined

(also found in enum.EnumMeta.__new__) and expectedly breaks the behavior of some Enum methods such as __len__.

Comments

1

Here's a simple solution that I came up with for a similar requirement.

from enum import Enum

class Foo(Enum):
    A = 1
    B = 2
    C = 3

    def __str__(self):
        return self.name

    @classmethod
    def _missing_(cls, value):
        unknown_enum_val = object.__new__(cls)
        unknown_enum_val._name_ = str(value)
        unknown_enum_val._value_ = value
        return unknown_enum_val

print(Foo(1)) # prints 'A'
print(Foo(2)) # print 'B'
print(Foo(3)) # prints 'C'
print(Foo(1) == Foo.A) # prints 'true'

print(Foo(4)) # prints '4'
print(Foo(123)) # prints '123'
print(Foo(123) == Foo.A) # prints 'False'

print(isinstance(Foo(123), Foo)) # prints 'True'

One caveat is that equality doesn't work as one might expect. E.g.,

print(Foo(123) == Foo(123) # Prints 'False'

You can probably work around this with a custom __eq__ method.

1 Comment

Thanks, made my __eq__(self, other) function with return self.name == other.name and self.value == other.value in addition and all seems good, just instead of unknown_enum_val._name_ = str(value) I do unknown_enum_val._name_ = "UNKNOWN".

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.