objects - use enum python




Comment puis-je représenter un 'Enum' en Python? (20)

A partir de Python 3.4, il y aura un support officiel pour les enums. Vous pouvez trouver la documentation et les exemples ici sur la page de documentation de Python 3.4 .

Les énumérations sont créées en utilisant la syntaxe de la classe, ce qui les rend faciles à lire et à écrire. Une méthode de création alternative est décrite dans l'API fonctionnelle. Pour définir une énumération, sous-classe Enum comme suit:

from enum import Enum
class Color(Enum):
     red = 1
     green = 2
     blue = 3

Je suis principalement un développeur C #, mais je travaille actuellement sur un projet en Python.

Comment puis-je représenter l'équivalent d'un Enum en Python?


Avant PPE 435, Python n'avait pas d'équivalent mais vous pouviez implémenter le vôtre.

Moi, j'aime garder les choses simples (j'ai vu des exemples horriblement complexes sur le net), quelque chose comme ça ...

class Animal:
    DOG = 1
    CAT = 2

x = Animal.DOG

En Python 3.4 ( PEP 435 ), vous pouvez faire Enum la classe de base. Cela vous apporte un peu de fonctionnalité supplémentaire, décrite dans le PEP. Par exemple, les valeurs enum sont distinctes des entiers.

class Animal(Enum):
    DOG = 1
    CAT = 2

print(Animal.DOG)
<Animal.DOG: 1>

Si vous ne voulez pas saisir les valeurs, utilisez le raccourci suivant:

class Animal(Enum):
    DOG, CAT = range(2)

Ce que j'utilise:

class Enum(object):
    def __init__(self, names, separator=None):
        self.names = names.split(separator)
        for value, name in enumerate(self.names):
            setattr(self, name.upper(), value)
    def tuples(self):
        return tuple(enumerate(self.names))

Comment utiliser:

>>> state = Enum('draft published retracted')
>>> state.DRAFT
0
>>> state.RETRACTED
2
>>> state.FOO
Traceback (most recent call last):
   File "<stdin>", line 1, in <module>
AttributeError: 'Enum' object has no attribute 'FOO'
>>> state.tuples()
((0, 'draft'), (1, 'published'), (2, 'retracted'))

Cela vous donne donc des constantes entières comme state.PUBLISHED et les deux-tuples à utiliser comme choix dans les modèles Django.


Des enums ont été ajoutés à Python 3.4 comme décrit dans PEP 435 . Il a également été rétroporté à 3.3, 3.2, 3.1, 2.7, 2.6, 2.5 et 2.4 sur pypi.

