tl; dr
github.com/QratorLabs/fastenumpip install fast-enum
Por que a enumeração é necessária
(se você souber tudo - vá para a seção "Enumerações na biblioteca padrão")
Imagine que você precise descrever um conjunto de todos os estados possíveis de entidades em seu próprio modelo de banco de dados. Provavelmente, você terá várias constantes definidas diretamente no namespace do módulo:
... ou como atributos de classe estática:
class MyModelStates: INITIAL = 0 PROCESSING = 1 PROCESSED = 2 DECLINED = 3 RETURNED = 4
Essa abordagem ajudará a se referir a esses estados por nomes mnemônicos, enquanto no seu repositório eles serão inteiros comuns. Assim, você se livra simultaneamente dos números mágicos espalhados em diferentes partes do código, tornando-o mais legível e informativo.
No entanto, a constante do módulo e a classe com atributos estáticos sofrem com a natureza intrínseca dos objetos Python: todos são mutáveis (mutáveis). Você pode acidentalmente atribuir um valor à sua constante no tempo de execução, e depurar e reverter objetos quebrados é uma aventura separada. Portanto, convém manter o pacote de constantes inalterado no sentido de que o número de constantes declaradas e seus valores para os quais estão mapeados não serão alterados durante a execução do programa.
Para fazer isso, você pode tentar organizá-los em tuplas nomeadas usando
namedtuple()
, como no exemplo:
MyModelStates = namedtuple('MyModelStates', ('INITIAL', 'PROCESSING', 'PROCESSED', 'DECLINED', 'RETURNED')) EntityStates = MyModelStates(0, 1, 2, 3, 4)
Mas isso não parece muito elegante e legível, e os objetos
namedtuple
, por sua vez, não são muito extensíveis. Suponha que você tenha uma interface do usuário que exiba todos esses estados. Você pode usar suas constantes em módulos, uma classe com atributos ou tuplas nomeadas para renderizá-las (as duas últimas são mais fáceis de renderizar porque estamos falando sobre isso). Mas esse código não torna possível fornecer ao usuário uma descrição adequada para cada estado que você definir. Além disso, se você planeja implementar o multilinguismo e o suporte ao i18n em sua interface do usuário, perceberá a rapidez com que concluir todas as traduções dessas descrições se torna uma tarefa incrivelmente entediante. Nomes de estados correspondentes não necessariamente significam correspondência à descrição, 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 assume o seguinte formato:
INITIAL = (0, 'My_MODEL_INITIAL_STATE')
Ou sua turma fica assim:
class MyModelStates: INITIAL = (0, 'MY_MODEL_INITIAL_STATE')
Por fim, a tupla nomeada se transforma em:
EntityStates = MyModelStates((0, 'MY_MODEL_INITIAL_STATE'), ...)
Já não é ruim - agora garante que o valor do status e o stub da tradução sejam exibidos nos idiomas suportados pela interface do usuário. Mas você pode perceber que o código usando esses mapeamentos se tornou uma bagunça. Sempre que tentar atribuir um valor de entidade, é necessário extrair o valor com o índice 0 da exibição que você está usando:
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 que usam constantes e atributos de classe, respectivamente, sofrem mutabilidade.
E as transferências vêm em nosso auxílio
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 é tudo. Agora você pode facilmente percorrer a listagem em sua renderização (sintaxe Jinja2):
{% for state in MyEntityState %} <option value=”{{ state.val }}”>{{ _(state.description) }}</option> {% endfor %}
Uma enumeração é imutável para um conjunto de elementos - você não pode definir um novo membro de uma enumeração em tempo de execução e não pode excluir um membro já definido e para os valores de elemento que ele armazena - não é possível [re] atribuir nenhum valor de atributo ou excluir um atributo.
No seu código, você simplesmente atribui valores às suas entidades, assim:
my_entity.state = MyEntityStates.INITIAL.val
Tudo é claro o suficiente, informativo e extensível. É para isso que usamos as enumerações.
Como poderíamos torná-lo mais rápido?
A enumeração da biblioteca padrão é bastante lenta, então nos perguntamos - podemos acelerá-la? Como se viu, podemos, a saber, a implementação de nossa enumeração:
- Três vezes mais rápido no acesso à enumeração de membros;
- ~ 8,5 mais rápido ao acessar o atributo (
name
, value
) de um membro; - 3 vezes mais rápido ao acessar um membro por valor (chame o construtor da enumeração
MyEnum(value))
; - 1,5 vezes mais rápido ao acessar um membro pelo nome (como no dicionário
MyEnum[name]
).
Tipos e objetos em Python são dinâmicos. Mas existem ferramentas para limitar a natureza dinâmica dos objetos. Você pode obter um aumento significativo no desempenho com
__slots__
. Também há potencial para ganhos de velocidade se você evitar o uso de descritores de dados sempre que possível - mas você deve considerar a possibilidade de um aumento significativo na complexidade do aplicativo.
Slots
Por exemplo, você pode usar uma declaração de classe usando
__slots__
- nesse caso, todas as instâncias de classes terão apenas um conjunto limitado de propriedades declaradas em
__slots__
e todos os
__slots__
classes pai.
Descritores
Por padrão, o intérprete Python retorna o valor do atributo do objeto diretamente (ao mesmo tempo, estipulamos que, nesse caso, o valor também é um objeto Python, e não, por exemplo, sem assinatura por muito tempo em termos da linguagem C):
value = my_obj.attribute # , .
De acordo com o modelo de dados Python, se o valor do atributo for um objeto que implementa o protocolo do descritor, ao tentar obter o valor desse atributo, o intérprete encontrará primeiro um link para o objeto ao qual a propriedade se refere e, em seguida, chamará o método
__get__
especial, que será transmitido ao nosso objeto original como argumento:
obj_attribute = my_obj.attribute obj_attribute_value = obj_attribute.__get__(my_obj)
Enumerações na biblioteca padrão
Pelo menos as propriedades de
name
e
value
dos membros da implementação de enumeração padrão são declaradas como
types.DynamicClassAttribute
. Isso significa que quando você tentar obter os valores de
name
e
value
, o seguinte acontecerá:
one_value = StdEnum.ONE.value
Assim, toda a sequência de chamadas pode ser representada pelo seguinte pseudo-código:
def get_func(enum_member, attrname):
Escrevemos um script simples demonstrando a saída descrita 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 após a execução, o script nos deu a seguinte imagem:

Isso mostra que toda vez que você acessa os atributos de
name
e
value
dos membros da enumeração da biblioteca padrão, um identificador é chamado. Este descritor, por sua vez, termina com uma chamada da classe
Enum
da biblioteca padrão do método
def name(self)
, decorada com o descritor.
Compare 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()
O que pode ser visto na imagem a seguir:

Tudo isso realmente acontece dentro da implementação de enumeração padrão toda vez que você acessa as propriedades de
name
e
value
de seus membros. Essa também é a razão pela qual nossa implementação é mais rápida.
A implementação de enumerações na biblioteca padrão do Python usa muitas chamadas para objetos que implementam o protocolo do descritor de dados. Quando tentamos usar a implementação de enumeração padrão em nossos projetos, notamos imediatamente quantos descritores de dados foram chamados para
name
e
value
.
E como as enumerações foram usadas extensivamente em todo o código, o desempenho resultante foi baixo.
Além disso, a classe Enum padrão contém vários atributos auxiliares "protegidos":
_member_names_
- uma lista contendo todos os nomes dos membros da enumeração;_member_map_
- OrderedDict
, que mapeia o nome de um membro de enumeração para seu valor;_value2member_map_
- um dicionário que contém correspondência na direção oposta: os valores dos membros da enumeração para os membros da enumeração correspondentes.
A pesquisa de dicionário é lenta, pois cada chamada leva ao cálculo da função de hash (a menos, é claro, que o resultado seja armazenado em cache separadamente, o que nem sempre é possível para código não gerenciado) e à pesquisa de tabela de hash, que torna esses dicionários uma base ideal para enumerações. Até a pesquisa por membros de enumeração (como no
StdEnum.MEMBER
) é uma pesquisa de dicionário.
Nossa abordagem
Criamos nossa implementação de enumerações de olho em enumerações elegantes em C e belas enumerações extensíveis em Java. As principais funções que queríamos implementar em casa eram as seguintes:
- a enumeração deve ser o mais estática possível; "Estático" aqui significa o seguinte - se algo puder ser calculado apenas uma vez e durante o anúncio, deve ser calculado neste momento (e somente neste momento);
- é impossível herdar de uma enumeração (deve ser uma classe "final") se a classe herdada definir novos membros da enumeração - isso é verdade para implementação na biblioteca padrão, exceto que a herança é proibida lá, mesmo que a classe herdada não defina novos membros;
- a enumeração deve ter amplo escopo para expansão (atributos adicionais, métodos etc.)
Usamos a pesquisa de dicionário no único caso - este é o mapeamento inverso do valor
value
para um membro de enumeração. Todos os outros cálculos são executados apenas uma vez durante a declaração de classe (onde as metaclasses são usadas para configurar a criação do tipo).
Diferente da biblioteca padrão, processamos apenas o primeiro valor após o sinal
=
na declaração da classe como um valor de membro:
A = 1, 'One'
na biblioteca padrão, toda a tupla
1, "One"
considerada como o valor do
value
;
A: 'MyEnum' = 1, 'One'
em nossa implementação, apenas
1
considerado como
value
.
Aceleração adicional é alcançada através do uso de
__slots__
que possível. Nas classes Python declaradas usando
__slots__
, o atributo
__dict__
não é criado nas
__dict__
, que contém o mapeamento dos nomes dos atributos para seus valores (portanto, você não pode declarar nenhuma propriedade da instância que não seja mencionada em
__slots__
). Além disso, os valores dos atributos definidos em
__slots__
são acessados com um deslocamento constante no ponteiro da instância do objeto. Esse é o acesso de alta velocidade às propriedades, pois evita cálculos de hash e varreduras de tabela de hash.
Quais são as fichas extras?
O FastEnum não é compatível com nenhuma versão do Python anterior à 3.6 porque universalmente usa anotações de tipo implementadas no Python 3.6. Pode-se supor que a instalação do módulo de
typing
do PyPi ajudará. A resposta curta é não. A implementação usa o PEP-484 para os argumentos de algumas funções, métodos e ponteiros para o tipo 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 na metaclasse
__new__
usa a sintaxe PEP-526 para indicar o tipo de variável. Portanto, o Python 3.5 também não funcionará. Você pode portar a implementação para versões mais antigas, embora nós do Qrator Labs tendamos a usar anotações de tipo sempre que possível, pois isso ajuda muito no desenvolvimento de projetos complexos. Bem, no final! Você não deseja ficar preso no Python antes da versão 3.6, pois nas versões mais recentes não há incompatibilidade com o código existente (desde que você não esteja usando o Python 2) e muito trabalho foi feito na implementação do
asyncio
comparado ao 3.5, em nossa opinião, vale uma atualização imediata.
Isso, por sua vez, torna desnecessária a importação especial de
auto
, diferente da biblioteca padrão. Você simplesmente indica que o membro da enumeração será uma instância dessa enumeração sem fornecer um valor - e o valor será gerado automaticamente para você. Embora o Python 3.6 seja suficiente para trabalhar com o FastEnum, lembre-se de que a preservação da ordem das chaves nos dicionários foi introduzida apenas no Python 3.7 (e não usamos o
OrderedDict
separadamente para o caso 3.6). Não conhecemos nenhum exemplo em que a ordem de valores gerada automaticamente seja importante, pois assumimos que, se o desenvolvedor forneceu ao ambiente a tarefa de gerar e atribuir um valor a um membro de enumeração, então o valor em si não é tão importante para ele. No entanto, se você ainda não mudou para o Python 3.7, avisamos.
Aqueles que precisam que suas enumerações iniciem de 0 (zero) em vez do valor padrão (1) podem fazer isso usando um atributo especial ao declarar a enumeração
_ZERO_VALUED
, que não será armazenada na classe resultante.
No entanto, existem algumas restrições: todos os nomes dos membros da enumeração devem ser escritos em letras maiúsculas, caso contrário, eles não serão processados pela metaclasse.
Finalmente, você pode declarar uma classe base para suas enumerações (lembre-se de que a classe base pode usar a metaclasse em si, para que você não precise fornecer a metaclasse para todas as subclasses) - basta definir a lógica geral (atributos e métodos) nessa classe e não definir os membros da enumeração (para que a turma não seja "finalizada"). Depois que você puder declarar quantas classes herdadas dessa classe desejar, os próprios herdeiros terão a mesma lógica.
Aliases e como eles podem ajudar
Suponha que você tenha código usando:
package_a.some_lib_enum.MyEnum
E que a classe MyEnum é declarada da seguinte maneira:
class MyEnum(metaclass=FastEnum): ONE: 'MyEnum' TWO: 'MyEnum'
Agora, você decidiu que deseja refatorar e transferir a listagem 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 o estágio em que a transferência localizada no endereço antigo é considerada obsoleta. Você reescreve as importações e chamadas dessa enumeração para que o novo nome dessa enumeração (seu alias) seja agora usado - você pode ter certeza de que todos os membros dessa enumeração de alias são realmente declarados na classe com o nome antigo. Na documentação do projeto, você declara que o
MyEnum
obsoleto e será removido do código no futuro. Por exemplo, na próxima versão. Suponha que seu código armazene seus objetos com atributos contendo membros de enumeração usando
pickle
. Neste ponto, você usa
MyMovedEnum
no seu código, mas internamente, todos os membros da enumeração ainda são instâncias do
MyEnum
. Seu próximo passo é trocar as declarações de
MyEnum
e
MyMovedEnum
para que
MyMovedEnum
não
MyMovedEnum
uma subclasse de
MyEnum
e
MyEnum
todos os seus membros;
MyEnum
, por outro lado, agora não declara nenhum membro, mas se torna apenas um alias (subclasse) de
MyMovedEnum
.
Isso é tudo. Quando você reinicia seus aplicativos no estágio
unpickle
todos os membros da enumeração serão declarados novamente como instâncias de
MyMovedEnum
e
MyMovedEnum
associados a essa nova classe. No momento em que você tiver certeza de que todos os seus objetos armazenados, por exemplo, no banco de dados, foram desserializados novamente (e possivelmente serializados novamente e armazenados no repositório) - é possível lançar um novo release, no qual ele foi marcado anteriormente como uma classe desatualizada
MyEnum
pode ser declarado mais desnecessário e removido da base de código.
Experimente você mesmo:
github.com/QratorLabs/fastenum ,
pypi.org/project/fast-enum .
Os profissionais de karma vão para o autor FastEnum -
santjagocorkez .
UPD: Na versão 1.3.0, tornou-se possível herdar das classes existentes, por exemplo,
int
,
float
,
str
. Os membros dessas enumerações passam com êxito no teste de igualdade para um objeto limpo com o mesmo valor (
IntEnum.MEMBER == int(value_given_to_member)
) e, é claro, que são instâncias dessas classes herdadas. Isso, por sua vez, permite que o membro da enum herdado de
int
seja um argumento direto para
sys.exit()
como o código de retorno do interpretador python.