O caminho para a verificação de tipo de 4 milhões de linhas de código Python. Parte 1

Hoje trazemos a sua atenção a primeira parte da tradução do material sobre como o Dropbox lida com o controle de tipo do código Python.

O caminho para a verificação de tipo de 4 milhões de linhas de código Python. Parte 1

O Dropbox escreve muito em Python. É uma linguagem que usamos amplamente, tanto para serviços de back-end quanto para aplicativos de cliente de desktop. Também usamos muito Go, TypeScript e Rust, mas Python é nossa linguagem principal. Considerando nossa escala, e estamos falando de milhões de linhas de código Python, descobriu-se que a digitação dinâmica desse código complicou desnecessariamente seu entendimento e começou a afetar seriamente a produtividade do trabalho. Para atenuar esse problema, começamos a fazer a transição gradual de nosso código para verificação de tipo estático usando mypy. Este é provavelmente o sistema autônomo de verificação de tipo mais popular para Python. Mypy é um projeto de código aberto, seus principais desenvolvedores trabalham no Dropbox.

O Dropbox foi uma das primeiras empresas a implementar a verificação de tipo estático no código Python nessa escala. Mypy é usado em milhares de projetos atualmente. Esta ferramenta inúmeras vezes, como dizem, "testada em batalha". Percorremos um longo caminho para chegar onde estamos agora. Ao longo do caminho, houve muitos empreendimentos malsucedidos e experimentos fracassados. Este post cobre a história da verificação estática de tipos em Python, desde seu início difícil como parte do meu projeto de pesquisa até os dias atuais, quando a verificação de tipos e dicas de tipos se tornaram comuns para inúmeros desenvolvedores que escrevem em Python. Esses mecanismos agora são suportados por muitas ferramentas, como IDEs e analisadores de código.

Leia a segunda parte

Por que a verificação de tipos é necessária?

Se você já usou o Python tipado dinamicamente, pode ter alguma confusão sobre por que tem havido tanto alarido em torno da digitação estática e do mypy ultimamente. Ou talvez você goste do Python precisamente por causa de sua digitação dinâmica, e o que está acontecendo simplesmente o incomoda. A chave para o valor da digitação estática é a escala das soluções: quanto maior o seu projeto, mais você se inclina para a digitação estática e, no final, mais você realmente precisa dela.

Suponha que um determinado projeto tenha atingido o tamanho de dezenas de milhares de linhas e que vários programadores estejam trabalhando nele. Olhando para um projeto semelhante, com base em nossa experiência, podemos dizer que entender seu código será a chave para manter os desenvolvedores produtivos. Sem anotações de tipo, pode ser difícil descobrir, por exemplo, quais argumentos passar para uma função ou quais tipos uma função pode retornar. Aqui estão perguntas típicas que muitas vezes são difíceis de responder sem usar anotações de tipo:

  • Essa função pode retornar None?
  • Qual deve ser esse argumento? items?
  • Qual é o tipo de atributo id: int é isso, str, ou talvez algum tipo personalizado?
  • Esse argumento deve ser uma lista? É possível passar uma tupla para ele?

Se você observar o seguinte trecho de código anotado por tipo e tentar responder a perguntas semelhantes, descobrirá que esta é a tarefa mais simples:

class Resource:
    id: bytes
    ...
    def read_metadata(self, 
                      items: Sequence[str]) -> Dict[str, MetadataItem]:
        ...

  • read_metadata não retorna None, já que o tipo de retorno não é Optional[…].
  • Argumento items é uma sequência de linhas. Não pode ser iterado aleatoriamente.
  • Atributo id é uma cadeia de bytes.

Em um mundo ideal, seria de se esperar que todas essas sutilezas fossem descritas na documentação integrada (docstring). Mas a experiência dá muitos exemplos do fato de que essa documentação geralmente não é observada no código com o qual você deve trabalhar. Mesmo que tal documentação esteja presente no código, não se pode contar com sua exatidão absoluta. Esta documentação pode ser vaga, imprecisa e sujeita a mal-entendidos. Em grandes equipes ou grandes projetos, esse problema pode se tornar extremamente grave.

