Enumeraciones en python

Duplicar:
¿Cuál es la mejor manera de implementar un ‘enum’ en Python?

¿Cuál es la forma reconocida de hacer enumeraciones en python?

Por ejemplo, en este momento estoy escribiendo un juego y quiero poder mover “arriba”, “abajo”, “izquierda” y “derecha”. Estoy usando cadenas porque todavía no he descubierto cómo funcionan las enumeraciones en python, por lo que mi lógica está llena de cosas como esta:

def move(self, direction): if direction == "up": # Do something 

Quiero reemplazar "up" con algo como Directions.up

 class Directions: up = 0 down = 1 left = 2 right =3 

ACTUALIZACIÓN 1 : Python 3.4 tendrá una biblioteca de enumeración bien diseñada incorporada. Los valores siempre saben su nombre y tipo; existe un modo compatible con enteros, pero el valor predeterminado recomendado para los nuevos usos es singletons, no igual a ningún otro objeto.

ACTUALIZACIÓN 2 : Desde que escribí esto, me di cuenta de que la prueba crítica para enums es la serialización . Otros aspectos se pueden refactorizar más adelante, pero si su enumeración se convierte en archivos / en el cable, pregúntese qué pasará si se deserializa con una versión más antigua / más nueva (que podría admitir un conjunto de valores diferente) …


Si está seguro de que necesita una enumeración, otros han respondido cómo hacerlo. Pero vamos a ver por qué los quieres? Comprender la motivación ayudará a elegir la solución.

  • Valores atómicos : en C, los números pequeños son fáciles de pasar, las cadenas no lo son. En Python, cadenas como “arriba” son perfectamente buenas para muchos usos. Además, cualquier solución que termine con solo un número es peor para la depuración.

  • Valores significativos : en C, con frecuencia tienes que lidiar con los números mágicos existentes, y solo quieres un poco de syntax de azúcar para eso. Ese no es el caso aquí. Sin embargo, hay otra información significativa que tal vez quiera asociar con las direcciones, por ejemplo, el vector (dx, dy). Más sobre esto a continuación.

  • Comprobación de tipos : en C, las enumeraciones ayudan a capturar valores no válidos en el momento de la comstackción. Pero, en general, Python prefiere sacrificar el comstackdor para verificar que haya menos tipeo.

  • Introspección (no existe en C enums): desea conocer todos los valores válidos.

    • Finalización : el editor puede mostrarle los valores posibles y ayudarlo a escribirlos.

Cadenas redimidas (también conocidos como símbolos)

Entonces, en el lado claro de las soluciones Pythonic, solo use cadenas, y tal vez tenga una lista / conjunto de todos los valores válidos:

 DIRECTIONS = set(['up', 'down', 'left', 'right']) def move(self, direction): # only if you feel like checking assert direction in DIRECTIONS # you can still just use the strings! if direction == 'up': # Do something 

Tenga en cuenta que el depurador le dirá que la función fue llamada con ‘up’ como argumento. ¡Cualquier solución donde la direction es realmente 0 es mucho peor que esto!

En la familia de lenguajes LISP, este uso se denomina símbolos : los objetos atómicos se pueden usar tan fácilmente como lo serían los números, pero tienen un valor textual . (Para ser precisos, los símbolos son como cadenas, pero son un tipo separado. Sin embargo, Python usa de forma rutinaria cadenas regulares donde LISP usaría símbolos).

Cadenas de nombre

Puede combinar la idea de que 'up' es mejor que 0 con las otras soluciones.

Si desea detectar errores de ortografía (en tiempo de ejecución):

 UP = 'up' ... RIGHT = 'right' 

Y si quieres insistir en escribir un prefijo para completar, coloca lo anterior en una clase:

 class Directions: UP = "up" ... RIGHT = "right" 

o simplemente en un archivo separado, convirtiéndolo en un módulo.

Un módulo permite a los usuarios perezosos hacer from directions import * para omitir el prefijo, dependiendo de usted, ya sea que lo considere un signo más o menos … (Personalmente, odiaría que me obligaran a escribir Directions.UP si lo estoy usando con frecuencia ).

Objetos con funcionalidad.

¿Qué pasa si hay información / funcionalidad útil asociada con cada valor? “correcto” no es solo uno de los 4 valores arbitrarios, ¡es la dirección positiva en el eje X!

Si lo que estás haciendo en eso if es algo como:

 def move(self, direction): if direction == 'up': self.y += STEP elif direction == 'down': self.y -= STEP elif direction == 'left': self.x -= STEP elif direction == 'right': self.x += STEP 

de lo que realmente te gustaría escribir es:

 def move(self, direction): self.x += direction.dx * STEP self.y += direction.dy * STEP 

¡y eso es!

Así que quieres meter esto en cualquiera de los casos :

 # Written in full to give the idea. # Consider using collections.namedtuple class Direction(object): def __init__(self, dx, dy, name): self.dx = dx self.dy = dy self.name = name def __str__(self): return self.name UP = Direction(0, 1, "up") DOWN = Direction(0, -1, "down") LEFT = Direction(-1, 0, "left") RIGHT = Direction(1, 0, "right") 

o simplemente clases :

 class Direction(object): pass class Up(Direction): dx = 0 dy = 1 ... class Right(Direction): dx = 1 dy = 0 

Recuerda que en Python, las clases también son objetos (distintos de cualquier otro objeto), y puedes compararlos: direction == Up etc.

