How to correctly define a classmethod that accesses a value of a mangled child attribute?

196 Views Asked by At

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.

3

There are 3 best solutions below

2
Jonny Henly On BEST ANSWER

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
7
juanpa.arrivillaga On

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())
3
Ethan Furman On

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.