Enquanto o Python se destaca nos estágios iniciais ou intermediários dos projetos, em algum momento os projetos bem-sucedidos e as empresas que usam o Python podem enfrentar a questão vital: “Devemos reescrever tudo em uma linguagem estaticamente tipada?”.

Sistemas de verificação de tipo como mypy resolvem o problema acima fornecendo ao desenvolvedor uma linguagem formal para descrever tipos e verificando se as declarações de tipo correspondem à implementação do programa (e, opcionalmente, verificando sua existência). Em geral, podemos dizer que esses sistemas colocam à nossa disposição algo como uma documentação cuidadosamente verificada.

O uso de tais sistemas tem outras vantagens e eles já são completamente não triviais:

  • O sistema de verificação de tipos pode detectar alguns erros pequenos (e não tão pequenos). Um exemplo típico é quando eles esquecem de processar um valor None ou alguma outra condição especial.
  • A refatoração de código é bastante simplificada porque o sistema de verificação de tipo geralmente é muito preciso sobre qual código precisa ser alterado. Ao mesmo tempo, não precisamos esperar 100% de cobertura de código com testes, o que, de qualquer forma, geralmente não é viável. Não precisamos nos aprofundar no rastreamento de pilha para descobrir a causa do problema.
  • Mesmo em grandes projetos, o mypy geralmente pode fazer a verificação completa do tipo em uma fração de segundo. E a execução dos testes costuma levar dezenas de segundos ou até minutos. O sistema de verificação de tipos fornece feedback instantâneo ao programador e permite que ele faça seu trabalho mais rapidamente. Ele não precisa mais escrever testes de unidade frágeis e difíceis de manter que substituem entidades reais por simulações e patches apenas para obter resultados de teste de código mais rapidamente.

IDEs e editores como PyCharm ou Visual Studio Code usam o poder das anotações de tipo para fornecer aos desenvolvedores autocompletação de código, realce de erros e suporte para construções de linguagem comumente usadas. E esses são apenas alguns dos benefícios da digitação. Para alguns programadores, tudo isso é o principal argumento a favor da digitação. Isso é algo que se beneficia imediatamente após a implementação. Este caso de uso para tipos não requer um sistema de verificação de tipo separado como mypy, embora deva ser observado que mypy ajuda a manter as anotações de tipo consistentes com o código.

Plano de fundo do mypy

A história do mypy começou no Reino Unido, em Cambridge, alguns anos antes de eu ingressar no Dropbox. Estive envolvido, como parte de minha pesquisa de doutorado, na unificação de linguagens estaticamente tipadas e dinâmicas. Fui inspirado por um artigo sobre digitação incremental de Jeremy Siek e Walid Taha e pelo projeto Typed Racket. Tentei encontrar maneiras de usar a mesma linguagem de programação para vários projetos - de pequenos scripts a bases de código que consistem em muitos milhões de linhas. Ao mesmo tempo, eu queria garantir que em um projeto de qualquer escala, não fosse necessário fazer concessões muito grandes. Uma parte importante de tudo isso foi a ideia de passar gradualmente de um projeto de protótipo não digitado para um produto finalizado com tipagem estática exaustivamente testado. Hoje em dia, essas ideias são amplamente aceitas, mas em 2010 era um problema que ainda estava sendo explorado ativamente.

Meu trabalho original em verificação de tipos não era voltado para Python. Em vez disso, usei uma pequena linguagem "caseira" alore. Aqui está um exemplo que permitirá que você entenda do que estamos falando (as anotações de tipo são opcionais aqui):

def Fib(n as Int) as Int
  if n <= 1
    return n
  else
    return Fib(n - 1) + Fib(n - 2)
  end
end

