4

I'm trying to create an enum.Enum with lazy evaluation.

According to the docs:

An enumeration is a set of symbolic names (members) bound to unique, constant values. Within an enumeration, the members can be compared by identity, and the enumeration itself can be iterated over.

I would say my case still falls within this definition. The values are still unique and constant, I would just like them to be initialised only when necessary. This could be because initialisation is time-consuming, relies on other Enum members, has circular dependencies with other Enums, or (in my current case) because initialisation requires a running QGuiApplication, and I would like my module to still be importable (but keep that specific enum unusable) if one is not running (there are of course other ways to get around this specific requirement, but I'd like to implement this Enum class regardless).

Since we are only supposed to subclass EnumMeta in rare cases, I wanted to do this by subclassing Enum itself:

class Lazy:
    def __init__(self, value):
        self.value = value

class LazyEnum(Enum):
    def __getattribute__(self, name):
        result = super().__getattribute__(name)
        if name == 'value' and isinstance(result, Lazy):
            result = self._lazy_init(result.value)
            self.__setattr__(name, result)
        return result

The idea is to mark some values as Lazy to be initialised later. However, my sample code:

class MyEnum(LazyEnum):
    X = Lazy('x')
    Y = Lazy('y')
    Z = 'z'

    def _lazy_init(self, value):
        print(f"Evaluating {value}")
        return value * 2

print(MyEnum.X.value)
print(MyEnum.X)
print(MyEnum.X.value)

raises the error:

AttributeError: can't set attribute

How would I get around this issue?

Mate de Vita
  • 1,102
  • 12
  • 32
  • Try to use `object.__setattr__` – juanpa.arrivillaga Feb 08 '21 at 19:43
  • So you want the member to exist, but only have it's value set at a later time? – Ethan Furman Feb 08 '21 at 20:00
  • @juanpa.arrivillaga `object.__setattr__(name, result)` raises `TypeError: expected 2 arguments, got 1`. Changing it to `object.__setattr__(self, name, result)` again raises the same error described in the original post. The same goes for `setattr(self, name, result)`. – Mate de Vita Feb 08 '21 at 20:18
  • @EthanFurman I want the value to be initialised the first time the member's value is accessed (such as with `MyEnum.X.value`). – Mate de Vita Feb 08 '21 at 20:20
  • In your above example, is it the first `print(MyEnum.X.value)` or the second `print(MyEnum.X.value)` that raises the error? – Ethan Furman Feb 08 '21 at 20:48
  • It's the first one. Even if I delete the 2nd and 3rd `print` statement, the error still persists. – Mate de Vita Feb 09 '21 at 11:50
  • Will these "lazy" members get actual values all at once, or as needed? Could they get them all at once? (In other words, you load the GUI, then you can update all the members with their final values.) – Ethan Furman Feb 13 '21 at 04:19
  • I suppose that would be fine for all the described use cases, other than the computationally expensive initialisation one (and I'm not sure enums were intended for that particular use in any case). – Mate de Vita Feb 13 '21 at 15:11

1 Answers1

3

I looked at the source code of Enum and I solved this issue by setting the _value_ attribute directly, rather than the value property. The solution feels somewhat hacky to me, but it seems to work:

from abc import ABCMeta, abstractmethod
from enum import Enum, EnumMeta


class AbstractEnumMeta(EnumMeta, ABCMeta):
    pass


class Lazy:
    def __init__(self, *lazy_init_arguments):
        self.args = lazy_init_arguments


class LazyEnum(Enum, metaclass=AbstractEnumMeta):
    def __getattribute__(self, name):
        result = super().__getattribute__(name)
        if name == 'value' and isinstance(result, Lazy):
            result = self._lazy_init(*result.args)
            setattr(self, '_value_', result)
        return result

    @classmethod
    @abstractmethod
    def _lazy_init(cls, *args):
        return args


class MyEnum(LazyEnum):
    X = Lazy('x')
    Y = Lazy('y')
    Z = 'z'

    @classmethod
    def _lazy_init(cls, value):
        print(f"Lazy init of {value}")
        return value * 2


>>> MyEnum.Z
MyEnum.Z
>>> MyEnum.Z.value
'z'
>>> MyEnum.X
MyEnum.X
>>> MyEnum.X.value
Lazy init of x
'xx'
>>> MyEnum.X.value
'xx'
Mate de Vita
  • 1,102
  • 12
  • 32