14

Is it possible to have an enum of enums in Python? For example, I'd like to have

enumA
    enumB
        elementA
        elementB
    enumC
        elementC
        elementD

And for me to be able to refer to elementA as enumA.enumB.elementA, or to refer to elementD as enumA.enumC.elementD.

Is this possible? If so, how?

EDIT: When implemented in the naive way:

from enum import Enum

class EnumA(Enum):
    class EnumB(Enum):
        member = 0

print(EnumA)
print(EnumA.EnumB.member)

It gives:

<enum 'EnumA'>
Traceback (most recent call last):
  File "Maps.py", line 15, in <module>
    print(EnumA.EnumB.member)
AttributeError: 'EnumA' object has no attribute 'member'
Leonora Tindall
  • 1,391
  • 2
  • 12
  • 30
  • First, you'll have to actually write this in Python syntax, using the stdlib `enum` module or a third-party one. So… have you tried it? What happens? – abarnert May 27 '15 at 01:58
  • I'm curious - why do you need this functionality in the first place? What is the specific use case for this? – inspectorG4dget May 27 '15 at 02:03
  • 1
    @inspectorG4dget I want to provide a system for classifying objects in my RPG engine's generic object system. For example one might classify an object as terrain.rocks.smallRock or as weapons.melee.swords.shortSword. Is there a better way? – Leonora Tindall May 27 '15 at 02:05
  • 1
    That seems like it should be dynamic information, not static—in other words, something you store in a string or a tuple, not as part of the type. Unless this is meant to be used for an embedded Python console or something? – abarnert May 27 '15 at 02:20
  • Object types, in the object engine, are immutable - though I'm now reconsidering that. – Leonora Tindall May 27 '15 at 03:26

7 Answers7

16

You can't do this with the enum stdlib module. If you try it:

class A(Enum):
    class B(Enum):
        a = 1
        b = 2
    class C(Enum):
        c = 1
        d = 2

A.B.a

… you'll just get an exception like:

AttributeError: 'A' object has no attribute 'a'

This is because the enumeration values of A act like instances of A, not like instances of their value type. Just like a normal enum holding int values doesn't have int methods on the values, the B won't have Enum methods. Compare:

class D(Enum):
    a = 1
    b = 2

D.a.bit_length()

You can, of course, access the underlying value (the int, or the B class) explicitly:

D.a.value.bit_length()
A.B.value.a

… but I doubt that's what you want here.


So, could you use the same trick that IntEnum uses, of subclassing both Enum and int so that its enumeration values are int values, as described in the Others section of the docs?

No, because what type would you subclass? Not Enum; that's already your type. You can't use type (the type of arbitrary classes). There's nothing that works.

So, you'd have to use a different Enum implementation with a different design to make this work. Fortunately, there are about 69105 different ones on PyPI and ActiveState to choose from.


For example, when I was looking at building something similar to Swift enumerations (which are closer to ML ADTs than Python/Java/etc. enumerations), someone recommended I look at makeobj. I forgot to do so, but now I just did, and:

class A(makeobj.Obj):
    class B(makeobj.Obj):
        a, b = makeobj.keys(2)
    class C(makeobj.Obj):
        c, d = makeobj.keys(2)

print(A.B, A.B.b, A.B.b.name, A.B.b.value)

This gives you:

<Object: B -> [a:0, b:1]> <Value: B.b = 1> b 1

It might be nice if it looked at its __qualname__ instead of its __name__ for creating the str/repr values, but otherwise it looks like it does everything you want. And it has some other cool features (not exactly what I was looking for, but interesting…).

