3

I'm trying to achieve the following behavior in a python "enum" (so far to no success):

Given the enum class

class MyEnum(enum.Enum):
    A=1
    B=2
    C=3

I want to have an "Other" member such that MyEnum(5) will be interpreted as "Other", while retaining the value 5, or,

>>> str(MyEnum(5))
... "<MyEnum.Other: 5>"

I thought of doing something along the line of overriding the _missing_ function, but I don't know how to create a "custom" instance of MyEnum without rewriting EnumMeta.

Advices will be appreciated.

EDIT: Following some comments that fail to understand my question precisely, I do not wish to have a default value for the enum, as this default value will not retain the value (that I wish to keep). I wish only that the value will be accepted with a default name.

EZLearner
  • 1,614
  • 16
  • 25
  • That does not look like an application for `Enum`. What exactly are you trying to do? – Ethan Furman Dec 09 '19 at 20:15
  • Why not? I'm trying to simplify the use of an internet enumerable set of constants, and have a default assignment given an unknown value. This allows for differentiating between known and unknown data in a simple and coherent way throughout the code, as well as allows for more encapsulated handling of unknown data (say, in a plugin or an outside module). – EZLearner Dec 09 '19 at 21:09
  • Check out [When and where to use Python Enum](https://stackoverflow.com/q/22586895/208880). It is possible to have a default [as shown in my answer here](https://stackoverflow.com/a/44871907/208880), but the invalid/unknown code is not saved. – Ethan Furman Dec 09 '19 at 21:21
  • What you wrote could be accomplished simply by overriding the `_missing_` sunder method in Python 3.4+, but a nice answer nonetheless. – EZLearner Dec 10 '19 at 09:31
  • Does this answer your question? [Is there a way to specify a default value for python enums?](https://stackoverflow.com/questions/44867597/is-there-a-way-to-specify-a-default-value-for-python-enums) – mkrieger1 Dec 10 '19 at 10:00
  • No, that's exactly what @EthanFurman referred me to. Anyway, I answered my own question, thanks. – EZLearner Dec 10 '19 at 10:09

2 Answers2

2

As the saying goes, if you want something done... I created the following enum subclass (I didn't add any new members so that's allowed):

class DefaultNameEnum(Enum):
    """Support for "Other"/default-name values"""

    @classmethod
    def _missing_(cls, value):
        possible_member = cls._value2member_map_.get(value, None)

        if possible_member is None:
            possible_member = cls._create_pseudo_member_(value)

        return possible_member

    @classmethod
    def _create_pseudo_member_(cls, value):
        """
        Create a default-name member.
        """

        default_member = cls._value2member_map_.get(None, None)

        if default_member is None:
            raise ValueError("%r is not a valid %s" % (value, cls.__name__))

        # construct a singleton enum pseudo-member
        other_member = object.__new__(cls)
        other_member._name_ = default_member._name_
        other_member._value_ = value

        # use setdefault in case another thread already created a composite
        # with this value
        other_member = cls._value2member_map_.setdefault(value, other_member)

        return other_member

    def __eq__(self, other):
        """Overrides the default implementation"""
        if isinstance(other, DefaultNameEnum):
            return self._name_ == other._name_
        return False

    def __ne__(self, other):
        return not self == other

This is based on the Flag enum subclass. Its usage is quite simple, actually - Just define as None whichever name you wish to have as your default. It is best illustrated using an example - consider the class:

class ABC(DefaultNameEnum):
    A = 1
    B = 2
    C = 3

    Other = None

Than, the following console calls will give:

>>> print([repr(mem) for mem in ABC])    
... ['<ABC.A: 1>', '<ABC.B: 2>', '<ABC.C: 3>', '<ABC.Other: None>']
>>> ABC(123)
... '<ABC.Other: 123>'
>>> ABC(1) == ABC(2)
... False
>>> ABC(123) == ABC.Other
... True
>>> ABC(123) == ABC(1374)
... True

If you wish to take this implementation and use it, note the following points:

  1. The behavior in the last line might be wanted and might not - depending on your usage. If this is an unwanted usage, just change the __eq__ method to compare names when either self._value_ or other._value_ are None.

  2. If you use this class, for the sake of representability you might wish for the default value's __repr__ to output '<ABC.Other>' rather than '<ABC.Other: None>' in the case of None value. This could easily be accomplished by overriding the __repr__ method.

  3. If you don't define a default member, the class will raise an exception upon calling it upon an unknown value (just like any Enum subclass).

I also wish to note that in the above implementation I would've preferred to use a sunder member such as _default_name_ or _default_value_member_ rather than assigning None, but alas the enum module does not permit defining a new sunder member for Enum subclasses.

EZLearner
  • 1,614
  • 16
  • 25
  • 1
    Kudos! I appreciate that your solution does not involve messing with `EnumMeta`. The docstring for `_create_pseudo_member_` is not correct, though. Also, I don't understand how you would have used `_default_value_`? (Note: you could have used a single leading underscore.) – Ethan Furman Dec 10 '19 at 17:54
  • You are correct about the docstring - I just took the `Flag`'s class implementation of this class as a basis (and hence the last line in this function which I guess solves a bug which I don't understand). I altered the docstring. Thanks! – EZLearner Dec 10 '19 at 20:35
  • Regarding `_default_value_`, I should've used a different naming, so I renamed these in the answer to perhaps clarify better. Two possibilities of additional members come to mind - either `_default_name_` that would contain a string representing the name of the "Other"-member; or `_default_value_member_` (again, not a good naming) that would contain the value whose enum-label should be the "Other"-label. For example, in this case, add `_default_value_member_=None` – EZLearner Dec 10 '19 at 20:35
0

This looks like what you're asking for:

>>> class Yep(enum.IntFlag):
...  A = 1
...  B = 2
... 
>>> Yep(5)
<Yep.4|A: 5>

So, Yep(5) is valid, but there won't be a Yep.Other magic member.

ForceBru
  • 43,482
  • 10
  • 63
  • 98
  • I thought of something of the sort (even though I don't want to restrict myself to integer values), but then I lose the equality-comparison and representabilty features. – EZLearner Dec 09 '19 at 18:26
  • @EZLearner, equality comparison works fine: `Yep(5) == Yep(5)` – ForceBru Dec 09 '19 at 18:30
  • Yes, but what I meant is to check if two different "Other"s are similar, and that doesn't happen here – EZLearner Dec 09 '19 at 21:05