tl; dr
github.com/QratorLabs/fastenumpip install fast-enum
¿Qué son las enumeraciones?
(Si cree que lo sabe, desplácese hacia abajo hasta la sección "Enums en la biblioteca estándar").
Imagine que necesita describir un conjunto de todos los estados posibles para las entidades en su modelo de base de datos. Probablemente usará un montón de constantes definidas como atributos de nivel de módulo:
... o como atributos de nivel de clase definidos en su propia clase:
class MyModelStates: INITIAL = 0 PROCESSING = 1 PROCESSED = 2 DECLINED = 3 RETURNED = 4
Eso lo ayuda a referirse a esos estados por sus nombres mnemotécnicos, mientras persisten en su almacenamiento como enteros simples. Con esto, se deshace de los números mágicos dispersos a través de su código y lo hace más legible y descriptivo.
Pero, tanto la constante de nivel de módulo como la clase con los atributos estáticos adolecen de la naturaleza inherente de los objetos de python: todos son mutables. Puede asignar accidentalmente un valor a su constante en tiempo de ejecución, y eso es un desastre para depurar y revertir sus entidades rotas. Por lo tanto, es posible que desee que su conjunto de constantes sea inmutable, lo que significa que tanto el número de constantes declaradas como los valores a los que están asignados no deben modificarse en tiempo de ejecución.
Para este propósito, podría intentar organizarlos en tuplas con nombre con
namedtuple()
, como ejemplo:
MyModelStates = namedtuple('MyModelStates', ('INITIAL', 'PROCESSING', 'PROCESSED', 'DECLINED', 'RETURNED')) EntityStates = MyModelStates(0, 1, 2, 3, 4)
Sin embargo, esto todavía no parece demasiado comprensible: además de eso, los objetos con nombre de
namedtuple
no son realmente extensibles. Digamos que tiene una interfaz de usuario que muestra todos estos estados. Luego puede usar sus constantes basadas en módulos, su clase con los atributos o tuplas con nombre para representarlas (las dos últimas son más fáciles de procesar, mientras estamos en ello). Pero su código no brinda ninguna oportunidad para darle al usuario una descripción adecuada de cada estado que haya definido. Además, si planea implementar soporte multilingüe e i18n en su interfaz de usuario, encontrará que completar todas las traducciones de estas descripciones se convierte en una tarea increíblemente tediosa. Es posible que los valores de estado coincidentes no tengan necesariamente descripciones coincidentes, lo que significa que no puede simplemente asignar todos sus estados
INITIAL
a la misma descripción en
gettext
. En cambio, su constante se convierte en esto:
INITIAL = (0, 'My_MODEL_INITIAL_STATE')
Tu clase se convierte en esto:
class MyModelStates: INITIAL = (0, 'MY_MODEL_INITIAL_STATE')
Y finalmente, tu
namedtuple
convierte en esto:
EntityStates = MyModelStates((0, 'MY_MODEL_INITIAL_STATE'), ...)
Bueno, lo suficientemente bueno, ahora se asegura de que tanto el valor de estado como el código auxiliar de traducción estén asignados a los idiomas admitidos por su IU. Pero ahora puede notar que el código que usa esas asignaciones se ha convertido en un desastre. Siempre que intente asignar un valor a su entidad, tampoco debe olvidar extraer el valor en el índice 0 de la asignación que utiliza:
my_entity.state = INITIAL[0]
o
my_entity.state = MyModelStates.INITIAL[0]
o
my_entity.state = EntityStates.INITIAL[0]
Y así sucesivamente. Tenga en cuenta que los primeros dos enfoques que usan constantes y atributos de clase, respectivamente, aún sufren de mutabilidad.
Y luego Enums viene al escenario
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 podría iterar fácilmente la enumeración en su renderizador (sintaxis Jinja2):
{% for state in MyEntityState %} <option value=”{{ state.val }}”>{{ _(state.description) }}</option> {% endfor %}
Enum es inmutable tanto para el conjunto de miembros (no puede definir un nuevo miembro en tiempo de ejecución, ni puede eliminar un miembro ya definido) y los valores de miembro que mantienen (no puede reasignar ningún valor de atributo ni eliminar un atributo).
En su código, simplemente asigna valores a sus entidades de esta manera:
my_entity.state = MyEntityStates.INITIAL.val
Bueno, lo suficientemente claro. Autodescriptivo. Bastante extensible. Para eso utilizamos Enums.
¿Por qué es más rápido?
Pero el ENUM predeterminado es bastante lento, así que nos preguntamos: ¿podríamos hacerlo más rápido?
Resulta que sí podemos. A saber, es posible hacerlo:
- 3 veces más rápido en el acceso de miembros
- ~ 8.5 veces más rápido en el acceso al atributo (
name
, value
) - 3 veces más rápido en el acceso enum por valor (llame a la clase
MyEnum(value)
enum) - 1.5 veces más rápido en el acceso enum por nombre (dict-like
MyEnum[name]
)
Los tipos y objetos son dinámicos en Python. Pero Python tiene las herramientas para limitar la naturaleza dinámica de los objetos. Con su ayuda, se puede obtener un aumento significativo del rendimiento utilizando
__slots__
, así como evitar el uso de descriptores de datos cuando sea posible sin un crecimiento de complejidad significativo o si puede obtener beneficios en la velocidad.
Tragamonedas
Por ejemplo, uno podría usar una declaración de clase con
__slots__
; en este caso, las instancias de clase solo tendrían un conjunto restringido de atributos: atributos declarados en
__slots__
y todos los
__slots__
de clases primarias.
Descriptores
Por defecto, el intérprete de Python devuelve un valor de atributo de un objeto directamente:
value = my_obj.attribute
De acuerdo con el modelo de datos de Python, si el valor del atributo de un objeto es en sí mismo un objeto que implementa el Protocolo Descriptor de Datos, significa que cuando intentas obtener ese valor, primero obtienes el atributo como un objeto y luego un método especial
__get__
es invocó a ese atributo-objeto que pasa el objeto guardián como argumento:
obj_attribute = my_obj.attribute obj_attribute_value = obj_attribute.__get__(my_obj)
Enums en la biblioteca estándar
Al menos los atributos de
name
y
value
de la implementación estándar de Enum se declaran como
types.DynamicClassAttribute
. Eso significa que cuando intenta obtener el
name
(o
value
) de un miembro, el flujo es el siguiente:
one_value = StdEnum.ONE.value
Entonces, el flujo completo podría representarse como el siguiente pseudocódigo:
def get_func(enum_member, attrname):
Hemos creado un script simple que demuestra la conclusión anterior:
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 ejecutar el script, creó esta imagen para nosotros:

