REPL inútil Relatório Yandex

O REPL (loop de leitura-avaliação-impressão) é inútil no Python, mesmo que seja IPython mágico. Hoje vou oferecer uma das soluções possíveis para esse problema. Primeiro de tudo, o relatório e minha extensão TheREPL serão úteis para aqueles que estão interessados ​​em um desenvolvimento mais rápido e eficiente, bem como para aqueles que escrevem sistemas com estado.


- Meu nome é Alexander, trabalho como programador em Yandex. Estamos escrevendo na minha equipe em Python, ainda não mudamos para o Go. Porém, no meu tempo livre, por incrível que pareça, também programo e faço em uma linguagem muito dinâmica - Common Lisp. Talvez seja ainda mais dinâmico que o Python. Sua peculiaridade reside no fato de que o próprio processo de desenvolvimento é organizado de maneira um pouco diferente. É mais interativo e iterativo, porque no REPL on Lisp você pode fazer tudo: criar novos e excluir módulos antigos, adicionar métodos, classes e excluí-los, redefinir classes etc.



No Python, isso é ainda mais difícil. Possui IPython. Obviamente, o IPython aprimora o REPL de alguma forma, adiciona o preenchimento automático e permite o uso de extensões diferentes. Mas para o desenvolvimento iterativo, ele não se encaixa muito bem. Nele você pode baixar o código, testá-lo um pouco e é isso. E, às vezes, ele quer mais interatividade para que você possa realmente usar esse REPL no desenvolvimento, alternar entre módulos, alterar funções e classes dentro deles.

Isso acontece comigo - você executa, por exemplo, o IPython REPL no ambiente de produção e começa a executar alguns comandos lá, investiga alguma coisa e, em seguida, verifica-se que há um erro no módulo e você deseja corrigi-lo rapidamente. Mas isso não funciona, porque você precisa criar uma nova imagem do Docker, colocá-la em produção, entrar neste REPL novamente, alcançar o estado desejado lá novamente, iniciar tudo o que caiu nela novamente. E, idealmente, eu teria que corrigir a função, executá-la imediatamente e obter o resultado instantaneamente.

O que pode ser feito sobre isso? Como recarregar o código no IPython? Tentei usar o carregamento automático e não gostei por vários motivos. Antes de tudo, quando o módulo é reiniciado, ele perde o estado que estava nas variáveis ​​globais dentro deste módulo. E pode haver um valor em cache com os resultados de algumas funções. Ou eu poderia, por exemplo, carregar dados pela rede lá, para que mais tarde eu pudesse trabalhar com eles mais rapidamente. Ou seja, o carregamento automático perde o estado.

Portanto, como um experimento, fiz minha extensão simples para IPython e o nomeei TheREPL.

Eu vim para você com este relatório como uma idéia do que pode ser feito com o REPL em Python. E eu realmente espero que você goste dessa idéia, que você a realize em sua mente e continue a criar coisas que tornarão o Python ainda mais eficiente e conveniente.

O que é o TheREPL? Essa é a extensão que você baixa, após a qual um conceito como namespace aparece no IPython, e você pode pegar e alternar para qualquer módulo Python, ver quais variáveis, funções etc. estão lá. E, mais importante, você pode escrever diretamente def, o nome da função, redefinir a função ou a classe, e ela será alterada em todos os módulos em que foi importada. Mas, ao mesmo tempo, o próprio módulo não é reiniciado, portanto, o estado é salvo. Além disso, o TheREPL permite evitar mais alguns artefatos que estão em carregamento automático e que examinaremos agora.



Portanto, no carregamento automático, a atualização do código ocorre apenas quando o arquivo é salvo. Mas, ao mesmo tempo, é necessário inserir algo no próprio REPL, e somente então o carregamento automático capta essas alterações. Esse é o problema número 1. Ou seja, se você tiver algum tipo de processo em segundo plano em um encadeamento separado (por exemplo, o servidor está em execução), não poderá simplesmente pegar e corrigir o código. O carregamento automático não aplicará essas alterações até que você insira algo no IPython REPL.

No caso da minha extensão, você pressiona o atalho diretamente no editor e a função que está sob o cursor é aplicada imediatamente e começa a funcionar. Ou seja, usando TheREPL, você pode alterar o código de forma mais granular. Você também pode escrever def no IPython.