En general, las instancias son probablemente más limpias, pero si sus conceptos enumerados tienen alguna relación jerárquica, a veces modelarlos directamente con clases es muy bueno.

Le di un +1 a Kugel, pero otra opción más sencilla es

 dirUp, dirDown, dirLeft, dirRight = range(4) 
      • (Algún tiempo pasa)

Así que estaba pensando … tenemos una obvia infracción de SECO aquí en que ya especificamos cuatro elementos en el LHS, y luego volvemos a especificar cuatro en el RHS. ¿Qué pasa si agregamos artículos en el futuro? ¿Qué sucede cuando alguien más los agrega y quizás son más descuidados que nosotros? Una forma obvia de eliminar la infracción DRY es usar la lista de enumeraciones para asignar sus valores:

 >>> enums = ['dirUp', 'dirDown'] >>> for v, k in enumerate(enums): ... exec(k + '=' + str(v)) ... >>> print dirDown 1 >>> print dirUp 0 

Si puedes usar el estómago con exec() para esto, entonces bien. Si no, entonces usa el otro enfoque. Esta discusión actual es todo académica de todos modos. Sin embargo, todavía hay un problema aquí. ¿Qué sucede si los enums se usan en un gran cuerpo de código fuente, y algún otro progtwigdor aparece e inserta un nuevo valor entre dirUp y dirDown ? Esto causará desdicha porque la asignación entre los nombres de las enumeraciones y las enumeraciones en sí mismas será incorrecta. Tenga en cuenta que esto sigue siendo un problema incluso en la solución simple original.

Aquí, tenemos la novedosa idea de usar la hash() incorporada para determinar nuestro valor de enumeración como un int, y usamos el nombre de texto de la enumeración en sí para determinar el hash:

 >>> for k in enums: ... exec(k + '=' + str(hash(k))) ... >>> dirUp -1147857581 >>> dirDown 453592598 >>> enums = ['dirUp', 'dirLeft', 'dirDown'] >>> for k in enums: ... exec(k + '=' + str(hash(k))) ... >>> dirUp -1147857581 >>> dirDown 453592598 >>> dirLeft -300839747 >>> 

Observe que dirUp un nuevo valor entre dirUp y dirDown , es decir, dirLeft , y nuestros valores de mapeo originales para los dos primeros no cambiaron .

Realmente puedo usar esto en mi propio código. Gracias a la OP por publicar la pregunta.

      • (Pasa algo más de tiempo)

Beni Cherniavsky-Paskin hizo algunos comentarios muy buenos:

  • El hash() predeterminado de Python hash() no es estable en todas las plataformas (peligroso para las aplicaciones de persistencia)
  • La posibilidad de colisiones está siempre presente.

Tiendo a estar de acuerdo con ambas observaciones. Su sugerencia es usar las cadenas en sí mismas (me gusta mucho el comportamiento de auto-documentación de usar los valores) como hash, y así el código se convierte en el siguiente (tenga en cuenta que usamos un conjunto en lugar de una lista, para imponer la singularidad):

 >>> items=('dirUp','dirDown','dirLeft','dirRight') >>> for i in items: exec('{}="{}"'.format(i,i)) >>> dirDown 'dirDown' 

También es trivial colocarlos en un espacio de nombres, para evitar colisiones con otro código:

 >>> class Direction(): for i in ('dirUp','dirDown','dirLeft','dirRight'): exec('{}="{}"'.format(i,i)) >>> Direction.dirUp 'dirUp' 

La longitud del hash criptográfico que menciona se puede ver aquí:

 >>> from hashlib import md5 >>> crypthash = md5('dirDown'.encode('utf8')) >>> crypthash.hexdigest() '6a65fd3cd318166a1cc30b3e5e666d8f' 

El objeto collections.namedtuple puede proporcionar dicho espacio de nombres:

 >>> import collections >>> dircoll=collections.namedtuple('directions', ('UP', 'DOWN', 'LEFT', 'RIGHT')) >>> directions=dircoll(0,1,2,3) >>> directions directions(UP=0, DOWN=1, LEFT=2, RIGHT=3) >>> directions.DOWN 1 >>> 

Esto es simple y efectivo:

 class Enum(object): def __init__(self, *keys): self.__dict__.update(zip(keys, range(len(keys)))) 

Uso:

 >>> x = Enum('foo', 'bar', 'baz', 'bat') >>> x.baz 2 >>> x.bat 3 

Si está utilizando Python 2.6+, puede usar namedtuple . Tienen la ventaja de tener un número fijo de propiedades, y cuando necesita todos los valores de enumeración, puede usarlo como una tupla.

Para tener más control sobre los valores de enumeración, puede crear su propia clase de enumeración.

 def enum(args, start=0): class Enum(object): __slots__ = args.split() def __init__(self): for i, key in enumerate(Enum.__slots__, start): setattr(self, key, i) return Enum() >>> e_dir = enum('up down left right') >>> e_dir.up 0 >>> e_dir = enum('up down left right', start=1) >>> e_dir.up 1 

Declarar __slots__ sella su clase Enum, no se pueden establecer más atributos para un objeto que se creó a partir de una clase con la propiedad __slots__ .

Su clase Enum también se puede namedtuple basada en namedtuple , en ese caso también obtiene las características de una tupla. Ver los namedtuple docs en la subclase namedtuple