Esto demuestra que cada vez que accede al
name
y al
value
los atributos de stdlib enum, llama a un descriptor. Ese descriptor a su vez termina con una llamada a la propiedad
def name(self)
stdlib enum decorada con el descriptor.
Bueno, puedes comparar esto 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 genera esta imagen:

Eso es lo que realmente se hace dentro de la implementación estándar de Enum cada vez que accede a los atributos de
name
y
value
de los miembros de Enum. Y es por eso que nuestra implementación es más rápida.
La implementación de Python Standard Library de la clase Enum utiliza toneladas de llamadas de protocolo descriptor. Cuando intentamos usar la enumeración estándar en nuestros proyectos, notamos cuántas llamadas al protocolo descriptor para los atributos de
name
y
value
de los miembros de
Enum
se invocaron. Y debido a que las enumeraciones se usaron excesivamente en todo el código, el rendimiento resultante fue deficiente.
Además, la clase enum estándar contiene un par de atributos auxiliares "protegidos":
_member_names_
: una lista que contiene todos los nombres de los miembros de enumeración;_member_map_
: un OrderedDict que asigna un nombre de un miembro de enumeración al miembro mismo;_value2member_map_
: un diccionario inverso que asigna valores de miembros de enumeración a los miembros de enumeración correspondientes.
Las búsquedas de diccionario son lentas, ya que cada una conduce a un cálculo hash y una búsqueda de tabla hash, lo que hace que esos diccionarios no sean estructuras de base óptimas para la clase enum. Incluso la recuperación de miembros en sí (como en
StdEnum.MEMBER
) es una búsqueda de diccionario.
Nuestro camino
Mientras desarrollamos nuestra implementación de Enum, tuvimos en cuenta esas enumeraciones bonitas en lenguaje C y las hermosas Enums extensibles de Java. Las principales características que queríamos en nuestra implementación:
- un Enum debe ser lo más estático posible; por "estático" queremos decir: si algo se puede calcular una vez y en el momento de la declaración, debería;
- una Enum no puede subclasificarse (debe ser una clase "final") si una subclase define nuevos miembros de enumeración; esto es cierto para la implementación estándar de la biblioteca, con la excepción de que la subclasificación está prohibida incluso si no se definieron nuevos miembros;
- un Enum debería tener grandes posibilidades de extensiones (atributos adicionales, métodos, etc.).
La única vez que usamos búsquedas de diccionario es en un
value
mapeo inverso al miembro Enum. Todos los demás cálculos se realizan solo una vez durante la declaración de clase (donde los ganchos de metaclases se utilizan para personalizar la creación de tipos).
A diferencia de la implementación estándar de la biblioteca, tratamos el primer valor después del signo
=
en la declaración de clase como el valor del miembro:
A = 1, 'One'
en la biblioteca estándar enumera toda la tupla
1, "One"
se trata como
value
A: 'MyEnum' = 1, 'One'
en nuestra implementación solo
1
se trata como
value
Se acelera aún más mediante el uso de
__slots__
siempre que sea posible. En el modelo de datos de Python, las clases declaradas con
__slots__
no tienen el atributo
__dict__
que contiene atributos de instancia (por lo que no puede asignar ningún atributo que no se mencione en
__slots__
). Además, los atributos definidos en
__slots__
accedieron en desplazamientos constantes al puntero de objeto de nivel C. Es acceso a atributos de alta velocidad, ya que evita los cálculos hash y los escaneos de tablas hash.
¿Cuáles son las ventajas adicionales?
FastEnum no es compatible con ninguna versión de Python anterior a 3.6, ya que utiliza excesivamente el módulo de
typing
que se introdujo en Python 3.6; Se podría suponer que ayudaría instalar un módulo de
typing
con respaldo de PyPI. La respuesta es: no. La implementación utiliza PEP-484 para algunas funciones y métodos, argumentos y sugerencias de tipo de valor 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
__new__
de la metaclase usa la sintaxis PEP-526 para sugerencias de tipo variable. Entonces Python 3.5 tampoco lo hará. Es posible portar la implementación a versiones anteriores, aunque en Qrator Labs tendemos a usar sugerencias de tipo siempre que sea posible, ya que ayuda a desarrollar proyectos complejos en gran medida. Y oye! No desea apegarse a ninguna python anterior a 3.6 ya que no hay incompatibilidades con su código existente (suponiendo que no esté usando Python 2), aunque se realizó mucho trabajo en asyncio en comparación con 3.5.
Eso, a su vez, hace innecesarias las importaciones especiales como
auto
, a diferencia de la biblioteca estándar. Escriba una pista a todos los miembros de Enum con su nombre de clase de Enum, sin proporcionar ningún valor, y el valor se generaría automáticamente. Aunque python 3.6 es suficiente para trabajar con FastEnum, tenga en cuenta que el orden estándar de garantía de declaración del diccionario se introdujo solo en python 3.7. No conocemos dispositivos útiles en los que el orden de valor generado automáticamente sea importante (dado que asumimos que el valor generado en sí mismo no es el valor que le interesa a un programador). Sin embargo, considérate advertido si sigues con Python 3.6;
Aquellos que necesitan su enumeración comienzan desde 0 (cero) en lugar del predeterminado 1 pueden hacerlo con un atributo especial de declaración de enumeración
_ZERO_VALUED
, ese atributo se "borra" de la clase Enum resultante;
Sin embargo, existen algunas limitaciones: todos los nombres de miembros de enum deben estar CAPITALIZADOS o la metaclase no los recogerá y no se tratarán como miembros de enum;
Sin embargo, podría declarar una clase base para sus enumeraciones (tenga en cuenta que la clase base puede usar la metaclase enum, por lo que no necesita proporcionar metaclase a todas las subclases): puede definir una lógica común (atributos y métodos) en este clase, pero puede no definir miembros de enumeración (para que la clase no se "finalice"). Entonces podría subclasificar esa clase en tantas declaraciones enum como desee y eso le proporcionaría toda la lógica común;
Alias Los explicaremos en un tema separado (implementado en 1.2.5)
Alias y cómo podrían ayudar
Supongamos que tiene un código que usa:
package_a.some_lib_enum.MyEnum
Y que MyEnum se declara así:
class MyEnum(metaclass=FastEnum): ONE: 'MyEnum' TWO: 'MyEnum'
Ahora, decidió realizar algunas refactorizaciones y desea mover su enumeración 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 comenzar la etapa de "desaprobación" de todo el código que utiliza sus enumeraciones. Usted desvía los usos directos de
MyEnum
para usar
MyMovedEnum
(este último tiene todos sus miembros en proxy en
MyEnum
). Usted
MyEnum
dentro de los documentos de su proyecto que
MyEnum
está en desuso y se eliminará del código en algún momento en el futuro. Por ejemplo, en la próxima versión. Considere que su código guarda sus objetos con atributos de enumeración usando pickle. En este punto, usa
MyMovedEnum
en su código, pero internamente todos los miembros de su enumeración siguen siendo las instancias de
MyEnum
. Su próximo paso sería intercambiar declaraciones de
MyEnum
y
MyMovedEnum
para que
MyMovedEnum
ahora no sea una subclase de
MyEnum
y declare a todos sus miembros;
MyEnum
, por otro lado, no declararía ningún miembro, sino que se convertiría en un alias (subclase) de
MyMovedEnum
.
Y eso lo concluye. En el reinicio de sus tiempos de ejecución en la etapa de inactividad, todos sus valores de enumeración se redirigirán a
MyMovedEnum
y se volverán a vincular a esa nueva clase. En el momento en que esté seguro de que todos sus objetos encurtidos han sido (re) encurtidos con esta estructura de organización de clase, tiene la libertad de hacer una nueva versión, donde previamente marcado como obsoleto,
MyEnum
puede declararse obsoleto y borrado de su base de código.
¡Te animamos a que lo pruebes!
github.com/QratorLabs/fastenum ,
pypi.org/project/fast-enum . Todos los créditos van al autor
FastEnum santjagocorkez .