Alternar entre módulos, como eu disse, o carregamento automático não oferece suporte de forma alguma. Você só pode encontrar o arquivo no sistema de arquivos, alterá-lo e esperar que o carregamento automático resolva tudo lá.



Mais. O carregamento automático perde variáveis ​​globais, o TheREPL salva e permite que você continue pesquisando a operação do seu aplicativo, altere seu código interno e, assim, desenvolva-o rapidamente.



O carregamento automático ainda possui esse recurso. Ele astuciosamente aplica alterações no módulo que é recarregado. Em particular, ele faz um truque muito interessante lá. Se a função neste módulo foi atualizada, para alterá-la onde quer que fosse importada, ele usa o coletor de lixo para encontrá-la e todas essas instâncias de funções e alterar o código dentro delas. Além disso, veremos exemplos de como isso acontece. Devido a isso, o código da função é alterado, mesmo que ele entre no fechamento.

Você sabe o que é um fechamento? Isso é uma coisa muito útil. Os desenvolvedores de JavaScript usam isso o tempo todo. Você, provavelmente, também simplesmente nunca prestou atenção. Mas como o carregamento automático faz o que descrevi acima, você pode se encontrar em uma situação em que o código antigo usa um novo código que pode funcionar de maneira diferente. Por exemplo, uma função pode retornar não um valor, mas dois, tupla, em vez de string, etc. O código antigo será quebrado com isso.

O REPL não faz uma coisa tão complicada especificamente para garantir que tudo seja mais consistente. Ou seja, ele altera a função ou classe no módulo em que está definido. Localiza essa classe em todos os outros módulos e a altera também. Depois disso, tudo funciona de uma nova maneira.



Como substituir a função que o carregamento automático faz? Temos duas funções, uma e duas. Cada função possui um conjunto de atributos: documentação, código, argumentos, etc. Aqui no slide está um exemplo de substituição dos atributos nos quais o bytecode está armazenado.

Depois que o carregamento automático é alterado, a função chamada começa a funcionar de maneira diferente. Mas este é um exemplo sintético que acabei de reproduzir com as mãos para que você entenda o que está acontecendo. A função é chamada de uma maneira, mas o código é realmente diferente. E se você desmontar, também mostra que retorna um empate. O que isso leva a?



Aqui está um exemplo de encerramento. Na segunda linha, criamos um fechamento no qual capturamos a função foo. O fechamento em si espera que essa função que passamos retorne uma linha, ela a codifique em utf-8 e tudo funcione.



Mas suponha que você altere o módulo no qual foo está definido e o carregamento automático capta a alteração. E você altera para que não retorne uma string, mas um número. Então o fechamento já funcionará incorretamente, porque a função foi alterada por dentro, mas o fechamento não espera isso, não foi alterado. E esses problemas com o carregamento automático podem "disparar" em locais inesperados.



Como o carregamento automático atualiza as classes? Muito simples. Ele atualiza todos os métodos da classe da mesma maneira que as funções e também atualiza o atributo __class__ para todas as instâncias, para que a resolução dos métodos (determinando qual método deve ser chamado) comece a funcionar de uma nova maneira.

Tudo é um pouco mais complicado no TheREPL, porque quando você atualiza a _classe_, pode haver alguns descendentes, classes filho, que também precisam ser atualizados, porque algo mudou na lista de classes base.

Para resolver esse problema, você pode reconstruir a classe. Mas vamos primeiro ver o que acontece com o carregamento automático quando ele recarrega um módulo.



Aqui está um bom exemplo. Existem dois módulos - a e b. No módulo a, uma classe pai é definida, no módulo b uma classe filho, e criamos uma instância da classe filho. E a linha 10 mostra que sim, esta é uma instância da classe Foo, o pai.



Em seguida, apenas pegamos e alteramos o módulo a. Por exemplo, adicione documentação à classe Foo. O carregamento automático pega essas alterações. O que você acha que, nesse caso, ele retornará de Bar?



E retorna false, porque o carregamento automático alterou a classe Foo e agora é uma classe completamente diferente, não a da qual a classe Bar é herdada.



E uma surpresa! Nos dois módulos aeb, a classe Foo é uma classe diferente e Bar herda de um deles. Devido a esses batentes, é muito difícil prever como o seu código funcionará após o carregamento automático corrigir algo nele.



