Is it possible to define a class constant inside an Enum?

后端 未结 5 947
感情败类
感情败类 2020-11-27 15:51

Python 3.4 introduces a new module enum, which adds an enumerated type to the language. The documentation for enum.Enum provides an example to demonstrate how i

相关标签:
5条回答
  • 2020-11-27 16:23
    from enum import Enum
    
    
    class classproperty(object):
        """A class property decorator"""
    
        def __init__(self, getter):
            self.getter = getter
    
        def __get__(self, instance, owner):
            return self.getter(owner)
    
    
    class classconstant(object):
        """A constant property from given value,
           visible in class and instances"""
    
        def __init__(self, value):
            self.value = value
    
        def __get__(self, instance, owner):
            return self.value
    
    
    class strictclassconstant(classconstant):
        """A constant property that is
           callable only from the class """
    
        def __get__(self, instance, owner):
            if instance:
                raise AttributeError(
                    "Strict class constants are not available in instances")
    
            return self.value
    
    
    class Planet(Enum):
        MERCURY = (3.303e+23, 2.4397e6)
        VENUS   = (4.869e+24, 6.0518e6)
        EARTH   = (5.976e+24, 6.37814e6)
        MARS    = (6.421e+23, 3.3972e6)
        JUPITER = (1.9e+27,   7.1492e7)
        SATURN  = (5.688e+26, 6.0268e7)
        URANUS  = (8.686e+25, 2.5559e7)
        NEPTUNE = (1.024e+26, 2.4746e7)
        def __init__(self, mass, radius):
            self.mass = mass       # in kilograms
            self.radius = radius   # in meters
    
        G = classconstant(6.67300E-11)
    
        @property
        def surface_gravity(self):
            # universal gravitational constant  (m3 kg-1 s-2)
            return Planet.G * self.mass / (self.radius * self.radius)
    
    
    print(Planet.MERCURY.surface_gravity)
    print(Planet.G)
    print(Planet.MERCURY.G)
    
    class ConstantExample(Enum):
        HAM  = 1
        SPAM = 2
    
    
        @classproperty
        def c1(cls):
            return 1
    
        c2 = classconstant(2)
    
        c3 = strictclassconstant(3)
    
    print(ConstantExample.c1, ConstantExample.HAM.c1)
    print(ConstantExample.c2, ConstantExample.SPAM.c2)
    print(ConstantExample.c3)
    
    
    # This should fail:
    print(ConstantExample.HAM.c3)
    

    The reason why @property does NOT work and classconstant DOES work is quite simple, and explained in the answer here

    The reason that the actual property object is returned when you access it via a class Hello.foo lies in how the property implements the __get__(self, instance, owner) special method. If a descriptor is accessed on an instance, then that instance is passed as the appropriate argument, and owner is the class of that instance.

    On the other hand, if it is accessed through the class, then instance is None and only owner is passed. The property object recognizes this and returns self.

    Thus, the code in classproperty is actually a generalization of property, lacking the if instance is None part.

    0 讨论(0)
  • 2020-11-27 16:31

    The most elegant solution (IMHO) is to use mixins / base class to provide the correct behavior.

    • base class to provide the behavior that's needed for all implementation that's common to e.g. Satellite and Planet.
    • mixins are interesting if you decide to provide optional behavior (e.g. Satellite and Planet may have to provide a different behavior)

    Here is an example, where you first define your behavior:

    #
    # business as usual, define your class, methods, constants...
    #
    class AstronomicalObject:
        # universal gravitational constant
        G = 6.67300E-11
        def __init__(self, mass, radius):
            self.mass = mass       # in kilograms
            self.radius = radius   # in meters
    
    class PlanetModel(AstronomicalObject):
        @property
        def surface_gravity(self):
            return self.G * self.mass / (self.radius * self.radius)
    
    class SatelliteModel(AstronomicalObject):
        FUEL_PRICE_PER_KG = 20000
        @property
        def fuel_cost(self):
            return self.FUEL_PRICE_PER_KG * self.mass
        def falling_rate(self, destination):
            return complicated_formula(self.G, self.mass, destination)
    

    Then create your Enum with the correct base classes / mixins.

    #
    # then create your Enum with the correct model.
    #
    class Planet(PlanetModel, Enum):
        MERCURY = (3.303e+23, 2.4397e6)
        VENUS   = (4.869e+24, 6.0518e6)
        EARTH   = (5.976e+24, 6.37814e6)
        MARS    = (6.421e+23, 3.3972e6)
        JUPITER = (1.9e+27,   7.1492e7)
        SATURN  = (5.688e+26, 6.0268e7)
        URANUS  = (8.686e+25, 2.5559e7)
        NEPTUNE = (1.024e+26, 2.4746e7)
    
    class Satellite(SatelliteModel, Enum):
        GPS1 = (12.0, 1.7)
        GPS2 = (22.0, 1.5)
    
    0 讨论(0)
  • 2020-11-27 16:37

    A property can be used to provide most of the behaviour of a class constant:

    class Planet(Enum):
    
        # ...
    
        @property
        def G(self):
            return 6.67300E-11
    
        # ...
    
        @property
        def surface_gravity(self):
            return self.G * self.mass / (self.radius * self.radius)
    

    This would be a little unwieldy if you wanted to define a large number of constants, so you could define a helper function outside the class:

    def constant(c):
        """Return a class property that returns `c`."""
        return property(lambda self: c)
    

    ... and use it as follows:

    class Planet(Enum):
    
        # ...
    
        G = constant(6.67300E-11)
    

    One limitation of this approach is that it will only work for instances of the class, and not the class itself:

    >>> Planet.EARTH.G
    6.673e-11
    >>> Planet.G
    <property object at 0x7f665921ce58>
    
    0 讨论(0)
  • 2020-11-27 16:40

    TLDR; NO, it can not be done inside an Enum class.

    This said, as the other answers showed, there are ways to get such class owned values associated to an Enum (i.e. via class inheritance / mixins) but such values are not "defined .. inside an Enum".

    0 讨论(0)
  • 2020-11-27 16:46

    This is advanced behavior which will not be needed in 90+% of the enumerations created.

    According to the docs:

    The rules for what is allowed are as follows: _sunder_ names (starting and ending with a single underscore) are reserved by enum and cannot be used; all other attributes defined within an enumeration will become members of this enumeration, with the exception of __dunder__ names and descriptors (methods are also descriptors).

    So if you want a class constant you have several choices:

    • create it in __init__
    • add it after the class has been created
    • use a mixin
    • create your own descriptor

    Creating the constant in __init__ and adding it after the class has been created both suffer from not having all the class info gathered in one place.

    Mixins can certainly be used when appropriate (see dnozay's answer for a good example), but that case can also be simplified by having a base Enum class with the actual constants built in.

    First, the constant that will be used in the examples below:

    class Constant:  # use Constant(object) if in Python 2
        def __init__(self, value):
            self.value = value
        def __get__(self, *args):
            return self.value
        def __repr__(self):
            return '%s(%r)' % (self.__class__.__name__, self.value)
    

    And the single-use Enum example:

    from enum import Enum
    
    class Planet(Enum):
        MERCURY = (3.303e+23, 2.4397e6)
        VENUS   = (4.869e+24, 6.0518e6)
        EARTH   = (5.976e+24, 6.37814e6)
        MARS    = (6.421e+23, 3.3972e6)
        JUPITER = (1.9e+27,   7.1492e7)
        SATURN  = (5.688e+26, 6.0268e7)
        URANUS  = (8.686e+25, 2.5559e7)
        NEPTUNE = (1.024e+26, 2.4746e7)
    
        # universal gravitational constant
        G = Constant(6.67300E-11)
    
        def __init__(self, mass, radius):
            self.mass = mass       # in kilograms
            self.radius = radius   # in meters
        @property
        def surface_gravity(self):
            return self.G * self.mass / (self.radius * self.radius)
    
    print(Planet.__dict__['G'])             # Constant(6.673e-11)
    print(Planet.G)                         # 6.673e-11
    print(Planet.NEPTUNE.G)                 # 6.673e-11
    print(Planet.SATURN.surface_gravity)    # 10.44978014597121
    

    And, finally, the multi-use Enum example:

    from enum import Enum
    
    class AstronomicalObject(Enum):
    
        # universal gravitational constant
        G = Constant(6.67300E-11)
    
        def __init__(self, mass, radius):
            self.mass = mass
            self.radius = radius
        @property
        def surface_gravity(self):
            return self.G * self.mass / (self.radius * self.radius)
    
    class Planet(AstronomicalObject):
        MERCURY = (3.303e+23, 2.4397e6)
        VENUS   = (4.869e+24, 6.0518e6)
        EARTH   = (5.976e+24, 6.37814e6)
        MARS    = (6.421e+23, 3.3972e6)
        JUPITER = (1.9e+27,   7.1492e7)
        SATURN  = (5.688e+26, 6.0268e7)
        URANUS  = (8.686e+25, 2.5559e7)
        NEPTUNE = (1.024e+26, 2.4746e7)
    
    class Asteroid(AstronomicalObject):
        CERES = (9.4e+20 , 4.75e+5)
        PALLAS = (2.068e+20, 2.72e+5)
        JUNOS = (2.82e+19, 2.29e+5)
        VESTA = (2.632e+20 ,2.62e+5
    
    Planet.MERCURY.surface_gravity    # 3.7030267229659395
    Asteroid.CERES.surface_gravity    # 0.27801085872576176
    

    Note:

    The Constant G really isn't. One could rebind G to something else:

    Planet.G = 1
    

    If you really need it to be constant (aka not rebindable), then use the new aenum library [1] which will block attempts to reassign constants as well as Enum members.


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

    0 讨论(0)
提交回复
热议问题