Usando módulos rigorosos em projetos Python em larga escala: experiência no Instagram. Parte 2

Apresentamos a sua atenção a segunda parte da tradução do material dedicado aos recursos do trabalho com módulos em projetos do Instagram em Python. A primeira parte da tradução deu uma visão geral da situação e mostrou dois problemas. Um deles diz respeito ao início lento do servidor, o segundo - os efeitos colaterais de comandos de importação não seguros. Hoje essa conversa continuará. Vamos considerar outro problema e falar sobre abordagens para resolver todos os problemas levantados.



Problema 3: status global mutável


Dê uma olhada em outra categoria de erros comuns.

def myview(request):     SomeClass.id = request.GET.get("id") 

Aqui estamos na função de apresentação e anexamos o atributo a uma determinada classe com base nos dados recebidos da solicitação. Você provavelmente já entendeu a essência do problema. O fato é que as classes são singletones globais. E aqui colocamos o estado, dependendo da solicitação, em um objeto de vida longa. Em um processo de servidor da Web que leva muito tempo para ser concluído, isso pode levar à poluição de todas as solicitações futuras feitas como parte desse processo.

O mesmo pode acontecer facilmente nos testes. Em particular, nos casos em que os programadores tentam usar patches de macaco e não usam o gerenciador de contexto , como mock.patch . Isso pode levar não à poluição de solicitações, mas à poluição de todos os testes que serão executados no mesmo processo. Esse é um motivo sério para o comportamento não confiável de nosso sistema de testes. Este é um problema significativo e é muito difícil evitar isso. Como resultado, abandonamos o sistema de teste unificado e mudamos para um esquema de isolamento de teste, que pode ser descrito como "um teste por processo".

Na verdade, este é o nosso terceiro problema. Um estado global mutável é um fenômeno não exclusivo do Python. Você pode encontrá-lo em qualquer lugar. Estamos falando de classes, módulos, listas ou dicionários anexados a módulos ou classes, sobre objetos singleton criados no nível do módulo. Trabalhar em tal ambiente requer disciplina. Para evitar a poluição do estado global enquanto o programa está em execução, você precisa de um conhecimento muito bom do Python.

Apresentando módulos rigorosos


Uma das causas principais de nossos problemas pode ser o uso do Python para resolver problemas para os quais essa linguagem não foi projetada. Em equipes pequenas e projetos pequenos, se você seguir as regras ao usar o Python, essa linguagem funcionará perfeitamente. E devemos ir para uma linguagem mais rigorosa.

Mas nossa base de códigos já superou o tamanho que nos permite pelo menos falar sobre como reescrevê-lo em outro idioma. E, mais importante, apesar de todos os problemas que enfrentamos, o Python tem muito a ver com isso. Ele nos dá mais do que mal. Nossos desenvolvedores realmente gostam dessa linguagem. Como resultado, depende apenas de nós como fazer com que o Python funcione em nossa escala e como garantir que possamos continuar trabalhando no projeto à medida que ele se desenvolve.

Encontrar soluções para nossos problemas nos levou a uma idéia. Consiste no uso de módulos rígidos.

Módulos estritos são módulos Python de um novo tipo, no início dos quais existe uma construção __strict__ = True . Eles são implementados usando muitos dos mecanismos de extensibilidade de baixo nível que o Python já possui. Um carregador de módulo especial analisa o código usando o módulo ast , executa uma interpretação abstrata do código carregado para analisá-lo, aplica várias transformações ao AST e, em seguida, compila o AST novamente no bytecode do Python usando a função de compile interna.

Sem efeitos colaterais de importação


Módulos estritos impõem algumas restrições ao que pode acontecer no nível do módulo. Portanto, todo o código no nível do módulo (incluindo decoradores e funções / inicializadores chamados no nível do módulo) deve estar limpo, ou seja, código isento de efeitos colaterais e que não use mecanismos de E / S. Essas condições são verificadas pelo interpretador abstrato usando os meios de análise de código estático no tempo de compilação.

Isso significa que o uso de módulos estritos não causa efeitos colaterais ao importá-los. O código executado durante a importação do módulo não pode mais causar problemas inesperados. Devido ao fato de testá-lo no nível da interpretação abstrata, usando ferramentas que compreendem um grande subconjunto do Python, eliminamos a necessidade de limitar excessivamente a expressividade do Python. Muitos tipos de código dinâmico, sem efeitos colaterais, podem ser usados ​​com segurança no nível do módulo. Isso inclui vários decoradores e a definição de constantes no nível do módulo usando listas ou geradores de dicionário.

