可以在枚举中定义类常量吗?

2024-10-01 17:24:15 发布

您现在位置:Python中文网/ 问答频道 /正文

Python 3.4引入了一个新的模块^{},它向语言添加了一个enumerated typeenum.Enum的文档提供了an example来演示如何扩展它:

>>> 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
...     @property
...     def surface_gravity(self):
...         # universal gravitational constant  (m3 kg-1 s-2)
...         G = 6.67300E-11
...         return G * self.mass / (self.radius * self.radius)
...
>>> Planet.EARTH.value
(5.976e+24, 6378140.0)
>>> Planet.EARTH.surface_gravity
9.802652743337129

本例还演示了Enum的一个问题:在surface_gravity()属性方法中,定义了一个常量G,该常量通常在类级别定义,但是在Enum中尝试这样做只会将其添加为枚举的成员之一,因此它是在方法中定义的。

如果类想在其他方法中使用这个常量,它也必须在那里定义,这显然不是理想的。

有没有办法在一个Enum中定义一个类常量,或者有什么办法可以达到同样的效果?


Tags: 方法inself定义defenumsurfacegravity
3条回答

最优雅的解决方案(IMHO)是使用mixins/基类来提供正确的行为。

  • 基类提供所有公共实现所需的行为,例如SatellitePlanet
  • mixins are interesting如果决定提供可选行为(例如,SatellitePlanet可能必须提供不同的行为)

下面是一个示例,您首先定义自己的行为:

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

然后用正确的基类/混合创建Enum

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

这是高级行为,在创建的90%以上的枚举中不需要它。

根据文件:

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

所以如果你想要一个类常量,你有几个选择:

  • __init__中创建
  • 创建类后添加
  • 使用mixin
  • 创建自己的descriptor

__init__中创建常量并在创建类之后添加它都会因为没有在一个地方收集所有的类信息而受到影响。

mixin当然可以在适当的时候使用(see dnozay's answer for a good example),但是也可以通过使用内置了实际常量的基类Enum来简化这种情况。

首先,将在以下示例中使用的常数:

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)

以及一次性枚举示例:

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

最后,多用途枚举示例:

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

注意:

实际上,ConstantG不是这样的。可以将G重新绑定到其他对象:

Planet.G = 1

如果您真的需要它是常量(也称为不可重新绑定),那么使用new aenum library[1],它将阻止重新分配constants和Enum成员的尝试。


1披露:我是Python stdlib ^{}^{} backportAdvanced Enumeration (^{})库的作者。

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)

@property不起作用而classconstant起作用的原因非常简单,并在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.

因此,classproperty中的代码实际上是property的泛化,缺少if instance is None部分。

相关问题 更多 >

    热门问题