Pour les techniques Enum plus avancées, essayez la librairie aenum (2.7, 3.3+, le même auteur que enum34 code n'est pas parfaitement compatible entre py2 et py3, par exemple vous aurez besoin de __order__ en python 2 ).

  • Pour utiliser enum34 , faites $ pip install enum34
  • Pour utiliser aenum , $ pip install aenum

L'installation d' enum (no numbers) va installer une version complètement différente et incompatible.

from enum import Enum     # for enum34, or the stdlib version
# from aenum import Enum  # for the aenum version
Animal = Enum('Animal', 'ant bee cat dog')

Animal.ant  # returns <Animal.ant: 1>
Animal['ant']  # returns <Animal.ant: 1> (string lookup)
Animal.ant.name  # returns 'ant' (inverse lookup)

ou équivalent:

class Animal(Enum):
    ant = 1
    bee = 2
    cat = 3
    dog = 4

Dans les versions antérieures, une façon d'accomplir des énumérations est:

def enum(**enums):
    return type('Enum', (), enums)

qui est utilisé comme ça:

>>> Numbers = enum(ONE=1, TWO=2, THREE='three')
>>> Numbers.ONE
1
>>> Numbers.TWO
2
>>> Numbers.THREE
'three'

Vous pouvez également facilement prendre en charge l'énumération automatique avec quelque chose comme ceci:

def enum(*sequential, **named):
    enums = dict(zip(sequential, range(len(sequential))), **named)
    return type('Enum', (), enums)

et utilisé comme ça:

>>> Numbers = enum('ZERO', 'ONE', 'TWO')
>>> Numbers.ZERO
0
>>> Numbers.ONE
1

La prise en charge de la conversion des valeurs en noms peut être ajoutée de cette manière:

def enum(*sequential, **named):
    enums = dict(zip(sequential, range(len(sequential))), **named)
    reverse = dict((value, key) for key, value in enums.iteritems())
    enums['reverse_mapping'] = reverse
    return type('Enum', (), enums)

Cela écrase tout ce qui porte ce nom, mais il est utile pour rendre vos énumérations en sortie. Il lancera KeyError si le mappage inverse n'existe pas. Avec le premier exemple:

>>> Numbers.reverse_mapping['three']
'THREE'

Hmmm ... Je suppose que la chose la plus proche d'une énumération serait un dictionnaire, défini soit comme ceci:

months = {
    'January': 1,
    'February': 2,
    ...
}

ou

months = dict(
    January=1,
    February=2,
    ...
)

Ensuite, vous pouvez utiliser le nom symbolique pour les constantes comme ceci:

mymonth = months['January']

Il existe d'autres options, comme une liste de tuples, ou un tuple de tuples, mais le dictionnaire est le seul qui vous fournit un moyen "symbolique" (chaîne constante) d'accéder à la valeur.

Edit: J'aime la réponse d'Alexandru aussi!


J'ai eu besoin d'une classe Enum, dans le but de décoder un format de fichier binaire. Les caractéristiques que je souhaitais obtenir sont une définition enum concise, la possibilité de créer librement des instances de l'énumération par une valeur entière ou une chaîne, et une représentation utile. Voici ce que j'ai fini avec:

>>> class Enum(int):
...     def __new__(cls, value):
...         if isinstance(value, str):
...             return getattr(cls, value)
...         elif isinstance(value, int):
...             return cls.__index[value]
...     def __str__(self): return self.__name
...     def __repr__(self): return "%s.%s" % (type(self).__name__, self.__name)
...     class __metaclass__(type):
...         def __new__(mcls, name, bases, attrs):
...             attrs['__slots__'] = ['_Enum__name']
...             cls = type.__new__(mcls, name, bases, attrs)
...             cls._Enum__index = _index = {}
...             for base in reversed(bases):
...                 if hasattr(base, '_Enum__index'):
...                     _index.update(base._Enum__index)
...             # create all of the instances of the new class
...             for attr in attrs.keys():
...                 value = attrs[attr]
...                 if isinstance(value, int):
...                     evalue = int.__new__(cls, value)
...                     evalue._Enum__name = attr
...                     _index[value] = evalue
...                     setattr(cls, attr, evalue)
...             return cls
... 

Un exemple fantaisiste de l'utiliser:

>>> class Citrus(Enum):
...     Lemon = 1
...     Lime = 2
... 
>>> Citrus.Lemon
Citrus.Lemon
>>> 
>>> Citrus(1)
Citrus.Lemon
>>> Citrus(5)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 6, in __new__
KeyError: 5
>>> class Fruit(Citrus):
...     Apple = 3
...     Banana = 4
... 
>>> Fruit.Apple
Fruit.Apple
>>> Fruit.Lemon
Citrus.Lemon
>>> Fruit(1)
Citrus.Lemon
>>> Fruit(3)
Fruit.Apple
>>> "%d %s %r" % ((Fruit.Apple,)*3)
'3 Apple Fruit.Apple'
>>> Fruit(1) is Citrus.Lemon
True

Principales caractéristiques:

  • str() , int() et repr() produisent toutes la sortie la plus utile possible, respectivement le nom de l'énumération, sa valeur entière et une expression Python qui revient à l'énumération.
  • Les valeurs énumérées renvoyées par le constructeur sont strictement limitées aux valeurs prédéfinies, pas de valeurs enum accidentelles.
  • Les valeurs énumérées sont des singletons; ils peuvent être strictement comparés avec is

Je préfère définir enums en Python comme ça:

class Animal:
  class Dog: pass
  class Cat: pass

x = Animal.Dog

C'est plus résistant aux bogues que d'utiliser des entiers puisque vous n'avez pas à vous soucier de vous assurer que les entiers sont uniques (par exemple si vous avez dit Chien = 1 et Cat = 1 vous seriez foutu).

C'est plus résistant aux bogues que d'utiliser des chaînes car vous n'avez pas à vous soucier des fautes de frappe (par exemple x == "catt" échoue silencieusement, mais x == Animal.Catt est une exception d'exécution).


La meilleure solution pour vous dépend de ce dont vous avez besoin de votre faux enum .

Enum simple:

Si vous avez besoin de l' enum comme une liste de noms identifiant différents éléments , la solution de Mark Harrison (ci-dessus) est géniale:

Pen, Pencil, Eraser = range(0, 3)

L'utilisation d'une range vous permet également de définir n'importe quelle valeur de départ :

Pen, Pencil, Eraser = range(9, 12)

