Python Enum combination
Asked Answered
C

3

8

I would like to create a new Enum (IntEnum) class based on two existing ones. There is a working solution for this, like so:

from enum import unique, IntEnum
from itertools import chain
from collections import OrderedDict

@unique
class FirstEnumClass(IntEnum):
    a = 1
    b = 2

@unique
class SecondEnumClass(IntEnum):
    c = 3
    d = 4

# here a combined class is created:
CombinedEnumClass = unique(IntEnum('CombinedEnumClass', OrderedDict([(i.name, i.value) for i in chain(FirstEnumClass, SecondEnumClass)])))

My question: is there a fancy way to achieve this, so that there is a proper class definition? Like overriding some of the metaclass methods, or so? I would like something like this, so that docstring can also be given:

@unique
class CombinedEnumClass(IntEnum):
    """ docstring """
    # magic needed here

Any idea? Thanks!

Carycaryatid answered 6/9, 2017 at 10:56 Comment(3)
You don't need the OrderedDict, you can give just the list comprehension.Opera
I know, it works that way, the thing is that I'm using PyCharm, and it gives a warning if just a list is given: "Expected type 'Integral', got 'List[Tuple[Any, Any]]' instead", and OrderedDict solved that. But true, it works without that. Anyways, I still cannot manage to do this like a class definition.Carycaryatid
Possible duplicate of How to extend Python Enum?Scorify
L
1

The library prevents explicitly to do that:

Subclassing an enumeration is allowed only if the enumeration does not define any members.

Allowing subclassing of enums that define members would lead to a violation of some important invariants of types and instances. On the other hand, it makes sense to allow sharing some common behavior between a group of enumerations.

Therefore, I found a Stackoverflow answer using almost the same workaround as you do. I think this is the only way.

Lemnos answered 6/9, 2017 at 13:42 Comment(2)
Yeah, it seems like this is the only way. meanwhile I tried something like in this post, to subclass the EnumMeta metaclass itself and play with its __new__ method, but eventually all try runs into the same problem: TypeError: Cannot extend enumerations.Carycaryatid
Correct link: docs.python.org/3/howto/enum.html#restricted-enum-subclassingGaekwar
T
9

Python 2

Using a trick with vars() this can be accomplished:

class CombinedEnum(IntEnum):
    """ doc string """
    cls = vars()
    for member in chain(list(FirstEnumClass), list(SecondEnumClass)):
        cls[member.name] = member.value
    del member, cls

print(list(CombinedEnum))

This works because:

  • vars() returns the current namespace
  • modifying that namespace modifies the class

We delete member and cls because we don't want them to become members.


Python 3

The above doesn't (yet) work with the new Enum in Python3 3.4 and 3.5 (not sure about 3.6). The work-a-round is to use aenum instead, which allows us to tell Enum to ignore certain names:

from aenum import IntEnum

class CombinedEnum(IntEnum):
    """ doc string """
    _ignore_ = 'member cls'
    cls = vars()
    for member in chain(list(FirstEnumClass), list(SecondEnumClass)):
        cls[member.name] = member.value

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

Tupiguarani answered 6/9, 2017 at 17:5 Comment(8)
It looks good, but it does not work for me: File "C:\Anaconda3\lib\enum.py", line 66, in __setitem__ raise TypeError('Attempted to reuse key: %r' % key) TypeError: Attempted to reuse key: 'member' I tried to skip the cls[member.name] = member.value line and write a pass instead, but still the same is raised. I use Python 3.5.2 with Anaconda3.Carycaryatid
I tried it in Python 2.7, and it works there. Do you know a way for Python 3.x?Carycaryatid
@waszil: Thanks for finding that. I added a Python 3 work around.Tupiguarani
@EthanFurman, so why did you [restrict subclassing of enumerations?]( docs.python.org/3/library/…) in the first place. And what does _ignore_ var is doing? Thanks for your libraries.Spearing
@OwlMax: subclassing added complications with equality and identity, and we didn't feel it was a net win to allow it; The _ignore_ variable is telling the Enum metaclass to discard those names from the final enum class.Tupiguarani
@EthanFurman I have posted a question on enum about "Combine two or more enumeration classes in order to have more levels access". If you have some tips, I am infinitely grateful to you. Thanks in advance #71528481Aggiornamento
@EthanFurman, Tried this, and getting TypeError 'member' already defined as < FirstEnumClass.a: 1> (python v 3.12.2) Thanks in advance and thanks for your librariesAustralopithecus
@ArunPanneerselvam: Please ask a new question and show the details. Thanks.Tupiguarani
L
1

