Python 3.4 introduit un nouveau module enum
, qui ajoute un type énuméré au langage. La documentation de enum.Enum
Fournit n exemple pour montrer comment il peut être étendu:
>>> 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
Cet exemple illustre également un problème avec Enum
: dans la méthode de propriété surface_gravity()
, une constante G
est définie qui serait normalement définie au niveau de la classe - mais en essayant de le faire à l'intérieur d'un Enum
l'ajouterait simplement comme l'un des membres de l'énumération, donc à la place, il a été défini dans la méthode.
Si la classe voulait utiliser cette constante dans d'autres méthodes, elle devrait également y être définie, ce qui n'est évidemment pas idéal.
Existe-t-il un moyen de définir une constante de classe dans un Enum
, ou une solution de contournement pour obtenir le même effet?
Il s'agit d'un comportement avancé qui ne sera pas nécessaire dans plus de 90% des énumérations créées.
Selon la documentation:
Les règles pour ce qui est autorisé sont les suivantes:
_sunder_
les noms (commençant et se terminant par un seul trait de soulignement) sont réservés par enum et ne peuvent pas être utilisés; tous les autres attributs définis dans une énumération deviendront membres de cette énumération, à l'exception de__dunder__
names etdescriptors
(les méthodes sont aussi des descripteurs).
Donc, si vous voulez une constante de classe, vous avez plusieurs choix:
__init__
descriptor
Création de la constante dans __init__
et l'ajouter après la création de la classe souffrent tous deux de ne pas avoir toutes les informations de classe rassemblées au même endroit.
Les mixins peuvent certainement être utilisés le cas échéant ( voir la réponse de dnozay pour un bon exemple ), mais ce cas peut également être simplifié en ayant une classe de base Enum
avec les constantes réelles intégrées.
Tout d'abord, la constante qui sera utilisée dans les exemples ci-dessous:
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)
Et l'exemple Enum à usage unique:
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
Et, enfin, l'exemple Enum multi-usage:
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
Remarque :
Le Constant
G
ne l'est vraiment pas. On pourrait relier G
à autre chose:
Planet.G = 1
Si vous avez vraiment besoin qu'elle soit constante (c'est-à-dire non réassignable), utilisez le nouvelle bibliothèque aenum [1] qui bloquera les tentatives de réaffectation de constant
s ainsi que de Enum
membres.
1 Divulgation: Je suis l'auteur du Python stdlib Enum
, le enum34
backport , et la bibliothèque Advanced Enumeration (aenum
) .
La solution la plus élégante (IMHO) est d'utiliser des mixins/classe de base pour fournir le comportement correct.
Satellite
et Planet
.Satellite
et Planet
peuvent avoir à fournir un comportement différent)Voici un exemple dans lequel vous définissez d'abord votre comportement:
#
# 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)
Créez ensuite votre Enum
avec les classes de base/mixins corrects.
#
# 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)
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)
La raison pour laquelle @property ne fonctionne PAS et classconstant fonctionne est assez simple, et expliquée dans le réponse ici
La raison pour laquelle l'objet de propriété réel est renvoyé lorsque vous y accédez via une classe Hello.foo réside dans la manière dont la propriété implémente la méthode spéciale
__get__(self, instance, owner)
. Si un descripteur est accédé sur une instance, alors cette instance est transmise comme argument approprié et le propriétaire est la classe de cette instance.D'un autre côté, s'il est accédé via la classe, alors instance vaut None et seul le propriétaire est passé. L'objet de propriété reconnaît cela et renvoie self.
Ainsi, le code dans classproperty
est en fait une généralisation de property
, sans la partie if instance is None
.
A property
peut être utilisé pour fournir la plupart du comportement d'une constante de classe:
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)
Ce serait un peu compliqué si vous vouliez définir un grand nombre de constantes, vous pouvez donc définir une fonction d'assistance en dehors de la classe:
def constant(c):
"""Return a class property that returns `c`."""
return property(lambda self: c)
... et utilisez-le comme suit:
class Planet(Enum):
# ...
G = constant(6.67300E-11)
Une limitation de cette approche est qu'elle ne fonctionnera que pour les instances de la classe, et non la classe elle-même:
>>> Planet.EARTH.G
6.673e-11
>>> Planet.G
<property object at 0x7f665921ce58>