Selecting Enum value from constructor?

Viewed 104

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 :)

2 Answers

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.

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
Related