关于python:可以在枚举中定义类常量吗?

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

python 3.4引入了一个新的模块enum,它向语言中添加了一个枚举类型。enum.Enum的文档提供了一个示例,演示如何扩展它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
>>> 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中有没有定义类常量的方法,或者一些变通方法来实现相同的效果?


这是高级行为,在创建的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__中创建
  • 创建类后添加它
  • 使用混音
  • 创建自己的descriptor

__init__中创建常量并在创建类之后添加常量,这两种方法都会导致没有将所有类信息收集到一个地方。

在适当的时候当然可以使用mixin(参见dnozay的回答中的一个好例子),但是这种情况也可以通过使用一个基本的Enum类和内置的实际常量来简化。

首先,将在下面的示例中使用的常量:

1
2
3
4
5
6
7
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)

以及一次性枚举示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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

最后,多用途枚举示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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重新绑定到其他东西上:

1
Planet.G = 1

如果您真的需要它是常量(即不可重新绑定),那么使用新的aenum库[1]来阻止重新分配ConstantEnum成员的尝试。

1公开:我是python stdlib Enumenum34backport和advanced enumeration(aenum库的作者。


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

  • 提供所有实现所需的行为的基类,如SatellitePlanet
  • 如果您决定提供可选的行为(例如,SatellitePlanet可能必须提供不同的行为),则mixin很有趣。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#
# 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#
# 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)


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
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起作用的原因很简单,并在这里的答案中进行了解释。

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部分。


property可用于提供类常量的大部分行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
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)

如果您想定义大量常量,这会有点笨拙,因此可以在类外部定义一个助手函数:

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

…使用方法如下:

1
2
3
4
5
class Planet(Enum):

    # ...

    G = constant(6.67300E-11)

这种方法的一个局限性是,它只适用于类的实例,而不适用于类本身:

1
2
3
4
>>> Planet.EARTH.G
6.673e-11
>>> Planet.G
<property object at 0x7f665921ce58>

tldr;不,它不能在枚举类内完成。

这也就是说,如其他答案所示,有一些方法可以获得与枚举关联的类拥有的值(即通过类继承/混合),但这些值没有"定义"。在枚举中"。