En plus de ce qui précède, si vous souhaitez également que les éléments appartiennent à un conteneur quelconque, incorporez-les dans une classe:

class Stationery:
    Pen, Pencil, Eraser = range(0, 3)

Pour utiliser l'élément enum, vous devez maintenant utiliser le nom du conteneur et le nom de l'élément:

stype = Stationery.Pen

Enum complexe:

Pour de longues listes d'enum ou des utilisations plus compliquées d'enum, ces solutions ne suffiront pas. Vous pouvez consulter la recette de Will Ware pour Simulating Enumerations en Python publiée dans le livre de recettes Python . Une version en ligne de cela est disponible here .

Plus d'informations:

PEP 354: Les énumérations en Python ont les détails intéressants d'une proposition d'énumération en Python et pourquoi elle a été rejetée.


La suggestion d'Alexandru d'utiliser des constantes de classe pour les énumérations fonctionne assez bien.

J'aime aussi ajouter un dictionnaire pour chaque ensemble de constantes afin de rechercher une représentation de chaîne lisible par un humain.

Cela sert à deux fins: a) il fournit un moyen simple d'imprimer votre enum et b) le dictionnaire regroupe logiquement les constantes afin que vous puissiez tester l'appartenance.

class Animal:    
  TYPE_DOG = 1
  TYPE_CAT = 2

  type2str = {
    TYPE_DOG: "dog",
    TYPE_CAT: "cat"
  }

  def __init__(self, type_):
    assert type_ in self.type2str.keys()
    self._type = type_

  def __repr__(self):
    return "<%s type=%s>" % (
        self.__class__.__name__, self.type2str[self._type].upper())

Le 2013-05-10, Guido a accepté d'accepter PEP 435 dans la bibliothèque standard Python 3.4. Cela signifie que Python a finalement intégré le support des énumérations!

Il existe un backport disponible pour Python 3.3, 3.2, 3.1, 2.7, 2.6, 2.5 et 2.4. C'est sur Pypi comme enum34 .

Déclaration:

>>> from enum import Enum
>>> class Color(Enum):
...     red = 1
...     green = 2
...     blue = 3

Représentation:

>>> print(Color.red)
Color.red
>>> print(repr(Color.red))
<Color.red: 1>

Itération:

>>> for color in Color:
...   print(color)
...
Color.red
Color.green
Color.blue

Accès par programme:

>>> Color(1)
Color.red
>>> Color['blue']
Color.blue

Pour plus d'informations, reportez-vous à la proposition . La documentation officielle suivra probablement bientôt.


Le paquet enum de PyPI fournit une implémentation robuste des énumérations. Une réponse antérieure mentionnait le PEP 354; ceci a été rejeté mais la proposition a été implémentée http://pypi.python.org/pypi/enum .

L'utilisation est facile et élégante:

>>> from enum import Enum
>>> Colors = Enum('red', 'blue', 'green')
>>> shirt_color = Colors.green
>>> shirt_color = Colors[2]
>>> shirt_color > Colors.red
True
>>> shirt_color.index
2
>>> str(shirt_color)
'green'

Python n'a pas d'équivalent intégré à enum , et d'autres réponses ont des idées pour implémenter les vôtres (vous pouvez aussi être intéressé here dans le livre de recettes Python).

Cependant, dans les situations où une enum serait appelée en C, je finis généralement par utiliser des chaînes simples : en raison de la façon dont les objets / attributs sont implémentés, (C) Python est optimisé pour fonctionner très rapidement avec des chaînes courtes. ne serait pas vraiment un avantage de performance à l'utilisation des entiers. Pour vous prémunir contre les fautes de frappe / valeurs invalides, vous pouvez insérer des contrôles dans les endroits sélectionnés.

ANIMALS = ['cat', 'dog', 'python']

def take_for_a_walk(animal):
    assert animal in ANIMALS
    ...

(Un inconvénient par rapport à l'utilisation d'une classe est que vous perdez le bénéfice de la saisie semi-automatique)


Si vous avez besoin des valeurs numériques, voici le moyen le plus rapide:

dog, cat, rabbit = range(3)

En Python 3.x vous pouvez également ajouter un espace réservé étoilé à la fin, qui absorbera toutes les valeurs restantes de la gamme dans le cas où cela ne vous dérange pas de gaspiller de la mémoire et ne peut pas compter:

dog, cat, rabbit, horse, *_ = range(100)

Une autre, très simple, implémentation d'une énumération en Python, en utilisant namedtuple :

from collections import namedtuple

def enum(*keys):
    return namedtuple('Enum', keys)(*keys)

MyEnum = enum('FOO', 'BAR', 'BAZ')

