3

Extending the Enum example here, I can add a class method to select the correct enum when only part of its tuple is supplied:

class Planet1(Enum):
    MERCURY = (3.303e23, 2.4397e6)
    VENUS = (4.869e24, 6.0518e6)
    EARTH = (5.976e24, 6.37814e6)
    MARS = (6.421e23, 3.3972e6)

    @classmethod
    def from_partial(cls, mass=None, radius=None):
        for member in cls:
            if member.mass == mass or member.radius == radius:
                return member
        raise ValueError("No planet with these characteristics.")

    def __init__(self, mass, radius):
        self.mass = mass  # in kg
        self.radius = radius  # in m

p1 = Planet1.MARS
p2 = Planet1.from_partial(6.421e23)
p3 = Planet1.from_partial(radius=3.3972e6)
p1 is p2 is p3  # True

I'd like to be able to select one without using the .from_partial() method though. Is that possible?

What I've tried:

class Planet2(Enum):
    MERCURY = (3.303e23, 2.4397e6)
    VENUS = (4.869e24, 6.0518e6)
    EARTH = (5.976e24, 6.37814e6)
    MARS = (6.421e23, 3.3972e6)

    def __init__(self, mass=None, radius= None):
        if mass is None or radius is None:
            for member in Planet2:
                if member.mass == mass or member.radius == radius:
                    self.__dict__ = member.__dict__
        else:
            self.mass = mass  # in kg
            self.radius = radius  # in m

p2a = Planet2.MARS
p2b = Planet2(6.421e23)  # ValueError: 6.421e+23 is not a valid Planet2
p2c = Planet2(radius=3.3972e6) # TypeError: __call__() got an unexpected keyword argument 'radius'

Is there a way to do this, or does this go against what Enums are intended to be used for? The self.__dict__ = member.__dict__ looks dodgy, but I'd have thought it would work :)

ElRudi
  • 2,122
  • 2
  • 18
  • 33
  • Does this answer your question? [How can I override the enum constructor?](https://stackoverflow.com/questions/60172586/how-can-i-override-the-enum-constructor) – mkrieger1 Sep 10 '21 at 15:45
  • 2
    Why do you want to store the planet mass in an enum that identifies the planet? Seems like an unusual extension to the notion of an enumerated type. – jarmod Sep 10 '21 at 15:48
  • 1
    @jarmod, This is the example given in the official python docs. I think of a python enum as a (small) closed set of related information, rather than just an integer that is given a human-readable name. – ElRudi Sep 13 '21 at 05:47

2 Answers2

2

Edit for updated question:

The closest you can get would be option 2 below, but you would only be able to specify a value, not which one you wanted (so no keyword arguments).

Given that, I would stick with a separate from_partial() classmethod instead


You have a couple choices, depending on your use-case:

  1. Write your own __new__ and assign a number to _value_ and whatever else to other attributes:
    class Planet(Enum):
        #
        def __new__(cls, mass):
            value = len(cls.__members__) + 1
            member = object.__new__(cls)
            member._value_ = value
            member.mass = mass
            return obj
        #
        MERCURY = 3.303e23
        VENUS = 4.869e24
        EARTH = 5.976e24
        MARS = 6.421e23
  1. As of Python 3.6 you can add a _missing_ method instead of from_num:
    class Planet(Enum):
        #
        @classmethod
        def _missing_(cls, value):
            for member in cls:
                if member._value_[0] == value:
                    return member
        #
        MERCURY = 1, 3.303e23
        VENUS = 2, 4.869e24
        EARTH = 3, 5.976e24
        MARS = 4, 6.421e23

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

Ethan Furman
  • 63,992
  • 20
  • 159
  • 237
  • Ah, what an honour :) I see that I made my example too concise and your proposed solutions do not work in my actual use case. I've updated the question; it is now closer to the implementation in the docs as well. (I chose my original example to avoid comparing floats, but it seems there is no problem there; my updated `.from_partial` simply works. In my actual use-case, the value tuples contain strings, integers, and other enums.) – ElRudi Sep 13 '21 at 06:10
0

Enum values should be (ideally opaque) integers, so your use isn't as intended. For example, if created properly, Planet(3) does work:

from enum import Enum

class Planet(Enum):
    MERCURY = 1
    VENUS = 2
    EARTH = 3
    MARS = 4

class Planets:

    def __init__(self):
        ordered_masses = 3.303e23, 4.869e24, 5.976e24, 6.421e23
        self._data = dict((Planet(i),m) for i,m in enumerate(ordered_masses,1))

    def mass(self,planet):
        return _data[planet]

    def display(self):
        for k,v in self._data.items():
            print(k,v)

print(Planet(3))
Planets().display()

Output:

Planet.EARTH
Planet.MERCURY 3.303e+23
Planet.VENUS 4.869e+24
Planet.EARTH 5.976e+24
Planet.MARS 6.421e+23
Mark Tolonen
  • 166,664
  • 26
  • 169
  • 251
  • I agree that the common case for Enum is opaque values; however, if the OP's use wasn't intended I wouldn't have built in support for it, nor added the examples to the docs. – Ethan Furman Sep 10 '21 at 18:29
  • @EthanFurman Fair enough, you're the author, but still feels like a misuse of an enumeration and the separation of concerns principle. – Mark Tolonen Sep 10 '21 at 18:38
  • Thanks for your answer @MarkTolonen. I'm trying to store related information about a not-extendible set of values, and python enums seem perfect for that. It's the first time I'm using them, and I do get your point - feel free to comment [on my actual use case](https://codereview.stackexchange.com/questions/267948/using-python-enums-to) – ElRudi Sep 13 '21 at 06:54