Vamos deixar mais claro, considere um exemplo. Aqui está o módulo estrito corretamente escrito:

 """Module docstring.""" __strict__ = True from utils import log_to_network MY_LIST = [1, 2, 3] MY_DICT = {x: x+1 for x in MY_LIST} def log_calls(func):    def _wrapped(*args, **kwargs):        log_to_network(f"{func.__name__} called!")        return func(*args, **kwargs)    return _wrapped @log_calls def hello_world():    log_to_network("Hello World!") 

Neste módulo, podemos usar as construções usuais do Python, incluindo código dinâmico, um que é usado para criar o dicionário e outro que descreve o decorador no nível do módulo. Ao mesmo tempo, acessar recursos de rede nas funções _wrapped ou hello_world é completamente normal. O fato é que eles não são chamados no nível do módulo.

Mas se log_to_network chamada log_to_network para a função externa log_calls , ou se tentássemos usar um decorador que causasse efeitos colaterais (como @route do exemplo anterior), ou se @route a chamada hello_world() no nível do módulo, deixaria de ser estritamente estrita -module.

Como descobrir que não é seguro chamar log_to_network ou route funções no nível do módulo? Partimos do pressuposto de que tudo o que é importado de módulos que não sejam módulos estritos não é seguro, com exceção de algumas funções da biblioteca padrão que são conhecidas por serem seguras. Se o módulo utils for um módulo estrito, podemos confiar na análise do nosso módulo para nos informar se a função log_to_network é log_to_network .

Além de melhorar a confiabilidade do código, as importações que não apresentam efeitos colaterais eliminam uma séria barreira para garantir downloads incrementais de código. Isso abre outras possibilidades para explorar maneiras de acelerar as equipes de importação. Se o código no nível do módulo estiver livre de efeitos colaterais, isso significa que podemos executar com segurança instruções individuais do módulo no modo "preguiçoso", mediante solicitação, ao acessar os atributos do módulo. Isso é muito melhor do que seguir o algoritmo “ganancioso”, na aplicação em que todo o código do módulo é executado antecipadamente. E, levando em consideração o fato de que a forma de todas as classes no módulo estrito é completamente conhecida em tempo de compilação, no futuro, podemos até tentar organizar o armazenamento permanente de metadados do módulo (classes, funções, constantes) gerados durante a execução do código. Isso nos permitirá organizar a importação rápida de módulos inalterados, o que não requer execução repetida do bytecode do nível do módulo.

Imunidade e atributo __slots__


Módulos e classes estritos declarados neles são imutáveis ​​após serem criados. Os módulos são imutáveis ​​com a ajuda da transformação interna do corpo do módulo em uma função na qual o acesso a todas as variáveis ​​globais é organizado por meio de variáveis ​​de fechamento. Essas mudanças reduziram seriamente as possibilidades de uma mudança aleatória no estado global, embora o estado global mutável ainda possa ser resolvido se for decidido usá-lo através de contêineres mutáveis ​​no nível do módulo.

Membros de classes declaradas em módulos estritos também devem ser declarados em __init__ . Eles são gravados automaticamente no atributo __slots__ durante a transformação AST executada pelo carregador de módulos. Como resultado, mais tarde você não poderá mais anexar atributos adicionais à instância da classe. Aqui está uma classe semelhante:

 class Person:    def __init__(self, name, age):        self.name = name        self.age = age 

Durante a transformação AST, que é executada durante o processamento de módulos estritos, as operações de atribuição de valores age atributos de name e age executados em __init__ serão detectadas e um atributo do formato __slots__ = ('name', 'age') será anexado à classe. Isso impedirá que outros atributos sejam adicionados à instância da classe. (Se as anotações de tipo forem usadas, levaremos em conta as informações sobre os tipos disponíveis no nível da classe, como name: str , e também as adicionaremos à lista de slots).

As limitações descritas não apenas tornam o código mais confiável. Eles ajudam a acelerar a execução do código. A transformação automática de classes com a adição do atributo __slots__ aumenta a eficiência do uso de memória ao trabalhar com essas classes. Isso permite que você se livre das pesquisas de dicionário ao trabalhar com instâncias individuais de classes, o que acelera o acesso aos atributos. Além disso, podemos continuar a otimizar esses padrões durante a execução do código Python, o que nos permitirá melhorar ainda mais nosso sistema.

Sumário


Módulos rigorosos ainda são tecnologia experimental. Temos um protótipo funcional, estamos nos estágios iniciais de implantação desses recursos na produção. Esperamos que, depois de ganhar experiência suficiente no uso de módulos estritos, possamos conversar mais sobre eles.

Caros leitores! Você acha que os recursos oferecidos por módulos estritos são úteis no seu projeto Python?


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


All Articles