The library prevents explicitly to do that:

Subclassing an enumeration is allowed only if the enumeration does not define any members.

Allowing subclassing of enums that define members would lead to a violation of some important invariants of types and instances. On the other hand, it makes sense to allow sharing some common behavior between a group of enumerations.

Therefore, I found a Stackoverflow answer using almost the same workaround as you do. I think this is the only way.

Lemnos answered 6/9, 2017 at 13:42 Comment(2)
Yeah, it seems like this is the only way. meanwhile I tried something like in this post, to subclass the EnumMeta metaclass itself and play with its __new__ method, but eventually all try runs into the same problem: TypeError: Cannot extend enumerations.Carycaryatid
Correct link: docs.python.org/3/howto/enum.html#restricted-enum-subclassingGaekwar
A
0

In case of handling multiple Enum/IntEnum or IntFlag, to adjoin and create a new Enum, a simple work around using Stackoverflow answer.

from enum import Enum, IntFlag

def extend_flag(inherited,_type):
   def wrapper(final):
     joined = {}
     inherited.append(final)
     for i in inherited:
        for j in i:
           joined[j.name] = j.value
     return _type(final.__name__, joined)
   return wrapper

and when using this with Enum, it works just fine with Enum but behaves strangely with other Py versions, but works cool in version 3.9

class A(Enum):
   a = "one"
   b = "two"

class B(Enum):
   c = "three"
   d = "four"


@extend_flag([A,B], Enum)
class C(Enum):
   e = "five"

print(C('three'))
print(C.b)
print(list(C))

output:

C.c
C.b
[<C.a: 'one'>, C.b: 'two'>, <C.c: 'three'>, <C.d: 'four'>, <C.e: 'five'>]

Update: with the extend_flag decorator for multiple Enums/Flags, up on digging more, in version 3.9.6, both Enum and IntFlag works just fine.

from enum import Enum, IntFlag

class A(Enum):
   a = "one"
   b = "two"

class B(Enum):
   c = "three"
   d = "four"

class C(IntFlag):
   e = 0x05
   f = 0x0A4
   g = 0x0457C
   
class D(IntFlag):
   h = 0x07
   i = 0x0B12
   j = 0x04C

@extend_flag([A,B], Enum)
class E(Enum):
   h = "five"

@extend_flag([C,D], IntFlag)
class F(IntFlag):
   k = 0x09
   l = 0x0B2
if __name__ == '__main__':
   print(list(E))
   print(list(F))

and the output (Python 3.9.6),

[<E.a: 'one'>, <E.b: 'two'>, <E.c: 'three'>, <E.d: 'four'>, <E.h: 'five'>]
[<F.e: 5>, <F.f: 164>, <F.g: 17788>, <F.h: 7>, <F.i: 2834>, <F.j: 76>, <F.k: 9>, <F.l: 178>]

in version 3.12.2, the IntFlag members can be assigned as follows:

class First(IntFlag):
  m1 = 0x0001
  m2 = 0x0002
  m3 = 0x0004 
  m4 = 0x0008

class Second(IntFlag):
  m5 = 0x0010
  m6 = 0x0020
  m7 = 0x0040 
  m8 = 0x0080
  m9 = 0x0100
class Third(IntFlag):
  m10 = 0x200
  m11 = 0x400
  .
  ..
  (and so on..)

I'm not sure it's the right way to do it. But for me, it just serves the purpose, tho I couldn't figure out a better solution. Enum, IntEnum and IntFlag can also be combined using Union of types or by creating a new type using NewType in latest versions but that's out of scope.

Australopithecus answered 25/4 at 14:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.