web-dev-qa-db-ja.com

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内では、それを列挙型のメンバーの1つとして追加するだけなので、代わりにメソッド内で定義されます。

クラスがこの定数を他のメソッドで使用したい場合は、そこでも定義する必要がありますが、これは明らかに理想的ではありません。

Enum内でクラス定数を定義する方法、または同じ効果を達成するための回避策はありますか?

51
Zero Piraeus

これは、作成された列挙の90 +%では不要な高度な動作です。

ドキュメントによると:

許可されるルールは次のとおりです。_sunder_の名前(単一のアンダースコアで開始および終了)はenumによって予約されており、使用できません。列挙内で定義された他のすべての属性は、__dunder__の名前とdescriptorsを除いて、この列挙のメンバーになります(メソッドも記述子です)。

したがって、クラス定数が必要な場合は、いくつかの選択肢があります。

  • __init__で作成してください
  • クラスが作成された後に追加します
  • ミックスインを使用する
  • 独自のdescriptorを作成する

__init__で定数を作成し、クラスが作成された後に追加すると、両方のクラス情報が1か所に収集されないという問題があります。

ミックスインは適切な場合に確かに使用できます( 良い例についてはdnozayの回答を参照してください )が、実際の定数が組み込まれた基本の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 Enumenum34 backport 、および 高度な列挙(aenum ライブラリ。

44
Ethan Furman

最もエレガントなソリューション(IMHO)は、正しい動作を提供するためにミックスイン/基本クラスを使用することです。

  • に共通するすべての実装に必要な動作を提供する基本クラス。 SatelliteおよびPlanet
  • mixinsは興味深い オプションの動作を提供する場合(例: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)
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)
_

@propertyが機能せず、classconstantが機能する理由は非常に単純で、- answer here で説明されています

Hello.fooクラスを介してアクセスしたときに実際のプロパティオブジェクトが返される理由は、プロパティが__get__(self, instance, owner)特殊メソッドを実装する方法にあります。記述子がインスタンスでアクセスされる場合、そのインスタンスは適切な引数として渡され、所有者はそのインスタンスのクラスです。

一方、クラスを通じてアクセスされた場合、インスタンスはNoneになり、所有者のみが渡されます。 プロパティオブジェクトはこれを認識し、自己を返します。

したがって、classpropertyのコードは実際にはpropertyを一般化したものであり、_if instance is None_の部分がありません。

9
Antti Haapala

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)

このアプローチの1つの制限は、クラス自体ではなく、クラスのインスタンスに対してのみ機能することです。

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