Ou bien,

# With sequential number values
def enum(*keys):
    return namedtuple('Enum', keys)(*range(len(keys)))

# From a dict / keyword args
def enum(**kwargs):
    return namedtuple('Enum', kwargs.keys())(*kwargs.values())

Comme la méthode ci-dessus ce set sous-classes, cela permet:

'FOO' in MyEnum
other = MyEnum.FOO
assert other == MyEnum.FOO

Mais a plus de flexibilité car il peut avoir différentes clés et valeurs. Ceci permet

MyEnum.FOO < MyEnum.BAR

pour agir comme prévu si vous utilisez la version qui remplit les valeurs numériques séquentielles.


Voici une implémentation:

class Enum(set):
    def __getattr__(self, name):
        if name in self:
            return name
        raise AttributeError

Voici son utilisation:

Animals = Enum(["DOG", "CAT", "HORSE"])

print(Animals.DOG)

Voici une variante de la solution d'Alec Thomas :

def enum(*args, **kwargs):
    return type('Enum', (), dict((y, x) for x, y in enumerate(args), **kwargs)) 

x = enum('POOH', 'TIGGER', 'EEYORE', 'ROO', 'PIGLET', 'RABBIT', 'OWL')
assert x.POOH == 0
assert x.TIGGER == 1

Here's an approach with some different characteristics I find valuable:

  • allows > and < comparison based on order in enum, not lexical order
  • can address item by name, property or index: xa, x['a'] or x[0]
  • supports slicing operations like [:] or [-1]

and most importantly prevents comparisons between enums of different types !

Based closely on http://code.activestate.com/recipes/413486-first-class-enums-in-python .

Many doctests included here to illustrate what's different about this approach.

def enum(*names):
    """
SYNOPSIS
    Well-behaved enumerated type, easier than creating custom classes

DESCRIPTION
    Create a custom type that implements an enumeration.  Similar in concept
    to a C enum but with some additional capabilities and protections.  See
    http://code.activestate.com/recipes/413486-first-class-enums-in-python/.

PARAMETERS
    names       Ordered list of names.  The order in which names are given
                will be the sort order in the enum type.  Duplicate names
                are not allowed.  Unicode names are mapped to ASCII.

RETURNS
    Object of type enum, with the input names and the enumerated values.

EXAMPLES
    >>> letters = enum('a','e','i','o','u','b','c','y','z')
    >>> letters.a < letters.e
    True

    ## index by property
    >>> letters.a
    a

    ## index by position
    >>> letters[0]
    a

    ## index by name, helpful for bridging string inputs to enum
    >>> letters['a']
    a

    ## sorting by order in the enum() create, not character value
    >>> letters.u < letters.b
    True

    ## normal slicing operations available
    >>> letters[-1]
    z

    ## error since there are not 100 items in enum
    >>> letters[99]
    Traceback (most recent call last):
        ...
    IndexError: tuple index out of range

    ## error since name does not exist in enum
    >>> letters['ggg']
    Traceback (most recent call last):
        ...
    ValueError: tuple.index(x): x not in tuple

    ## enums must be named using valid Python identifiers
    >>> numbers = enum(1,2,3,4)
    Traceback (most recent call last):
        ...
    AssertionError: Enum values must be string or unicode

    >>> a = enum('-a','-b')
    Traceback (most recent call last):
        ...
    TypeError: Error when calling the metaclass bases
        __slots__ must be identifiers

    ## create another enum
    >>> tags = enum('a','b','c')
    >>> tags.a
    a
    >>> letters.a
    a

    ## can't compare values from different enums
    >>> letters.a == tags.a
    Traceback (most recent call last):
        ...
    AssertionError: Only values from the same enum are comparable

    >>> letters.a < tags.a
    Traceback (most recent call last):
        ...
    AssertionError: Only values from the same enum are comparable

    ## can't update enum after create
    >>> letters.a = 'x'
    Traceback (most recent call last):
        ...
    AttributeError: 'EnumClass' object attribute 'a' is read-only

    ## can't update enum after create
    >>> del letters.u
    Traceback (most recent call last):
        ...
    AttributeError: 'EnumClass' object attribute 'u' is read-only

    ## can't have non-unique enum values
    >>> x = enum('a','b','c','a')
    Traceback (most recent call last):
        ...
    AssertionError: Enums must not repeat values

    ## can't have zero enum values
    >>> x = enum()
    Traceback (most recent call last):
        ...
    AssertionError: Empty enums are not supported

    ## can't have enum values that look like special function names
    ## since these could collide and lead to non-obvious errors
    >>> x = enum('a','b','c','__cmp__')
    Traceback (most recent call last):
        ...
    AssertionError: Enum values beginning with __ are not supported

LIMITATIONS
    Enum values of unicode type are not preserved, mapped to ASCII instead.

    """
    ## must have at least one enum value
    assert names, 'Empty enums are not supported'
    ## enum values must be strings
    assert len([i for i in names if not isinstance(i, types.StringTypes) and not \
        isinstance(i, unicode)]) == 0, 'Enum values must be string or unicode'
    ## enum values must not collide with special function names
    assert len([i for i in names if i.startswith("__")]) == 0,\
        'Enum values beginning with __ are not supported'
    ## each enum value must be unique from all others
    assert names == uniquify(names), 'Enums must not repeat values'

    class EnumClass(object):
        """ See parent function for explanation """

        __slots__ = names

        def __iter__(self):
            return iter(constants)

        def __len__(self):
            return len(constants)

        def __getitem__(self, i):
            ## this makes xx['name'] possible
            if isinstance(i, types.StringTypes):
                i = names.index(i)
            ## handles the more normal xx[0]
            return constants[i]

        def __repr__(self):
            return 'enum' + str(names)

        def __str__(self):
            return 'enum ' + str(constants)

        def index(self, i):
            return names.index(i)

    class EnumValue(object):
        """ See parent function for explanation """

        __slots__ = ('__value')

        def __init__(self, value):
            self.__value = value

        value = property(lambda self: self.__value)

        enumtype = property(lambda self: enumtype)

        def __hash__(self):
            return hash(self.__value)

        def __cmp__(self, other):
            assert self.enumtype is other.enumtype, 'Only values from the same enum are comparable'
            return cmp(self.value, other.value)

        def __invert__(self):
            return constants[maximum - self.value]

        def __nonzero__(self):
            ## return bool(self.value)
            ## Original code led to bool(x[0])==False, not correct
            return True

        def __repr__(self):
            return str(names[self.value])

    maximum = len(names) - 1
    constants = [None] * len(names)
    for i, each in enumerate(names):
        val = EnumValue(i)
        setattr(EnumClass, each, val)
        constants[i] = val
    constants = tuple(constants)
    enumtype = EnumClass()
    return enumtype

