Подтвердить что ты не робот

Можно ли определить константу класса внутри Enum?

В Python 3.4 представлен новый модуль enum, который добавляет перечисленный тип к языку. Документация для enum.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)
...     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 или какой-нибудь обходной путь для достижения того же эффекта?

4b9b3361

Ответ 1

Это расширенное поведение, которое не понадобится в 90 +% от созданных перечислений.

Согласно документам:

Правила для разрешенных: _sunder_ имена (начиная и заканчивая одним подчеркиванием) зарезервированы перечислением и не могут использоваться; все остальные атрибуты, определенные в перечислении, станут членами этого перечисления, за исключением имен __dunder__ и descriptors (методы также являются дескрипторами).

Итак, если вы хотите константу класса, у вас есть несколько вариантов:

  • создайте его в __init__
  • добавить его после создания класса
  • используйте mixin
  • создайте свой собственный descriptor

Создание константы в __init__ и добавление ее после того, как класс был создан, страдают от отсутствия всей информации о классе, собранной в одном месте.

Микшины, безусловно, могут быть использованы, когда это необходимо (см. dnozay answer для хорошего примера), но этот случай также можно упростить, имея базовый класс 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)

И пример однократного использования Enum:

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

И, наконец, многопользовательский пример Enum:

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

Примечание

Constant G действительно нет. Можно было бы перестроить G на что-то еще:

Planet.G = 1

Если вам действительно нужно, чтобы он был постоянным (он же не восстанавливается), используйте новую библиотеку aenum [1], которая блокирует попытки переназначить Constant, а также членов Enum.


[1] aenum записывается автором enum34.

Ответ 2

Наиболее элегантным решением (IMHO) является использование mixins/base class для обеспечения правильного поведения.

  • чтобы обеспечить поведение, которое необходимо для всей реализации, которая является общей для, например, Satellite и Planet.
  • mixins интересны, если вы решите предоставить необязательное поведение (например, Satellite и Planet, возможно, придется выполнять другое поведение)

Вот пример, где вы сначала определяете свое поведение:

#
# 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 с помощью базовых классов /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)

Ответ 3

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 НЕ работает, и работа с классом CONES довольно проста и объясняется в ответе здесь

Причина того, что фактический объект свойства возвращается при доступе он через класс Hello.foo заключается в том, как свойство реализует __get__(self, instance, owner) специальный метод. Если к экземпляру обращается дескриптор, то этот экземпляр передается как соответствующий аргумент, а владелец - это класс этого экземпляра.

С другой стороны, если он доступен через класс, то экземпляр Нет, и передается только владелец. Объект свойства распознает это и возвращает себя.

Таким образом, код в classproperty на самом деле является обобщением property, лишен части if instance is None.

Ответ 4

A property можно использовать для обеспечения большей части поведения константы класса:

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)

Это было бы немного громоздким, если бы вы хотели определить большое количество констант, поэтому вы могли бы определить вспомогательную функцию вне класса:

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

... и используйте его следующим образом:

class Planet(Enum):

    # ...

    G = constant(6.67300E-11)

Одним из ограничений этого подхода является то, что он будет работать только для экземпляров класса, а не для самого класса:

>>> Planet.EARTH.G
6.673e-11
>>> Planet.G
<property object at 0x7f665921ce58>

Ответ 5

TL;DR; НЕТ, это невозможно сделать внутри класса Enum.

Это сказало, как показали другие ответы, есть способы получить такие принадлежащие классу значения, связанные с Enum (то есть через наследование класса /mixins ), но такие значения не "определены.. внутри Enum".