abarnert
  • 354,177
  • 51
  • 601
  • 671
  • Thank you. That makes a lot more sense now. How would you suggest going about what I have in mind? See my comment on the OP. – Leonora Tindall May 27 '15 at 02:07
  • @SilverWingedSeraph: As I said at the end, I'd first look through PyPI and ActiveState at all of the other enum implementations and see if one of them has a design closer to what you want. What you're asking for seems _reasonable_, it's just not the design the stdlib went with, so there's a good chance someone else did. – abarnert May 27 '15 at 02:13
  • Thank you. I've decided to go with a different system for now, but I'll come back to this question at some point and take a look at those. – Leonora Tindall May 27 '15 at 02:19
  • Actually, it can be done. Mind you, I'm not saying it's a good idea (yet). Check my answer if your curious. – Ethan Furman Mar 09 '16 at 08:49
  • @EthanFurman Well, your answer doesn't actually have `Enum` values, it has `Constant` values. Of course you are using `Enum` types to _define_ those `Constant` instances, so it kind of looks like you're using Enums all the way down. I think that's likely to be more misleading than helpful, but it's worth having the answer--anyone who reads it and figured out how it works will learn some useful stuff, if nothing else. – abarnert Mar 09 '16 at 08:59
  • `Constant` is simply a descriptor. You wouldn't say the value returned by a `property` was a `property` instance, would you? No, you would say it was a `str` or a `float` or whatever type of value the `property` returned. – Ethan Furman Mar 09 '16 at 09:03
  • @EthanFurman Sure the value returned by `property` is a `property` instance. If `A.prop` is a property that returns a string, the type of `A().prop` is `str`, but the type of `A.prop` is `property`. (And neither one is the same type as the argument to @property, a function on self and type that returns a string.) Things are a little different with a non-data descriptor like yours, but still, the descriptor is a distinct thing. (If it weren't, why would you define a `__repr__` for it?) – abarnert Mar 09 '16 at 09:14
  • Oh, that was from the previous code to prove a point. You only see it of you access the descriptor directly through the class dictionary: `enumA.__dict__['enumB']`; if you do go through that hassle then the repr is nicer that ``. So, I concede the point that there is a descriptor in the way; but with Enums you're never going to see it directly unless you really, really go out of your way looking for it. – Ethan Furman Mar 09 '16 at 09:27
  • Problem solved: `aenum` includes a `skip` decorator which shelters an item from becoming a member, and the metaclass extracts the original value and places it into the class' `__dict__`. (See my update for the details.) – Ethan Furman Mar 16 '16 at 15:01
10

Note The below is interesting, and may be useful, but as @abarnert noted the resulting A Enum doesn't have Enum members -- i.e. list(A) returns an empty list.


Without commenting on whether an Enum of Enums is a good idea (I haven't yet decided ;) , this can be done... and with only a small amount of magic.

You can either use the Constant class from this answer:

class Constant:
    def __init__(self, value):
        self.value = value
    def __get__(self, *args):
        return self.value
    def __repr__(self):
        return '%s(%r)' % (self.__class__.__name__, self.value)

Or you can use the new aenum library and its built-in skip desriptor decorator (which is what I will show).

At any rate, by wrapping the subEnum classes in a descriptor they are sheltered from becoming members themselves.

Your example then looks like:

from aenum import Enum, skip

class enumA(Enum):
    @skip
    class enumB(Enum):
        elementA = 'a'
        elementB = 'b'
    @skip
    class enumC(Enum):
        elementC = 'c'
        elementD = 'd'

and you can then access them as:

print(enumA)
print(enumA.enumB)
print(enumA.enumC.elementD)

which gives you:

<enum 'enumA'>
<enum 'enumB'>
enumC.elementD

The difference between using Constant and skip is esoteric: in enumA's __dict__ 'enumB' will return a Constant object (if Constant was used) or <enum 'enumB'> if skip was used; normal access will always return <enum 'enumB'>.

In Python 3.5+ you can even (un)pickle the nested Enums:

print(pickle.loads(pickle.dumps(enumA.enumC.elementD)) is enumA.enumC.elementD)
# True

Do note that the subEnum doesn't include the parent Enum in it's display; if that's important I would suggest enhancing EnumMeta to recognize the Constant descriptor and modify its contained class' __repr__ -- but I'll leave that as an exercise for the reader. ;)

Ethan Furman
  • 63,992
  • 20
  • 159
  • 237
7

I made an enum of enum implementing de __ getattr __ in the base enum like this

def __getattr__(self, item):
    if item != '_value_':
        return getattr(self.value, item).value
    raise AttributeError

In my case I have an enum of enum of enum

class enumBase(Enum):
    class innerEnum(Enum):
        class innerInnerEnum(Enum):
           A

And

enumBase.innerEnum.innerInnerEnum.A

works

  • 3
    This for me is the best answer, as someone who has a huge library and dependent on enum, its difficult to change to a whole new library. Also ```return getattr(self.value, item).value``` if changed to ```return getattr(self.value, item)``` would be great! Value is accessible through enum, so the design remains constant. – kingspp Jul 11 '18 at 06:11
  • 1
    @kingspp: `aenum` is a drop-in replacement for the stdlib `enum`. – Ethan Furman Nov 26 '19 at 17:26
  • I knew it was possible. Thank you. – DonkeyKong Jul 13 '20 at 21:53
  • I didn't get why, but it works. Could you explain the mechanics? – ggguser Nov 20 '20 at 17:45
2

If you don't care about inheritance, here's a solution I've used before:

class Animal:
    class Cat(enum.Enum):
        TIGER = "TIGER"
        CHEETAH = "CHEETAH"
        LION = "LION"

    class Dog(enum.Enum):
        WOLF = "WOLF"
        FOX = "FOX"

    def __new__(cls, name):
        for member in cls.__dict__.values():
            if isinstance(member, enum.EnumMeta) and name in member.__members__:
                return member(name)
        raise ValueError(f"'{name}' is not a valid {cls.__name__}")

It works by overriding the __new__ method of Animal to find the appropriate sub-enum and return an instance of that.

Usage:

Animal.Dog.WOLF                    #=> <Dog.WOLF: 'WOLF'>
Animal("WOLF")                     #=> <Dog.WOLF: 'WOLF'>
Animal("WOLF") is Animal.Dog.WOLF  #=> True
Animal("WOLF") is Animal.Dog.FOX   #=> False
Animal("WOLF") in Animal.Dog       #=> True
Animal("WOLF") in Animal.Cat       #=> False
Animal("OWL")                      #=> ValueError: 'OWL' is not a valid Animal

However, notably:

isinstance(Animal.Dog, Animal)     #=> False

As long as you don't care about that this solution works nicely. Unfortunately there seems to be no way to refer to the outer class inside the definition of an inner class, so there's no easy way to make Dog extend Animal.

Milosz
  • 2,924
  • 3
  • 22
  • 24
1

You can use namedtuples to do something like this:

>>> from collections import namedtuple
>>> Foo = namedtuple('Foo', ['bar', 'barz'])
>>> Bar = namedtuple('Bar', ['element_a', 'element_b'])
>>> Barz = namedtuple('Barz', ['element_c', 'element_d'])
>>> bar = Bar('a', 'b')
>>> barz = Barz('c', 'd')
>>> foo = Foo(bar, barz)
>>> foo
Foo(bar=Bar(element_a='a', element_b='b'), barz=Barz(element_c='c', element_d='d'))
>>> foo.bar.element_a
'a'
>>> foo.barz.element_d
'd'

This is not a enum but, maybe solves your problem

drgarcia1986
  • 343
  • 1
  • 5
0

Solution based on attrs. This also allows to implement attributes validators and other goodies of attrs:

import enum

import attr


class CoilsTypes(enum.Enum):
    heating: str = "heating"


class FansTypes(enum.Enum):
    plug: str = "plug"


class HrsTypes(enum.Enum):
    plate: str = "plate"
    rotory_wheel: str = "rotory wheel"


class FiltersTypes(enum.Enum):
    bag: str = "bag"
    pleated: str = "pleated"


@attr.dataclass(frozen=True)
class ComponentTypes:
    coils: CoilsTypes = CoilsTypes
    fans: FansTypes = FansTypes
    hrs: HrsTypes = HrsTypes
    filter: FiltersTypes = FiltersTypes


cmp = ComponentTypes()
res = cmp.hrs.plate
zhukovgreen
  • 1,551
  • 16
  • 26
0

Try this:

# python3.7
import enum


class A(enum.Enum):
    def __get__(self, instance, owner):
        return self.value
    
    class B(enum.IntEnum):
        a = 1
        b = 2
    
    class C(enum.IntEnum):
        c = 1
        d = 2
    
    # this is optional (it just adds 'A.' before the B and C enum names)
    B.__name__ = B.__qualname__
    C.__name__ = C.__qualname__


print(A.C.d)        # prints: A.C.d
print(A.B.b.value)  # prints: 2
ElDomino
  • 1
  • 1