Algo assim, atualiza as classes. Vou comentar sobre a imagem. Inicialmente, a classe Foo é importada para o módulo b e, portanto, permanece lá. Ao substituir o carregamento automático, este módulo a é realocado e uma nova classe aparece lá, e no módulo b não é atualizado.



O REPL faz um pouco diferente. Ele injeta uma classe modificada em cada módulo em que foi importado. Portanto, tudo funciona corretamente lá. Além disso, se houver objetos na classe, eles serão preservados.



E é assim que TheREPL resolve o problema com as classes filho. Ou seja, quando a classe pai é alterada, ela define a lista de classes base através do atributo mágico mro (ordem de resolução do método). Este atributo contém uma lista de classes na ordem em que você deseja procurar métodos ou atributos nelas. E toda vez que você chama o método get_name no seu objeto, por exemplo, o Python primeiro o verifica na classe Bar, depois na classe Foo e depois na classe de objeto, se não o encontrar. Ele atua de acordo com o procedimento de ordem de resolução do método.

O REPL usa esse chip. É preciso uma lista de classes base, muda a classe que você acabou de mudar para uma nova. Cria um novo tipo filho, este é o segundo passo. Com a função type, você pode realmente criar classes. Se você nunca usou, experimente, é divertido.

Você acabou de dizer o nome da classe, dizer qual é a sua classe base. No caso mais simples, por exemplo, objeto. E - um dicionário com métodos e atributos de classe. Tudo, você tem uma nova classe que pode instanciar, como de costume. O REPL tira proveito deste chip. Ele gera uma classe filha e altera os ponteiros para ela em todos os objetos da classe Bar antiga.

Ainda tenho uma demonstração, vamos dar uma olhada em como funciona. Primeiro, vamos ver uma coisa tão simples.

Primeira demo

Eu disse que você pode alterar o código dentro do módulo. Suponha que tenhamos um servidor. Eu vou executá-lo agora. Em algum momento, descobrimos que, por algum motivo, ele cria diretórios temporários. Ou ele começou a criar, mas antes disso ele não criou. Em seguida, podemos nos conectar a este servidor e, supondo que provavelmente crie esses diretórios usando a função mkdtemp do módulo de arquivo, você pode ir diretamente para esse módulo Python.

Veja - no canto, o nome do módulo atual foi alterado. Agora diz tempfile. E eu posso ver quais recursos existem. Nós os vemos e, principalmente, podemos redefini-los. Eu preparei um invólucro especial que permite decorar qualquer função para que, com todas as chamadas, você possa ver o rastreio de onde é chamado. Agora vamos importá-los e aplicá-los.

Ou seja, envolvo a função Python padrão, sem ter acesso ao código-fonte para este módulo. Eu posso pegar e embrulhá-lo. E na próxima saída, veremos o Traceback e descobriremos de onde é chamado.

Da mesma forma, essas alterações podem ser revertidas para que não nos enviem spam. Ou seja, vemos que esse servidor dentro do worker na oitava linha chama mkdtemp e continua produzindo diretórios temporários para nós, sobrecarregando o sistema de arquivos. Esta é uma aplicação.

Vejamos outro exemplo de por que o carregamento automático às vezes não funciona muito bem. Eu tenho um bot de telegrama preparado:

Segunda demo

Agora, ativamos o carregamento automático e vemos como isso nos ajuda. É isso aí, agora você pode iniciar o bot e conversar com ele. Para que você possa ver melhor, iniciaremos um diálogo com ele. Conheça o bot. Assim. Há algum tipo de erro. Um erro completamente diferente foi concebido e eu decidi fazer alterações no último momento. Mas isso não importa. Agora vamos corrigi-lo, o carregamento automático nos ajudará com isso.

Estamos mudando para o bot. E agora vou comentar temporariamente sobre isso, se sim. Eu salvo o arquivo. A carga automática, em teoria, teve que captar essas mudanças. Inicie o bot novamente. O bot me reconheceu. Vamos conversar com ele.

Outro erro. Ela já concebeu. Vamos consertar. Deixarei o bot, ele funcionará em segundo plano, mudarei para o editor e, no editor, encontraremos esse erro. É apenas um erro de digitação e esqueci que minha variável se chama user_name. Eu salvei o arquivo. autoreload deveria pegá-la, e agora vamos vê-lo.

