Hoje publicamos a segunda parte da tradução do material sobre como o Dropbox organizou o controle de tipo de vários milhões de linhas de código Python.

→
Leia a primeira parteSuporte formal de tipo (PEP 484)
Fizemos o primeiro experimento sério com mypy no Dropbox durante a Hack Week 2014. A Hack Week é um evento realizado pelo Dropbox por uma semana. Neste momento, os funcionários podem trabalhar em qualquer coisa! Alguns dos projetos de tecnologia mais famosos do Dropbox começaram em eventos semelhantes. Como resultado desse experimento, chegamos à conclusão de que o mypy parece promissor, embora esse projeto ainda não estivesse pronto para uso generalizado.
Naquela época, a idéia de padronizar os sistemas de dicas para os tipos Python estava no ar. Como eu disse, começando no Python 3.0, você pode usar anotações de tipo para funções, mas essas são apenas expressões arbitrárias, sem nenhuma sintaxe e semântica específicas. Durante a execução do programa, essas anotações, na maioria das vezes, foram simplesmente ignoradas. Após a Hack Week, começamos a trabalhar na padronização da semântica. Este trabalho levou ao surgimento da
PEP 484 (Guido van Rossum, Lukas Langa e eu colaboramos neste documento).
Nossos motivos podem ser vistos de dois lados. Primeiro, esperávamos que todo o ecossistema Python pudesse adotar uma abordagem geral do uso de dicas de tipo (dicas de tipo é um termo usado no Python como um análogo de "anotações de tipo"). Isso, dados os possíveis riscos, seria melhor do que usar muitas abordagens mutuamente incompatíveis. Em segundo lugar, queríamos discutir abertamente os mecanismos de anotação de tipo com muitos membros da comunidade Python. Em parte, esse desejo foi ditado pelo fato de que não gostaríamos de parecer "apóstatas" das idéias básicas da linguagem aos olhos das grandes massas de programadores de Python. É uma linguagem de tipo dinâmico, conhecida como "digitação de pato". Na comunidade, no início, uma atitude um tanto desconfiada em relação à idéia de digitação estática não pôde deixar de surgir. Mas essa atitude acabou enfraquecendo - depois que ficou claro que a digitação estática não era planejada para ser obrigatória (e depois que as pessoas perceberam que era realmente útil).
A sintaxe resultante para as dicas de tipo era muito semelhante à suportada por mypy na época. O PEP 484 saiu com o Python 3.5 em 2015. Python não era mais uma linguagem que suportava apenas digitação dinâmica. Eu gosto de pensar neste evento como um marco significativo na história do Python.
Início da migração
No final de 2015, uma equipe de três pessoas foi criada no Dropbox para trabalhar no mypy. Incluía Guido van Rossum, Greg Price e David Fisher. A partir desse momento, a situação começou a se desenvolver extremamente rapidamente. O primeiro obstáculo ao crescimento do mypy foi o desempenho. Como já sugeri acima, no período inicial do desenvolvimento do projeto, pensei em traduzir a implementação do mypy em C, mas essa ideia foi excluída das listas até agora. Estamos presos ao fato de termos usado o intérprete CPython para iniciar o sistema, o que não é rápido o suficiente para ferramentas como mypy. (O projeto PyPy, uma implementação alternativa do Python com um compilador JIT, também não nos ajudou.)
Felizmente, aqui algumas melhorias algorítmicas vieram em nosso auxílio. O primeiro poderoso "acelerador" foi a implementação da verificação incremental. A idéia dessa melhoria foi simples: se todas as dependências do módulo não foram alteradas desde o lançamento anterior do mypy, podemos usar os dados armazenados em cache durante a sessão anterior enquanto trabalhamos com dependências. Tudo o que precisávamos fazer era digitar os arquivos modificados e os arquivos que dependiam deles. Mypy foi ainda mais longe: se a interface externa do módulo não fosse alterada - mypy pensava que outros módulos que importam esse módulo não deveriam ser verificados novamente.
A validação incremental nos ajudou muito na anotação de grandes volumes de código existente. O fato é que esse processo geralmente envolve muitas execuções iterativas do mypy, pois as anotações são gradualmente adicionadas ao código e gradualmente aprimoradas. O primeiro lançamento do mypy ainda era muito lento, pois quando você o executava, era necessário verificar muitas dependências. Então, para melhorar a situação, implementamos um mecanismo de cache remoto. Se o mypy detectar que o cache local provavelmente está desatualizado, ele fará o download do instantâneo do cache atual para toda a base de código de um repositório centralizado. Ele então executa uma verificação incremental usando este instantâneo. Este é outro grande passo que nos levou a aumentar a produtividade do mypy.
Este foi um período de introdução rápida e natural do sistema de verificação de tipos do Dropbox. Até o final de 2016, já tínhamos aproximadamente 420.000 linhas de código Python com anotações de tipo. Muitos usuários estavam entusiasmados com a verificação de tipo. O Dropbox usou o mypy com mais e mais equipes de desenvolvimento.
Tudo parecia bom então, mas ainda tínhamos muito o que fazer. Começamos a realizar pesquisas internas periódicas com usuários, a fim de identificar as áreas problemáticas do projeto e entender quais problemas precisam ser abordados primeiro (essa prática é usada hoje na empresa). O mais importante, como ficou claro, foram duas tarefas. O primeiro - você precisava de mais cobertura de código com os tipos, o segundo - era necessário para o mypy trabalhar mais rápido. Estava perfeitamente claro que nosso trabalho de acelerar o mypy e sua implementação nos projetos da empresa ainda estava longe de terminar. Cientes da importância dessas duas tarefas, resolvemos a solução.
Mais desempenho!
As verificações incrementais aceleraram o mypy, mas essa ferramenta ainda não era rápida o suficiente. Muitas verificações incrementais duraram cerca de um minuto. O motivo disso foram as importações cíclicas. Provavelmente isso não surpreenderá quem trabalhou com grandes bases de código escritas em Python. Tínhamos conjuntos de centenas de módulos, cada um deles importando indiretamente todos os outros. Se algum arquivo no ciclo de importação acabou por ser modificado, o mypy precisou processar todos os arquivos incluídos nesse ciclo e, com frequência, também os módulos que importam módulos desse ciclo. Um desses ciclos foi o infame "emaranhado de dependências", que causou muitos problemas no Dropbox. Uma vez que essa estrutura continha várias centenas de módulos, enquanto foi importada, direta ou indiretamente, muitos testes, ela também foi usada no código de produção.
Consideramos a possibilidade de "desvendar" dependências cíclicas, mas não tínhamos os recursos para fazer isso. Havia muito código com o qual não estávamos familiarizados. Como resultado, adotamos uma abordagem alternativa. Decidimos fazer o mypy funcionar rápido, mesmo que houvesse "bolas de dependência". Conseguimos isso com o daemon mypy. Um daemon é um processo do servidor que implementa dois recursos interessantes. Em primeiro lugar, ele mantém na memória informações sobre toda a base de códigos. Isso significa que toda vez que você executa o mypy, não precisa fazer o download de dados em cache relacionados a milhares de dependências importadas. Em segundo lugar, ele cuidadosamente, no nível de pequenas unidades estruturais, analisa as relações entre funções e outras entidades. Por exemplo, se a função
foo
chama a
bar
funções, existe uma dependência de
foo
na
bar
. Quando um arquivo é alterado, o daemon primeiro, isoladamente, processa apenas o arquivo alterado. Em seguida, ele analisa as alterações nesse arquivo que são visíveis de fora, como as assinaturas de funções alteradas. O daemon usa informações detalhadas de importação apenas para verificar novamente as funções que realmente usam a função alterada. Normalmente, com essa abordagem, poucas funções precisam ser verificadas.
A implementação de tudo isso não foi fácil, uma vez que a implementação original do mypy estava muito focada no processamento de um arquivo por vez. Tivemos que lidar com muitas situações limítrofes, cuja ocorrência exigiu verificações repetidas nesses casos, quando algo mudou no código. Por exemplo, isso acontece quando uma nova classe base é atribuída a uma classe. Depois que fizemos o que queríamos, conseguimos reduzir o tempo de execução da maioria das verificações incrementais para alguns segundos. Pareceu-nos uma grande vitória.
Mais desempenho!
Juntamente com o cache remoto, que descrevi acima, o daemon mypy resolveu quase completamente os problemas que surgem quando o programador geralmente executa a verificação de tipo, fazendo alterações em um pequeno número de arquivos. No entanto, o desempenho do sistema na variante menos favorável de seu uso ainda estava longe de ser o ideal. Um início limpo de mypy pode levar mais de 15 minutos. E foi muito mais do que gostaríamos. A cada semana, a situação piorava, pois os programadores continuavam escrevendo novos códigos e adicionando anotações ao código existente. Nossos usuários ainda ansiavam por mais desempenho, mas ficamos felizes em estar prontos para encontrá-los.
Decidimos voltar a uma das minhas primeiras idéias sobre o mypy. Ou seja, a conversão do código Python em código C. As experiências com o Cython (este é um sistema que permite traduzir o código Python em código C) não nos deram nenhuma aceleração visível, por isso decidimos reviver a ideia de escrever nosso próprio compilador. Como a base de código mypy (escrita em Python) já continha todas as anotações de tipo necessárias, uma tentativa de usá-las para acelerar o sistema parecia valer a pena. Eu rapidamente criei um protótipo para testar essa ideia. Ele mostrou em vários micro benchmarks um aumento de mais de 10 vezes na produtividade. Nossa idéia era compilar módulos Python em módulos C usando Cython e transformar anotações de tipo em verificações de tipo executadas no tempo de execução (normalmente as anotações de tipo são ignoradas no tempo de execução e são usadas apenas pelos sistemas de verificação de tipo ) Na verdade, planejamos traduzir a implementação mypy do Python para uma linguagem criada estaticamente, com aparência (e, na maior parte, trabalho) exatamente como o Python. (Esse tipo de migração entre idiomas se tornou uma tradição do projeto mypy. A implementação inicial do mypy foi escrita em Alore, e havia um híbrido sintático de Java e Python).
O foco na API de extensão CPython foi a chave para não perder os recursos de gerenciamento de projetos. Não precisamos implementar uma máquina virtual ou nenhuma biblioteca que mypy precisava. Além disso, todo o ecossistema Python ainda estaria disponível para nós, todas as ferramentas (como pytest) estariam disponíveis. Isso significava que poderíamos continuar usando o código Python interpretado durante o desenvolvimento, o que nos permitiria continuar trabalhando usando um esquema muito rápido para fazer alterações no código e testá-lo, em vez de esperar a compilação do código. Parecia que éramos soberbamente capazes, por assim dizer, de sentar em duas cadeiras, e gostávamos disso.
O compilador, que chamamos de mypyc (como usa mypy como frontend para análise de tipos), acabou sendo um projeto muito bem-sucedido. Em suma, alcançamos uma aceleração cerca de 4x mais rápida das execuções frequentes do mypy sem armazenar em cache. O desenvolvimento do núcleo do projeto mypyc levou cerca de 4 meses de calendário de uma pequena equipe que incluía Michael Sullivan, Ivan Levkivsky, Hugh Han e eu. Essa quantidade de trabalho era muito menos ambiciosa do que seria necessário para reescrever mypy, por exemplo, em C ++ ou Go. E tivemos que fazer muito menos alterações no projeto do que precisaríamos ao reescrevê-lo em outro idioma. Também esperávamos que o mypyc chegasse a tal nível que outros programadores do Dropbox pudessem usá-lo para compilar e acelerar o código.
Para atingir esse nível de desempenho, tivemos que aplicar algumas soluções interessantes de engenharia. Portanto, o compilador pode acelerar muitas operações usando as construções rápidas de baixo nível C. Por exemplo, uma chamada para uma função compilada se traduz em uma chamada para uma função C. E essa chamada é feita muito mais rápido do que chamar uma função interpretada. Algumas operações, como pesquisas de dicionário, ainda se resumiam ao uso de chamadas regulares de C-API do CPython, que após a compilação se mostraram um pouco mais rápidas. Conseguimos nos livrar da carga extra no sistema criada pela interpretação, mas, neste caso, deu apenas um pequeno ganho em termos de desempenho.
Para identificar as operações "lentas" mais comuns, realizamos a criação de perfil de código. Armado com os dados obtidos, tentamos ajustar o mypyc para gerar código C mais rápido para essas operações ou reescrever o código Python correspondente usando operações mais rápidas (e às vezes simplesmente não tínhamos uma solução suficientemente simples para isso ou outro problema). Reescrever o código Python frequentemente provou ser uma solução mais fácil para o problema do que implementar a mesma transformação automaticamente no compilador. A longo prazo, queríamos automatizar muitas dessas transformações, mas naquele momento pretendemos acelerar o mypy com o mínimo de esforço. E nós, caminhando em direção a esse objetivo, cortamos vários cantos.
Para continuar ...
Caros leitores! Quais foram suas impressões sobre o projeto mypy quando você soube da existência dele?