Usar uma linguagem nativa simplificada é uma abordagem comum usada em pesquisas científicas. Isso é assim, até porque permite realizar experimentos rapidamente, e também porque o que não tem nada a ver com pesquisa pode ser facilmente ignorado. As linguagens de programação do mundo real tendem a ser fenômenos de larga escala com implementações complexas, e isso retarda a experimentação. No entanto, quaisquer resultados baseados em uma linguagem simplificada parecem um pouco suspeitos, pois ao obter esses resultados o pesquisador pode ter sacrificado considerações importantes para o uso prático das línguas.

Meu verificador de tipo para Alore parecia muito promissor, mas eu queria testá-lo experimentando com código real, que, você pode dizer, não foi escrito em Alore. Felizmente para mim, a linguagem Alore foi amplamente baseada nas mesmas ideias do Python. Foi fácil refazer o typechecker para que ele pudesse funcionar com a sintaxe e a semântica do Python. Isso nos permitiu tentar a verificação de tipo no código Python de código aberto. Além disso, escrevi um transpiler para converter código escrito em Alore para código Python e o usei para traduzir meu código typechecker. Agora eu tinha um sistema de verificação de tipo escrito em Python que suportava um subconjunto de Python, algum tipo dessa linguagem! (Certas decisões arquitetônicas que faziam sentido para Alore eram pouco adequadas para Python, e isso ainda é perceptível em partes da base de código mypy.)

Na verdade, a linguagem suportada pelo meu sistema de tipo não poderia ser chamada de Python neste ponto: era uma variante do Python devido a algumas limitações da sintaxe de anotação de tipo do Python 3.

Parecia uma mistura de Java e Python:

int fib(int n):
    if n <= 1:
        return n
    else:
        return fib(n - 1) + fib(n - 2)

Uma das minhas ideias na época era usar anotações de tipo para melhorar o desempenho compilando esse tipo de Python para C, ou talvez bytecode JVM. Cheguei ao estágio de escrever um protótipo de compilador, mas abandonei essa ideia, pois a própria verificação de tipos parecia bastante útil.

Acabei apresentando meu projeto na PyCon 2013 em Santa Clara. Também conversei sobre isso com Guido van Rossum, o benevolente ditador Python vitalício. Ele me convenceu a abandonar minha própria sintaxe e ficar com a sintaxe padrão do Python 3. O Python 3 suporta anotações de função, então meu exemplo pode ser reescrito como mostrado abaixo, resultando em um programa Python normal:

def fib(n: int) -> int:
    if n <= 1:
        return n
    else:
        return fib(n - 1) + fib(n - 2)

Tive que fazer algumas concessões (antes de tudo, quero observar que inventei minha própria sintaxe exatamente por isso). Em particular, o Python 3.3, a versão mais recente da linguagem na época, não suportava anotações variáveis. Discuti com Guido por e-mail várias possibilidades de design sintático de tais anotações. Decidimos usar comentários de tipo para variáveis. Isso serviu ao propósito pretendido, mas foi um pouco complicado (o Python 3.6 nos deu uma sintaxe melhor):

products = []  # type: List[str]  # Eww

Os comentários de tipo também foram úteis para oferecer suporte ao Python 2, que não possui suporte integrado para anotações de tipo:

f fib(n):
    # type: (int) -> int
    if n <= 1:
        return n
    else:
        return fib(n - 1) + fib(n - 2)

Descobriu-se que esses (e outros) trade-offs realmente não importavam - os benefícios da digitação estática significavam que os usuários logo se esqueciam da sintaxe menos do que perfeita. Como nenhuma construção sintática especial foi usada no código Python com verificação de tipo, as ferramentas Python existentes e os processos de processamento de código continuaram a funcionar normalmente, tornando muito mais fácil para os desenvolvedores aprender a nova ferramenta.

Guido também me convenceu a ingressar no Dropbox depois que concluí minha tese de graduação. É aqui que começa a parte mais interessante da história do mypy.

Para ser continuado ...

Caros leitores! Se você usa Python, conte-nos sobre a escala de projetos que você desenvolve nessa linguagem.

O caminho para a verificação de tipo de 4 milhões de linhas de código Python. Parte 1
O caminho para a verificação de tipo de 4 milhões de linhas de código Python. Parte 1

Fonte: habr.com

Adicionar um comentário