Mas o carregamento automático, como já mencionei, não sabe nada sobre o fato de o arquivo ter sido alterado até você inserir algo nele. Com um processo tão longo ... Ele precisa ser interrompido, reiniciado. Feito. Volte para o nosso bot, escreva para ele. Bem, você vê, o bot esqueceu que meu nome é Sasha. Por quê? O autoreload o recriou novamente porque recarrega completamente o módulo. E eu preciso escrever para o bot novamente, para restaurar seu estado.

E se você estiver depurando algum tipo de erro que ocorre em um determinado estado, o estado não pode ser perdido, porque, caso contrário, você passará muito tempo novamente para atingir esse estado. O REPL ajuda nesses casos.

Vamos ver como o bot será atualizado no caso de usar o TheREPL. Para a pureza do experimento, vou reiniciar o IPython e repetiremos tudo de novo.

E agora eu baixo TheREPL. Ele imediatamente começa a ouvir em uma porta específica para que você possa enviar um código dentro dela. A propósito, isso pode ser feito mesmo que o IPython esteja sendo executado em algum lugar no servidor e o editor esteja sendo executado localmente, o que também pode ajudá-lo em alguns casos.

Nós importamos o bot, iniciamos, escrevemos novamente. Está claro aqui: reiniciamos o Python, para que não se lembre de quem eu sou. Verifique se há um erro dentro. Sim, há um erro. Bem, vamos fazê-lo.

Volto ao editor, corrijo o erro. Nem precisamos salvar o arquivo, pressione Ctrl-C, Ctrl-C, este é um atalho pelo qual o Emacs pega a descrição atual da função que está logo abaixo do cursor e a envia para o processo Python ao qual está conectado. Isso é tudo, agora podemos verificar como nosso bot responde às minhas mensagens lá. Agora, ele lembra que eu sou Sasha e responde honestamente que ele não sabe como.

Vamos tentar adicionar diretamente novas funcionalidades lá. Para fazer isso, volte ao editor. Por exemplo, adicione o comando help. Por enquanto, deixe-o responder que não sabe nada sobre ajuda. Mais uma vez, pressione Ctrl-C, Ctrl-C, o código é aplicado. Nós vamos ao bot. Veja se ele entende esse comando. Sim, a equipe se inscreveu.

A propósito, ele ainda tem uma coisa dessas, agora vamos ver como a classe vai mudar. Ele tem um comando de estado, um comando de depuração especial para visualizar o estado do bot. Então, alguns Oleg conectados. Interessante.

Quando o bot executa este comando, ele chama reply para visualizar a representação do bot. Podemos corrigir, por exemplo, esta resposta com outra coisa. Por exemplo, faça com que apenas os nomes sejam inseridos. Você pode fazer isso. Voltamos ao nosso messenger, novamente executamos o estado. E isso é tudo. Agora, a resposta funciona de uma nova maneira, mas o objeto é o mesmo, preservou seu estado, pois lembra de todos nós - Oleg, Sasha, kek e "Usuários da tabela da DROP, Alex"!

Assim, você pode escrever e depurar código diretamente, sem mudar para esse ciclo, quando precisar coletar um pacote, role-o em algum lugar. Você pode testar rapidamente algo, alterar tudo o que precisa e só então todas essas alterações devem ser empacotadas adequadamente e implantadas.

Naturalmente, você não deve fazer isso na produção real, porque com essa abordagem que tipo de problema pode ser. Você pode esquecer que o código que você acabou de iniciar no servidor precisa ser salvo e, em seguida, implantado como deveria. Essa abordagem requer disciplina. Mas, no processo de desenvolvimento e depuração de algum tipo de teste, isso é ótimo.

Certifique-se de criar um plugin para o PyCharm. Se houver um voluntário que me ajudará com o Kotlin e o plugin PyCharm, terei prazer em conversar. Escreva-me pelo correio ou telegrama .

* * *

Conectarpara o desenvolvimento do TheREPL. Existem muitos outros chips em que você pode pensar. Por exemplo, você pode encontrar uma maneira de atualizar instâncias de classe quando elas atualizam, adicionam novos atributos ou atualizam seu estado de alguma forma. Da mesma forma, atualizaremos o banco de dados. Agora isso não é.

Você pode criar um código de recarga a quente para produção, para que, quando novas alterações chegarem, não seja necessário reiniciar o servidor. Você pode criar muito mais. Isso é apenas uma ideia, e eu quero que você tire isso daqui. Devemos ajustar tudo por nós mesmos e torná-lo conveniente. Isso é tudo para mim.

All Articles