tl; dr
github.com/QratorLabs/fastenumpip install fast-enum
¿Por qué es necesaria la enumeración?
(si lo sabe todo, vaya a la sección "Enumeraciones en la biblioteca estándar")
Imagine que necesita describir un conjunto de todos los estados posibles de entidades en su propio modelo de base de datos. Lo más probable es que tome un montón de constantes definidas directamente en el espacio de nombres del módulo:
... o como atributos de clase estática:
class MyModelStates: INITIAL = 0 PROCESSING = 1 PROCESSED = 2 DECLINED = 3 RETURNED = 4
Este enfoque ayudará a referirse a estos estados por nombres mnemotécnicos, mientras que en su repositorio serán enteros comunes. Por lo tanto, al mismo tiempo, elimina los números mágicos dispersos en diferentes partes del código, al mismo tiempo que lo hace más legible e informativo.
Sin embargo, tanto la constante del módulo como la clase con atributos estáticos adolecen de la naturaleza intrínseca de los objetos de Python: todos son mutables (mutables). Puede asignar accidentalmente un valor a su constante en tiempo de ejecución, y depurar y deshacer objetos rotos es una aventura separada. Por lo tanto, es posible que desee que el conjunto de constantes no cambie en el sentido de que el número de constantes declaradas y sus valores a los que están asignados no cambiarán durante la ejecución del programa.
Para hacer esto, puede intentar organizarlos en tuplas con nombre usando
namedtuple()
, como en el ejemplo:
MyModelStates = namedtuple('MyModelStates', ('INITIAL', 'PROCESSING', 'PROCESSED', 'DECLINED', 'RETURNED')) EntityStates = MyModelStates(0, 1, 2, 3, 4)
Pero esto no se ve muy ordenado y legible, y los objetos con nombre de
namedtuple
, a su vez, no son muy extensibles. Supongamos que tiene una interfaz de usuario que muestra todos estos estados. Puede usar sus constantes en módulos, una clase con atributos o tuplas con nombre para representarlas (las dos últimas son más fáciles de representar ya que estamos hablando de esto). Pero dicho código no permite proporcionar al usuario una descripción adecuada para cada estado que defina. Además, si planea implementar el multilingüismo y el soporte de i18n en su interfaz de usuario, se dará cuenta de lo rápido que completar todas las traducciones de estas descripciones se vuelve una tarea increíblemente tediosa. Hacer coincidir los nombres de los estados no necesariamente significará hacer coincidir la descripción, lo que significa que no puede simplemente asignar todos sus estados
INITIAL
a la misma descripción en
gettext
. En cambio, su constante toma la siguiente forma:
INITIAL = (0, 'My_MODEL_INITIAL_STATE')
O tu clase se vuelve así:
class MyModelStates: INITIAL = (0, 'MY_MODEL_INITIAL_STATE')
Finalmente, la tupla nombrada se convierte en:
EntityStates = MyModelStates((0, 'MY_MODEL_INITIAL_STATE'), ...)
Ya no está mal: ahora garantiza que tanto el valor de estado como el código auxiliar de traducción se muestren en los idiomas admitidos por la IU. Pero puede notar que el código que usa estas asignaciones se ha convertido en un desastre. Cada vez que intente asignar un valor de entidad, debe extraer el valor con el índice 0 de la pantalla que está utilizando:
my_entity.state = INITIAL[0]
o
my_entity.state = MyModelStates.INITIAL[0]
o
my_entity.state = EntityStates.INITIAL[0]
Y así sucesivamente. Recuerde que los dos primeros enfoques que usan constantes y atributos de clase, respectivamente, sufren de mutabilidad.
Y las transferencias vienen en nuestra ayuda.
class MyEntityStates(Enum): def __init__(self, val, description): self.val = val self.description = description INITIAL = (0, 'MY_MODEL_INITIAL_STATE') PROCESSING = (1, 'MY_MODEL_BEING_PROCESSED_STATE') PROCESSED = (2, 'MY_MODEL_PROCESSED_STATE') DECLINED = (3, 'MY_MODEL_DECLINED_STATE') RETURNED = (4, 'MY_MODEL_RETURNED_STATE')
Eso es todo Ahora puede iterar fácilmente sobre la lista en su render (sintaxis Jinja2):
{% for state in MyEntityState %} <option value=”{{ state.val }}”>{{ _(state.description) }}</option> {% endfor %}
Una enumeración es inmutable tanto para un conjunto de elementos: no puede definir un nuevo miembro de una enumeración en tiempo de ejecución y no puede eliminar un miembro ya definido, y para los valores de elementos que almacena, no puede [volver] a asignar valores de atributo ni eliminar un atributo.
En su código, simplemente asigna valores a sus entidades, así:
my_entity.state = MyEntityStates.INITIAL.val
Todo es lo suficientemente claro, informativo y extensible. Para esto utilizamos enumeraciones.
¿Cómo podríamos hacerlo más rápido?
La enumeración de la biblioteca estándar es bastante lenta, por lo que nos preguntamos: ¿podemos acelerarla? Al final resultó que, podemos, a saber, la implementación de nuestra enumeración:
- Tres veces más rápido en el acceso a la enumeración de miembros;
- ~ 8.5 más rápido al acceder al atributo (
name
, value
) de un miembro; - 3 veces más rápido al acceder a un miembro por valor (llame al constructor de la enumeración
MyEnum(value))
; - 1.5 veces más rápido al acceder a un miembro por su nombre (como en el
MyEnum[name]
).
Los tipos y objetos en Python son dinámicos. Pero hay herramientas para limitar la naturaleza dinámica de los objetos. Puede obtener un aumento significativo del rendimiento con
__slots__
. También es posible que aumente la velocidad si evita usar descriptores de datos cuando sea posible, pero debe considerar la posibilidad de un aumento significativo en la complejidad de la aplicación.
Tragamonedas
Por ejemplo, puede usar una declaración de clase usando
__slots__
; en este caso, todas las instancias de clases tendrán solo un conjunto limitado de propiedades declaradas en
__slots__
y todas las
__slots__
clases primarias.
Descriptores
Por defecto, el intérprete de Python devuelve el valor del atributo del objeto directamente (al mismo tiempo, estipulamos que en este caso el valor también es un objeto de Python, y no, por ejemplo, sin signo largo en términos del lenguaje C):
value = my_obj.attribute # , .
Según el modelo de datos de Python, si el valor del atributo es un objeto que implementa el protocolo descriptor, cuando intente obtener el valor de este atributo, el intérprete primero encontrará una referencia al objeto al que se refiere la propiedad y luego llamará al método especial
__get__
, que se pasará a nuestro objeto original como argumento:
obj_attribute = my_obj.attribute obj_attribute_value = obj_attribute.__get__(my_obj)
Enumeraciones en la biblioteca estándar
Al menos las propiedades de
name
y
value
de los miembros de la implementación de enumeración estándar se declaran como
types.DynamicClassAttribute
. Esto significa que cuando intente obtener los valores de
name
y
value
, sucederá lo siguiente:
one_value = StdEnum.ONE.value
Por lo tanto, la secuencia completa de llamadas se puede representar mediante el siguiente pseudocódigo:
def get_func(enum_member, attrname):
Escribimos un script simple que demuestra el resultado descrito anteriormente:
from enum import Enum class StdEnum(Enum): def __init__(self, value, description): self.v = value self.description = description A = 1, 'One' B = 2, 'Two' def get_name(): return StdEnum.A.name from pycallgraph import PyCallGraph from pycallgraph.output import GraphvizOutput graphviz = GraphvizOutput(output_file='stdenum.png') with PyCallGraph(output=graphviz): v = get_name()
Y después de la ejecución, el script nos dio la siguiente imagen:

Esto muestra que cada vez que accede a los atributos de
name
y
value
de los miembros de enumeración desde la biblioteca estándar, se llama a un identificador. Este descriptor, a su vez, finaliza con una llamada de la clase
Enum
de la biblioteca estándar del método de
def name(self)
, decorada con el descriptor.
Compare con nuestro FastEnum:
from fast_enum import FastEnum class MyNewEnum(metaclass=FastEnum): A = 1 B = 2 def get_name(): return MyNewEnum.A.name from pycallgraph import PyCallGraph from pycallgraph.output import GraphvizOutput graphviz = GraphvizOutput(output_file='fastenum.png') with PyCallGraph(output=graphviz): v = get_name()
Lo que se puede ver en la siguiente imagen:

Todo esto realmente sucede dentro de la implementación de enumeración estándar cada vez que accede a las propiedades de
name
y
value
de sus miembros. Esta es también la razón por la cual nuestra implementación es más rápida.
La implementación de enumeraciones en la biblioteca estándar de Python utiliza muchas llamadas a objetos que implementan el protocolo descriptor de datos. Cuando intentamos usar la implementación de enumeración estándar en nuestros proyectos, notamos inmediatamente cuántos descriptores de datos se llamaban por
name
y
value
.
Y dado que las enumeraciones se usaron ampliamente en todo el código, el rendimiento resultante fue bajo.
Además, la clase Enum estándar contiene varios atributos auxiliares "protegidos":
_member_names_
: una lista que contiene todos los nombres de los miembros de la enumeración;_member_map_
- OrderedDict
, que asigna el nombre de un miembro de enumeración a su valor;_value2member_map_
: un diccionario que contiene coincidencias en la dirección opuesta: los valores de los miembros de la enumeración a los miembros correspondientes de la enumeración.
La búsqueda en el diccionario es lenta, ya que cada llamada conduce al cálculo de la función hash (a menos, por supuesto, que el resultado se almacene en caché por separado, lo que no siempre es posible para el código no administrado) y a la búsqueda en la tabla hash, lo que hace que estos diccionarios no sean una base óptima para las enumeraciones. Incluso la búsqueda de miembros de enumeración (como en
StdEnum.MEMBER
) en sí misma es una búsqueda de diccionario.
Nuestro enfoque
Creamos nuestra implementación de enumeraciones teniendo en cuenta las enumeraciones elegantes en C y las enumeraciones extensibles hermosas en Java. Las principales funciones que queríamos implementar en casa fueron las siguientes:
- la enumeración debe ser lo más estática posible; "Estático" aquí significa lo siguiente: si algo puede calcularse solo una vez y durante el anuncio, entonces debe calcularse en este momento (y solo en este momento);
- es imposible heredar de una enumeración (debe ser una clase "final") si la clase heredada define nuevos miembros de la enumeración; esto es cierto para la implementación en la biblioteca estándar, con la excepción de que la herencia está prohibida allí, incluso si la clase heredada no define nuevos miembros;
- la enumeración debe tener un amplio alcance para la expansión (atributos adicionales, métodos, etc.)
Utilizamos la búsqueda de diccionario en el único caso: esta es la asignación inversa del valor del
value
a un miembro de enumeración. Todos los demás cálculos se realizan solo una vez durante la declaración de clase (donde las metaclases se utilizan para configurar la creación de tipos).
A diferencia de la biblioteca estándar, solo procesamos el primer valor después del signo
=
en la declaración de clase como valor de miembro:
A = 1, 'One'
en la biblioteca estándar, toda la tupla
1, "One"
considera como el valor del
value
;
A: 'MyEnum' = 1, 'One'
en nuestra implementación, solo
1
considera como un valor de
value
.
Se logra una mayor aceleración mediante el uso de
__slots__
donde sea posible. En las clases de Python declaradas con
__slots__
, el atributo
__dict__
no se crea en las
__dict__
, que contiene la asignación de nombres de atributos a sus valores (por lo tanto, no puede declarar ninguna propiedad de la instancia que no se mencione en
__slots__
). Además, se accede a los valores de los atributos definidos en
__slots__
en un desplazamiento constante en el puntero de instancia de objeto. Este es el acceso de alta velocidad a las propiedades porque evita los cálculos hash y los escaneos de tablas hash.
¿Cuáles son las fichas extra?
FastEnum no es compatible con ninguna versión de Python anterior a 3.6 porque universalmente usa anotaciones de tipo implementadas en Python 3.6. Se puede suponer que la instalación del módulo de
typing
desde PyPi ayudará. La respuesta corta es no. La implementación utiliza PEP-484 para los argumentos de algunas funciones, métodos y punteros al tipo de retorno, por lo que no se admite ninguna versión anterior a Python 3.5 debido a la incompatibilidad de sintaxis. Pero, de nuevo, la primera línea de código en la metaclase
__new__
usa la sintaxis PEP-526 para indicar el tipo de variable. Entonces Python 3.5 tampoco funcionará. Puede portar la implementación a versiones anteriores, aunque en Qrator Labs tendemos a usar anotaciones de tipo siempre que sea posible, ya que esto ayuda mucho en el desarrollo de proyectos complejos. Bueno, al final! No desea quedarse atascado en Python antes de la versión 3.6, ya que en las versiones más recientes no hay incompatibilidad con su código existente (siempre que no esté usando Python 2), y se ha realizado mucho trabajo en la implementación de
asyncio
comparación con 3.5, en nuestra opinión, vale la pena una actualización inmediata.
Esto, a su vez, hace que la importación especial de
auto
innecesaria, a diferencia de la biblioteca estándar. Simplemente indica que el miembro de la enumeración será una instancia de esta enumeración sin proporcionar ningún valor, y el valor se generará automáticamente para usted. Aunque Python 3.6 es suficiente para trabajar con FastEnum, tenga en cuenta que la preservación del orden de las teclas en los diccionarios se introdujo solo en Python 3.7 (y no utilizamos
OrderedDict
por separado para el caso 3.6). No conocemos ningún ejemplo en el que el orden de valores generado automáticamente sea importante, ya que suponemos que si el desarrollador proporcionó al entorno la tarea de generar y asignar un valor a un miembro de enumeración, entonces el valor en sí no es tan importante para él. Sin embargo, si aún no se ha cambiado a Python 3.7, se lo advertimos.
Aquellos que necesitan que sus enumeraciones comiencen desde 0 (cero) en lugar del valor predeterminado (1) pueden hacerlo utilizando un atributo especial al declarar la enumeración
_ZERO_VALUED
, que no se almacenará en la clase resultante.
Sin embargo, existen algunas restricciones: todos los nombres de los miembros de la enumeración deben estar escritos en letras MAYÚSCULAS; de lo contrario, la metaclase no los procesará.
Finalmente, puede declarar una clase base para sus enumeraciones (tenga en cuenta que la clase base puede usar la metaclase en sí misma, por lo que no necesita proporcionar la metaclase a todas las subclases), solo defina la lógica general (atributos y métodos) en esta clase y no defina los miembros de la enumeración (para que la clase no se "finalice"). Después de que pueda declarar tantas clases heredadas de esta clase como desee, y los propios herederos tendrán la misma lógica.
Alias y cómo pueden ayudar
Supongamos que tiene código usando:
package_a.some_lib_enum.MyEnum
Y que la clase MyEnum se declara de la siguiente manera:
class MyEnum(metaclass=FastEnum): ONE: 'MyEnum' TWO: 'MyEnum'
Ahora, ha decidido que desea refactorizar y transferir la lista a otro paquete. Creas algo como esto:
package_b.some_lib_enum.MyMovedEnum
Donde MyMovedEnum se declara así:
class MyMovedEnum(MyEnum): pass
Ahora está listo para la etapa en la que la transferencia ubicada en la dirección anterior se considera obsoleta. Reescribe las importaciones y las llamadas de esta enumeración para que ahora se use el nuevo nombre de esta enumeración (su alias); puede estar seguro de que todos los miembros de esta enumeración se declaran realmente en la clase con el nombre anterior. En la documentación de su proyecto, declara que
MyEnum
desuso y se eliminará del código en el futuro. Por ejemplo, en la próxima versión. Suponga que su código almacena sus objetos con atributos que contienen miembros de enumeración usando
pickle
. En este punto, usa
MyMovedEnum
en su código, pero internamente, todos los miembros de enumeración siguen siendo instancias de
MyEnum
. Su próximo paso es intercambiar las declaraciones de
MyEnum
y
MyMovedEnum
para que
MyMovedEnum
no sea una subclase de
MyEnum
y
MyEnum
todos sus miembros;
MyEnum
, por otro lado, ahora no declara ningún miembro, sino que se convierte en un alias (subclase) de
MyMovedEnum
.
Eso es todo Cuando reinicie sus aplicaciones en la etapa de
unpickle
todos los miembros de la enumeración se volverán a declarar como instancias de
MyMovedEnum
y se asociarán con esta nueva clase. En el momento en que esté seguro de que todos sus objetos almacenados, por ejemplo, en la base de datos, han sido re-deserializados (y posiblemente serializados nuevamente y almacenados en el repositorio), puede lanzar una nueva versión, en la que previamente se marcó como una clase obsoleta
MyEnum
puede declararse más innecesario y eliminarse de la base del código.
Pruébelo usted mismo:
github.com/QratorLabs/fastenum ,
pypi.org/project/fast-enum .
Los profesionales en karma van al autor FastEnum -
santjagocorkez .
UPD: en la versión 1.3.0, se hizo posible heredar de clases existentes, por ejemplo,
int
,
float
,
str
. Los miembros de tales enumeraciones pasan con éxito la prueba de igualdad a un objeto limpio con el mismo valor (
IntEnum.MEMBER == int(value_given_to_member)
) y, por supuesto, que son instancias de estas clases heredadas. Esto, a su vez, permite que el miembro de la enumeración heredado de
int
sea un argumento directo para
sys.exit()
como el código de retorno del intérprete de Python.