ENUM rápido

tl; dr


github.com/QratorLabs/fastenum
pip 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:
 # /path/to/package/static.py: INITIAL = 0 PROCESSING = 1 PROCESSED = 2 DECLINED = 3 RETURNED = 4 ... 

... 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 #        #   ,      one_value_attribute = StdEnum.ONE.value one_value = one_value_attribute.__get__(StdEnum.ONE) 

 #   ,  __get__     (  python3.7): def __get__(self, instance, ownerclass=None): if instance is None: if self.__isabstractmethod__: return self raise AttributeError() elif self.fget is None: raise AttributeError("unreadable attribute") return self.fget(instance) 

 #   DynamicClassAttribute     `name`  `value`   __get__()  : @DynamicClassAttribute def name(self): """The name of the Enum member.""" return self._name_ @DynamicClassAttribute def value(self): """The value of the Enum member.""" return self._value_ 

Assim, toda a sequência de chamadas pode ser representada pelo seguinte pseudo-código:
 def get_func(enum_member, attrname): #        __dict__,        -     return getattr(enum_member, f'_{attrnme}_') def get_name_value(enum_member): name_descriptor = get_descriptor(enum_member, 'name') if enum_member is None: if name_descriptor.__isabstractmethod__: return name_descriptor raise AttributeError() elif name_descriptor.fget is None: raise AttributeError("unreadable attribute") return get_func(enum_member, 'name') 

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.

Source: https://habr.com/ru/post/pt480614/


All Articles