4

In Python, how do I correctly define a classmethod of a parent class that references an attribute of a child class?

from enum import Enum


class LabelledEnum(Enum):
    @classmethod
    def list_labels(cls):
        return list(l for c, l in cls.__labels.items())


class Test(LabelledEnum):
    A = 1
    B = 2
    C = 3

    __labels = {
        1: "Label A",
        2: "Custom B",
        3: "Custom label for value C + another string",
    }


print(Test.list_labels())
# expected output
# ["Label A", "Custom B", "Custom label for value C + another string"]

In the code above I expect that Test.list_labels() will correctly print out the labels, however because the __labels dictionary is defined with the double underscore, I cannot access it correctly.

The reason I wanted to have double underscore is to make sure that the labels would not show up when iterating over the enumerator, e.g. list(Test) should not show the dictionary containing labels.

4
  • 1
    @juanpa.arrivillaga agree, but it's not that straight forward when it comes to Enum. From the documentation: "Private names are not converted to enum members, but remain normal attributes." -- Which is what the OP is after. Commented Jan 15, 2024 at 2:16
  • 2
    You could do something really ugly like getattribute(cls, f"_{cls.__name__}__labels", {}), not sure if it's guaranteed to work in every situation. Commented Jan 15, 2024 at 2:19
  • @JonnyHenly: thank you, yes, I've seen variations of this question for classes that do not inherit from Enum (e.g. stackoverflow.com/q/49503062/10693596), but I'm wondering about the Enum case... I do realise I might be going down the completely wrong path on this one. Commented Jan 15, 2024 at 2:19
  • @JonnyHenly: the "ugly" solution is good enough for my use case. Can you post it as an answer so I accept it? Commented Jan 15, 2024 at 2:22

3 Answers 3

4

While the getattr method works, a more elegant solution is to make LabelledEnum a mix-in instead, and define the label with the value:

from enum import Enum

class LabelledEnumMixin:

    labels = {}

    def __new__(cls, value, label):
        member = object.__new__(cls)
        member._value_ = value
        member.label = label
        cls.labels[value] = label
        return member

    @classmethod
    def list_labels(cls):
        return list(l for c, l in cls.labels.items())


class Test(LabelledEnumMixin, Enum):
    A = 1, "Label A"
    B = 2, "Custom B"
    C = 3, "Custom label for value C + another string"

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.

3 Comments

Thank you, I tried getting the mixin method working on my own and failed. This looks great!
Wow. With this unexpected answer I've dug up the source code to find that a tuple value is special-cased so that it can be unpacked as arguments to the constructor of the member's type. How can such an important feature be undocumented? Please make this behavior a publicly documented feature so we won't have to fear using what would currently be considered an implementation detail. Thanks!
Added a request for documentation in the Python issue tracker. Thanks.
3

Your best bet, if you are on Python >= 3.11 is to use enum.nonmember and a single underscore:

In [8]: import enum
   ...:
   ...: class LabelledEnum(enum.Enum):
   ...:     @classmethod
   ...:     def list_labels(cls):
   ...:         return list(l for c, l in cls._labels.items())
   ...:
   ...:
   ...: class Test(LabelledEnum):
   ...:     A = 1
   ...:     B = 2
   ...:     C = 3
   ...:
   ...:     _labels = enum.nonmember({
   ...:         1: "Label A",
   ...:         2: "Custom B",
   ...:         3: "Custom label for value C + another string",
   ...:     })
   ...:

In [9]: list(Test)
Out[9]: [<Test.A: 1>, <Test.B: 2>, <Test.C: 3>]

In [10]: Test.list_labels()
Out[10]: ['Label A', 'Custom B', 'Custom label for value C + another string']
If you are working with at least Python 3.7, you can use enum specific "sunder" names, and add `"_lables"` to the ignore list:
class Test(LabelledEnum):
    A = 1
    B = 2
    C = 3
    _ignore_ = ["_labels"]
    _labels = {
        1: "Label A",
        2: "Custom B",
        3: "Custom label for value C + another string",
    }
The other approach is to build up the string dynamically, which is quite clunky, although, it would work (frankly, `enum` should be ignoring *single* underscores, not double, but alas):
@classmethod
def list_labels(cls):
    labels = getattr(cls, f"_{cls.__name__}__labels"
    return list(labels.values())

7 Comments

thank you, I really like enum.nonmember!
I would accept this answer, it's better. I was looking for nonmember in Enum's documentation but couldn't find it.
@JonnyHenly: yes, but I felt bad since I already promised to accept yours. :)
@juanpa.arrivillaga I'm not sure if _ignore_ is a viable option. The documentation states: "_ignore_ – a list of names, either as a list or a str, that will not be transformed into members, and will be removed from the final class." Which makes it seem like _labels won't be an enum member of Test, but it also won't be an attribute of the Test class returned by Enum's meta class's __new__.
@JonnyHenly yup!
|
2

Note: This answer was originally a comment to the question.

I strongly advise taking a different approach, like:


Python 3.11+

I do not suggest using private names. That being said, if for some reason you must use private names and you can't use the @enum.nonmember decorator, which is a much better approach. Then the following will work in Python 3.11+.

The _Private__names section in Enum HOWTO states:

Private names are not converted to enum members, but remain normal attributes.

You could do something really ugly like:

getattr(cls, f"_{cls.__name__}__labels", {})
from enum import Enum

class LabelledEnum(Enum):
    @classmethod
    def list_labels(cls):
        # account for private name mangling
        labels = getattr(cls, f"_{cls.__name__}__labels", {})
        return list(l for c, l in labels.items())

class Test(LabelledEnum):
    A = 1
    __labels = { 1: "Label A" }


print(Test.list_labels())
# ['Label A']

Python < 3.11

In Python versions less than 3.11, __labels will become the _Test__labels enum member of Test. And the above code will raise an error, due to getattr returning the enum rather than a dict.

print(Test.__members__)
#{'A': <Test.A: 1>, '_Test__labels': <Test._Test__labels: {1: 'Label A'}>}

print(type(Test._Test__labels))
#<enum 'Test'>

Also, in Python 3.9 and 3.10, using private names in an enum class will cause a DeprecationWarning, similar to the following:

DeprecationWarning: private variables, such as '_Test__labels', will be normal attributes in 3.10

2 Comments

Just because you can doesn't mean you should...
@MatBailie I agree, edited my answer to (hopefully) reflect that sentiment. I was unaware of @enum.nonmember and there is no mention of it in Enum HOWTO, only the private names section. I endorsed another answer to be accepted in a comment.

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.