tl; dr
github.com/QratorLabs/fastenumpip install fast-enum
O que são enums
(Se você acha que sabe disso - role para baixo até a seção “Enums na biblioteca padrão”).
Imagine que você precise descrever um conjunto de todos os estados possíveis para as entidades em seu modelo de banco de dados. Você provavelmente usará várias constantes definidas como atributos no nível do módulo:
... ou como atributos em nível de classe definidos em sua própria classe:
class MyModelStates: INITIAL = 0 PROCESSING = 1 PROCESSED = 2 DECLINED = 3 RETURNED = 4
Isso ajuda você a se referir a esses estados pelos nomes mnemônicos, enquanto eles persistem em seu armazenamento como números inteiros simples. Com isso, você se livra dos números mágicos espalhados pelo seu código e o torna mais legível e auto-descritivo.
Mas, tanto a constante no nível do módulo quanto a classe com os atributos estáticos sofrem com a natureza inerente dos objetos python: todos são mutáveis. Você pode acidentalmente atribuir um valor à sua constante no tempo de execução, e isso é uma bagunça para depurar e reverter suas entidades quebradas. Portanto, convém tornar seu conjunto de constantes imutável, o que significa que o número de constantes declaradas e os valores para os quais eles são mapeados não devem ser modificados no tempo de execução.
Para esse fim, você pode tentar organizá-los em tuplas nomeadas com
namedtuple()
, como um exemplo:
MyModelStates = namedtuple('MyModelStates', ('INITIAL', 'PROCESSING', 'PROCESSED', 'DECLINED', 'RETURNED')) EntityStates = MyModelStates(0, 1, 2, 3, 4)
No entanto, isso ainda não parece muito compreensível: além disso, os objetos
namedtuple
não são realmente extensíveis. Digamos que você tenha uma interface do usuário que exiba todos esses estados. Você pode usar suas constantes baseadas em módulo, sua classe com os atributos ou tuplas nomeadas para renderizá-las (as duas últimas são mais fáceis de renderizar, enquanto estamos nisso). Mas seu código não oferece nenhuma oportunidade para fornecer ao usuário uma descrição adequada para cada estado que você definiu. Além disso, se você planeja implementar o suporte multilíngue e o i18n na sua interface do usuário, verá que preencher todas as traduções dessas descrições se torna uma tarefa incrivelmente tediosa. Os valores do estado correspondente podem não ter necessariamente descrições correspondentes, o que significa que você não pode apenas mapear todos os seus estados
INITIAL
para a mesma descrição no
gettext
. Em vez disso, sua constante se torna esta:
INITIAL = (0, 'My_MODEL_INITIAL_STATE')
Sua classe então se torna esta:
class MyModelStates: INITIAL = (0, 'MY_MODEL_INITIAL_STATE')
E finalmente, o seu nome
namedtuple
se torna o seguinte:
EntityStates = MyModelStates((0, 'MY_MODEL_INITIAL_STATE'), ...)
Bem, bom o suficiente, agora garante que o valor do estado e o stub da tradução sejam mapeados para os idiomas suportados pela sua interface do usuário. Mas agora você pode perceber que o código que usa esses mapeamentos se transformou em uma bagunça. Sempre que você tenta atribuir um valor à sua entidade, também não se esqueça de extrair o valor no índice 0 do mapeamento usado:
my_entity.state = INITIAL[0]
ou
my_entity.state = MyModelStates.INITIAL[0]
ou
my_entity.state = EntityStates.INITIAL[0]
E assim por diante Lembre-se de que as duas primeiras abordagens usando constantes e atributos de classe, respectivamente, ainda sofrem mutabilidade.
E então Enums chegam ao palco
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')
É isso. Agora você pode facilmente iterar a enumeração no seu renderizador (sintaxe Jinja2):
{% for state in MyEntityState %} <option value=”{{ state.val }}”>{{ _(state.description) }}</option> {% endfor %}
Enum é imutável para o conjunto de membros (você não pode definir um novo membro em tempo de execução, nem pode excluir um membro já definido) e os valores de membro que eles mantêm (você não pode reatribuir nenhum valor de atributo ou excluir um atributo).
No seu código, você acabou de atribuir valores às suas entidades assim:
my_entity.state = MyEntityStates.INITIAL.val
Bem, claro o suficiente. Auto-descritivo. Bastante extensível. É para isso que usamos Enums.
Por que é mais rápido?
Mas o ENUM padrão é bastante lento, então nos perguntamos - poderíamos torná-lo mais rápido?
Como se vê, nós podemos. Ou seja, é possível fazê-lo:
- 3 vezes mais rápido no acesso de membros
- ~ 8,5 vezes mais rápido no acesso a atributos (
name
, value
) - 3 vezes mais rápido no acesso enum por valor (chame a classe
MyEnum(value)
da enum) - 1,5 vezes mais rápido no acesso enum por nome (
MyEnum[name]
tipo MyEnum[name]
)
Tipos e objetos são dinâmicos em Python. Mas o Python tem as ferramentas para limitar a natureza dinâmica dos objetos. Com a ajuda deles, é possível obter um aumento significativo no desempenho usando
__slots__
, além de evitar o uso de Descritores de Dados sempre que possível, sem um crescimento significativo da complexidade ou se você pode obter benefícios em velocidade.
Slots
Por exemplo, pode-se usar uma declaração de classe com
__slots__
- nesse caso, as instâncias de classe teriam apenas um conjunto restrito de atributos: atributos declarados em
__slots__
e todos os
__slots__
das classes pai.
Descritores
Por padrão, o interpretador Python retorna um valor de atributo de um objeto diretamente:
value = my_obj.attribute
De acordo com o modelo de dados Python, se o valor do atributo de um objeto é ele próprio um objeto que implementa o Data Descriptor Protocol, significa que, ao tentar obter esse valor, você primeiro obtém o atributo como objeto e, em seguida, um método especial
__get__
é chamou esse atributo-objeto passando o próprio objeto depositário como argumento:
obj_attribute = my_obj.attribute obj_attribute_value = obj_attribute.__get__(my_obj)
Enums na biblioteca padrão
Pelo menos os atributos de
name
e
value
da implementação padrão do Enum são declarados como
types.DynamicClassAttribute
. Isso significa que, quando você tenta obter o
name
(ou
value
) de um membro, o fluxo é o seguinte:
one_value = StdEnum.ONE.value
Portanto, o fluxo completo pode ser representado como o seguinte pseudocódigo:
def get_func(enum_member, attrname):
Criamos um script simples que demonstra a conclusão acima:
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()
E depois que executamos o script, ele criou esta imagem para nós:

Isso prova que sempre que você acessa o
name
e o
value
dos atributos de stdlib enum, ele chama um descritor. Por sua vez, esse descritor termina com uma chamada à propriedade
def name(self)
stdlib enum, decorada com o descritor.
Bem, você pode comparar isso com o nosso 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()
Qual saída esta imagem:

Isso é o que realmente é feito na implementação padrão do Enum sempre que você acessa os atributos de
name
e
value
dos seus membros do Enum. E é por isso que nossa implementação é mais rápida.
A implementação da classe Enum da Python Standard Library usa toneladas de chamadas de protocolo descritor. Quando tentamos usar enum padrão em nossos projetos, notamos quantas chamadas de protocolo do descritor para atributos de
name
e
value
dos membros do
Enum
foram chamadas. E como as enumerações foram usadas excessivamente em todo o código, o desempenho resultante foi ruim.
Além disso, a classe enum padrão contém alguns atributos auxiliares "protegidos":
_member_names_
- uma lista que contém todos os nomes dos membros da enumeração;_member_map_
- um OrderedDict que mapeia o nome de um membro da enumeração para o próprio membro;_value2member_map_
- um dicionário reverso que mapeia valores de membros de enumeração para membros de enumeração correspondentes.
As pesquisas de dicionário são lentas, pois cada uma leva a um cálculo de hash e a uma tabela de hash, tornando esses dicionários estruturas de base não ideais para a classe enum. Até a própria recuperação de membro (como em
StdEnum.MEMBER
) é uma pesquisa de dicionário.
Nosso caminho
Ao desenvolver nossa implementação do Enum, tínhamos em mente aquelas bonitas enumerações em linguagem C e os lindos Java Enums extensíveis. Os principais recursos que queríamos em nossa implementação:
- um Enum deve ser o mais estático possível; por "estático" queremos dizer: se algo poderia ser calculado uma vez e no momento da declaração, deveria;
- uma Enum não pode ser subclassificada (deve ser uma classe "final") se uma subclasse definir novos membros da enum - isso é verdade para a implementação padrão da biblioteca, com a exceção de que a subclasse é proibida, mesmo que nenhum novo membro seja definido;
- um Enum deve ter vastas possibilidades de extensões (atributos, métodos adicionais e outros).
O único momento em que usamos pesquisas de dicionário é em um
value
mapeamento reverso para o membro Enum. Todos os outros cálculos são feitos apenas uma vez durante a declaração de classe (onde ganchos de metaclasses são usados para personalizar a criação de tipos).
Em contraste com a implementação da biblioteca padrão, tratamos o primeiro valor após o sinal
=
na declaração da classe como o valor do membro:
A = 1, 'One'
na biblioteca padrão enum toda a tupla
1, "One"
é tratado como
value
A: 'MyEnum' = 1, 'One'
em nossa implementação, apenas
1
é tratado como
value
Maior velocidade é obtida usando
__slots__
sempre que possível. No modelo de dados Python, as classes declaradas com
__slots__
não possuem o atributo
__dict__
que contém atributos de instância (portanto, você não pode atribuir nenhum atributo que não seja mencionado em
__slots__
). Além disso, os atributos definidos em
__slots__
acessados com deslocamentos constantes do ponteiro de objeto no nível C. Esse é o acesso a atributos de alta velocidade, pois evita cálculos de hash e varreduras de hashtable.
Quais são as vantagens adicionais?
O FastEnum não é compatível com nenhuma versão do Python anterior à 3.6, pois usa excessivamente o módulo de
typing
que foi introduzido no Python 3.6; Pode-se supor que a instalação de um módulo de
typing
backport do PyPI ajudaria. A resposta é: não. A implementação usa o PEP-484 para algumas funções e argumentos de métodos e dica de tipo de valor de retorno, portanto, qualquer versão anterior ao Python 3.5 não é suportada devido à incompatibilidade de sintaxe. Mas, novamente, a primeira linha de código em
__new__
da metaclasse usa a sintaxe PEP-526 para dicas de tipo variável. Portanto, o Python 3.5 também não. É possível portar a implementação para versões mais antigas, embora nós, no Qrator Labs, tendamos a usar dicas de tipo sempre que possível, pois isso ajuda muito no desenvolvimento de projetos complexos. E ei! Você não deseja aderir a nenhum python anterior à 3.6, pois não há incompatibilidades anteriores com o código existente (supondo que você não esteja usando o Python 2), embora muito trabalho tenha sido feito em assíncrono em comparação com o 3.5.
Isso, por sua vez, torna desnecessárias as importações especiais, como
auto
, ao contrário da biblioteca padrão. Você digita uma dica para todos os seus membros Enum com o nome da classe Enum, sem fornecer nenhum valor - e o valor seria gerado automaticamente para você. Embora o python 3.6 seja suficiente para trabalhar com o FastEnum, esteja avisado de que a ordem padrão de garantia de declaração do dicionário foi introduzida apenas no python 3.7. Não conhecemos nenhum dispositivo útil em que a ordem de valor gerada automaticamente seja importante (já que assumimos que o valor gerado em si não é o valor que um programador se importa). No entanto, considere-se avisado se ainda continuar com o python 3.6;
Aqueles que precisam de seu enum iniciam de 0 (zero) em vez do padrão 1 podem fazer isso com um atributo de declaração de enum especial
_ZERO_VALUED
, esse atributo é "apagado" da classe Enum resultante;
Porém, existem algumas limitações: todos os nomes de membros da enum devem ser CAPITALIZADOS ou não serão capturados pela metaclasse e não serão tratados como membros da enum;
No entanto, você pode declarar uma classe base para suas enumerações (lembre-se de que a classe base pode usar a própria metaclasse enum, portanto, não é necessário fornecer metaclasse para todas as subclasses): você pode definir lógica comum (atributos e métodos) neste classe, mas não pode definir membros da enumeração (para que a classe não seja "finalizada"). Você pode subclassar essa classe em quantas declarações de enumeração desejar e que forneceriam toda a lógica comum;
Aliases Vamos explicá-los em um tópico separado (implementado em 1.2.5)
Aliases e como eles poderiam ajudar
Suponha que você tenha um código que use:
package_a.some_lib_enum.MyEnum
E que o MyEnum é declarado assim:
class MyEnum(metaclass=FastEnum): ONE: 'MyEnum' TWO: 'MyEnum'
Agora, você decidiu refatorar e deseja mover sua enumeração para outro pacote. Você cria algo como isto:
package_b.some_lib_enum.MyMovedEnum
Onde MyMovedEnum é declarado assim:
class MyMovedEnum(MyEnum): pass
Agora Você está pronto para iniciar o estágio de "descontinuação" de todo o código que usa suas enumerações. Você desvia o uso direto do
MyEnum
para usar o
MyMovedEnum
(este último possui todos os seus membros como proxy no
MyEnum
). Você declara nos documentos do projeto que o
MyEnum
está obsoleto e será removido do código em algum momento no futuro. Por exemplo, na próxima versão. Considere que seu código salva seus objetos com atributos enum usando pickle. Neste ponto, você usa
MyMovedEnum
no seu código, mas internamente todos os seus membros da enum ainda são as instâncias do
MyEnum
. Seu próximo passo seria trocar as declarações de
MyEnum
e
MyMovedEnum
para que
MyMovedEnum
agora não seja uma subclasse de
MyEnum
e declare todos os seus membros;
MyEnum
, por outro lado, não declararia nenhum membro, mas se tornaria apenas um alias (subclasse) de
MyMovedEnum
.
E isso conclui. No reinício de seus tempos de execução no estágio unpickle, todos os seus valores de enumeração serão redirecionados para
MyMovedEnum
e vinculados novamente a essa nova classe. No momento em que você tiver certeza de que todos os seus objetos
MyEnum
(re) da estrutura da organização da classe, você poderá fazer um novo lançamento, onde anteriormente marcado como obsoleto, o
MyEnum
pode ser declarado obsoleto e eliminado da sua base de código.
Nós encorajamos você a experimentar!
github.com/QratorLabs/fastenum ,
pypi.org/project/fast-enum . Todos os créditos vão para o autor do
FastEnum, santjagocorkez .