Enumeraciones en python
django enum (6)
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 aún 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
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
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 utilizan en un gran cuerpo de código fuente, y algún otro programador aparece e inserta un nuevo valor entre dirUp
y dirDown
? Esto causará desdicha porque la asignación entre los nombres de los enumerados y los enumerados en sí mismos 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 Pythonhash()
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''
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. Vea los namedtuple en la subclase namedtuple
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 / nueva (que podría admitir un conjunto diferente de valores) ...
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 sintaxis 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 eso a continuación.
Comprobación de tipos : en C, las enumeraciones ayudan a capturar valores no válidos en el momento de la compilación. Pero, en general, Python prefiere sacrificar el compilador para verificar que haya menos escritura.
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 en realidad 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 manera 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 que los usuarios perezosos realicen from directions import *
para omitir el prefijo, dependiendo de usted, ya sea que considere esto como un punto positivo o negativo ... ).
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.
class Directions:
up = 0
down = 1
left = 2
right =3