web-dev-qa-db-fra.com

Est-il possible de définir une constante de classe dans un Enum?

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?

51
Zero Piraeus

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 et descriptors (les méthodes sont aussi des descripteurs).

Donc, si vous voulez une constante de classe, vous avez plusieurs choix:

  • créez-le dans __init__
  • ajoutez-le après la création de la classe
  • utiliser un mixin
  • créez votre propre 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 ConstantG 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 constants 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) .

44
Ethan Furman

La solution la plus élégante (IMHO) est d'utiliser des mixins/classe de base pour fournir le comportement correct.

  • classe de base pour fournir le comportement nécessaire à toutes les implémentations communes à, par ex. Satellite et Planet.
  • les mixins sont intéressants si vous décidez de fournir un comportement optionnel (par exemple, 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)
15
dnozay
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.

9
Antti Haapala

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>
4
Zero Piraeus