I had need of some symbolic constants in pyparsing to represent left and right associativity of binary operators. I used class constants like this:

# an internal class, not intended to be seen by client code
class _Constants(object):
    pass


# an enumeration of constants for operator associativity
opAssoc = _Constants()
opAssoc.LEFT = object()
opAssoc.RIGHT = object()

Now when client code wants to use these constants, they can import the entire enum using:

import opAssoc from pyparsing

Les énumérations sont uniques, elles peuvent être testées avec 'is' au lieu de '==', elles n'occupent pas une grande place dans mon code pour un concept mineur, et elles sont facilement importées dans le code client. Ils ne supportent aucun comportement sophistiqué de str (), mais jusqu'ici c'est dans la catégorie YAGNI .


While the original enum proposal, PEP 354 , was rejected years ago, it keeps coming back up. Some kind of enum was intended to be added to 3.2, but it got pushed back to 3.3 and then forgotten. And now there's a PEP 435 intended for inclusion in Python 3.4. The reference implementation of PEP 435 is flufl.enum .

As of April 2013, there seems to be a general consensus that something should be added to the standard library in 3.4—as long as people can agree on what that "something" should be. That's the hard part. See the threads starting here and here , and a half dozen other threads in the early months of 2013.

Meanwhile, every time this comes up, a slew of new designs and implementations appear on PyPI, ActiveState, etc., so if you don't like the FLUFL design, try a PyPI search .


def M_add_class_attribs(attribs):
    def foo(name, bases, dict_):
        for v, k in attribs:
            dict_[k] = v
        return type(name, bases, dict_)
    return foo

def enum(*names):
    class Foo(object):
        __metaclass__ = M_add_class_attribs(enumerate(names))
        def __setattr__(self, name, value):  # this makes it read-only
            raise NotImplementedError
    return Foo()

Utilisez-le comme ceci:

Animal = enum('DOG', 'CAT')
Animal.DOG # returns 0
Animal.CAT # returns 1
Animal.DOG = 2 # raises NotImplementedError

Si vous voulez juste des symboles uniques et ne vous souciez pas des valeurs, remplacez cette ligne:

__metaclass__ = M_add_class_attribs(enumerate(names))

avec ça:

__metaclass__ = M_add_class_attribs((object(), name) for name in names)




enums