Por que existem tantos Pythons? Uma comparação de implementação do Python
Publicados: 2022-03-11Python é incrível.
Surpreendentemente, essa é uma afirmação bastante ambígua. O que quero dizer com 'Python'? Refiro-me ao Python a interface abstrata? Quero dizer CPython, a implementação comum do Python (e não deve ser confundida com o Cython de nome semelhante)? Ou quero dizer algo completamente diferente? Talvez eu esteja me referindo obliquamente a Jython, ou IronPython, ou PyPy. Ou talvez eu realmente tenha ido ao fundo do poço e estou falando sobre RPython ou RubyPython (que são coisas muito, muito diferentes).
Embora as tecnologias mencionadas acima sejam comumente nomeadas e comumente referenciadas, algumas delas servem a propósitos completamente diferentes (ou, pelo menos, operam de maneiras completamente diferentes).
Ao longo do meu tempo trabalhando com as interfaces Python, eu encontrei toneladas dessas ferramentas .*ython. Mas só recentemente eu tirei um tempo para entender o que eles são, como funcionam e por que são necessários (à sua maneira).
Neste tutorial, começarei do zero e percorrerei as várias implementações do Python, concluindo com uma introdução completa ao PyPy, que acredito ser o futuro da linguagem.
Tudo começa com uma compreensão do que 'Python' realmente é.
Se você tiver um bom entendimento de código de máquina, máquinas virtuais e similares, sinta-se à vontade para pular adiante.
“O Python é interpretado ou compilado?”
Este é um ponto comum de confusão para iniciantes em Python.
A primeira coisa a perceber ao fazer uma comparação é que 'Python' é uma interface . Há uma especificação do que o Python deve fazer e como ele deve se comportar (como em qualquer interface). E existem várias implementações (como em qualquer interface).
A segunda coisa a perceber é que 'interpretado' e 'compilado' são propriedades de uma implementação , não de uma interface .
Portanto, a questão em si não está muito bem formulada.
Dito isto, para a implementação Python mais comum (CPython: escrito em C, muitas vezes referido simplesmente como 'Python', e certamente o que você está usando se não tiver ideia do que estou falando), a resposta é: interpretado , com alguma compilação. O CPython compila * código-fonte Python para bytecode e, em seguida, interpreta esse bytecode, executando-o à medida que avança.
* Nota: isto não é 'compilação' no sentido tradicional da palavra. Normalmente, diríamos que 'compilação' é pegar uma linguagem de alto nível e convertê-la em código de máquina. Mas é uma espécie de 'compilação'.
Vamos analisar essa resposta mais de perto, pois ela nos ajudará a entender alguns dos conceitos que aparecem mais adiante no post.
Bytecode vs. Código de Máquina
É muito importante entender a diferença entre bytecode e código de máquina (também conhecido como código nativo), talvez melhor ilustrado pelo exemplo:
- C compila em código de máquina, que é executado diretamente em seu processador. Cada instrução instrui sua CPU a mover as coisas.
- Java compila para bytecode, que é então executado na Java Virtual Machine (JVM), uma abstração de um computador que executa programas. Cada instrução é então tratada pela JVM, que interage com seu computador.
Em termos muito breves: o código de máquina é muito mais rápido, mas o bytecode é mais portátil e seguro .
O código da máquina parece diferente dependendo da sua máquina, mas o bytecode parece o mesmo em todas as máquinas. Pode-se dizer que o código de máquina é otimizado para sua configuração.
Voltando à implementação do CPython, o processo da cadeia de ferramentas é o seguinte:
- CPython compila seu código-fonte Python em bytecode.
- Esse bytecode é então executado na máquina virtual CPython.
VMs alternativas: Jython, IronPython e mais
Como mencionei anteriormente, o Python tem várias implementações. Novamente, como mencionado anteriormente, o mais comum é o CPython, mas há outros que devem ser mencionados para este guia de comparação. Esta é uma implementação Python escrita em C e considerada a implementação 'padrão'.
Mas e as implementações alternativas do Python? Um dos mais proeminentes é o Jython, uma implementação do Python escrita em Java que utiliza a JVM. Enquanto CPython produz bytecode para rodar na CPython VM, Jython produz bytecode Java para rodar na JVM (este é o mesmo que é produzido quando você compila um programa Java).
“Por que você usaria uma implementação alternativa?”, você pode perguntar. Bem, por um lado, essas diferentes implementações do Python funcionam bem com diferentes pilhas de tecnologia .
O CPython torna muito fácil escrever extensões C para seu código Python porque no final ele é executado por um interpretador C. Jython, por outro lado, torna muito fácil trabalhar com outros programas Java: você pode importar qualquer classe Java sem esforço adicional, invocando e utilizando suas classes Java de dentro de seus programas Jython. (Além disso: se você não pensou sobre isso de perto, isso é realmente uma loucura. Estamos no ponto em que você pode misturar e misturar linguagens diferentes e compilá-las todas na mesma substância. (Como mencionado por Rostin, programas que mix Fortran e código C já existem há algum tempo. Então, é claro, isso não é necessariamente novo. Mas ainda é legal.))
Como exemplo, este é um código Jython válido:
[Java HotSpot(TM) 64-Bit Server VM (Apple Inc.)] on java1.6.0_51 >>> from java.util import HashSet >>> s = HashSet(5) >>> s.add("Foo") >>> s.add("Bar") >>> s [Foo, Bar]
IronPython é outra implementação popular do Python, escrita inteiramente em C# e voltada para a pilha .NET. Em particular, ele é executado no que você pode chamar de .NET Virtual Machine, Common Language Runtime (CLR) da Microsoft, comparável à JVM.
Você pode dizer que Jython : Java :: IronPython : C# . Eles são executados nas mesmas VMs respectivas, você pode importar classes C# do seu código IronPython e classes Java do seu código Jython, etc.
É totalmente possível sobreviver sem nunca tocar em uma implementação Python não CPython. Mas há vantagens na troca, a maioria das quais depende de sua pilha de tecnologia. Usando muitas linguagens baseadas em JVM? Jython pode ser para você. Tudo sobre a pilha .NET? Talvez você deva experimentar o IronPython (e talvez você já tenha).
A propósito: embora isso não seja um motivo para usar uma implementação diferente, observe que essas implementações realmente diferem em comportamento além de como tratam seu código-fonte Python. No entanto, essas diferenças geralmente são pequenas e se dissolvem ou surgem com o tempo, à medida que essas implementações estão em desenvolvimento ativo. Por exemplo, IronPython usa strings Unicode por padrão; CPython, no entanto, padroniza para ASCII para versões 2.x (falha com um UnicodeEncodeError para caracteres não-ASCII), mas suporta strings Unicode por padrão para 3.x.
Compilação Just-in-Time: PyPy e o futuro
Portanto, temos uma implementação Python escrita em C, uma em Java e uma em C#. O próximo passo lógico: uma implementação Python escrita em… Python. (O leitor instruído notará que isso é um pouco enganoso.)
Aqui é onde as coisas podem ficar confusas. Primeiro, vamos discutir a compilação just-in-time (JIT).
JIT: O porquê e como
Lembre-se de que o código de máquina nativo é muito mais rápido que o bytecode. Bem, e se pudéssemos compilar alguns de nossos bytecodes e executá-los como código nativo? Teríamos que pagar algum preço para compilar o bytecode (ou seja, tempo), mas se o resultado final fosse mais rápido, seria ótimo! Essa é a motivação da compilação JIT, uma técnica híbrida que mistura os benefícios de interpretadores e compiladores. Em termos básicos, o JIT deseja utilizar a compilação para acelerar um sistema interpretado.

Por exemplo, uma abordagem comum adotada por JITs:
- Identifique o bytecode que é executado com frequência.
- Compile-o em código de máquina nativo.
- Cache o resultado.
- Sempre que o mesmo bytecode estiver configurado para ser executado, pegue o código de máquina pré-compilado e colha os benefícios (ou seja, aumentos de velocidade).
É disso que se trata a implementação do PyPy: trazer o JIT para o Python (consulte o Apêndice para esforços anteriores). Existem, é claro, outros objetivos: o PyPy pretende ser multiplataforma, leve em memória e suporte sem pilha. Mas o JIT é realmente seu ponto de venda. Como uma média de vários testes de tempo, diz-se que melhora o desempenho por um fator de 6,27. Para um detalhamento, veja este gráfico do PyPy Speed Center:
PyPy é difícil de entender
PyPy tem um enorme potencial e, neste momento, é altamente compatível com CPython (para que possa executar Flask, Django, etc.).
Mas há muita confusão em torno do PyPy (veja, por exemplo, essa proposta absurda de criar um PyPyPy…). Na minha opinião, isso ocorre principalmente porque o PyPy é na verdade duas coisas:
Um interpretador Python escrito em RPython (não Python (eu menti antes)). RPython é um subconjunto do Python com tipagem estática. Em Python, é “quase impossível” raciocinar rigorosamente sobre tipos (Por que é tão difícil? Considere o fato de que:
x = random.choice([1, "foo"])
seria um código Python válido (crédito a Ademan). Qual é o tipo de
x
? Como podemos raciocinar sobre os tipos de variáveis quando os tipos nem são estritamente aplicados?). Com o RPython, você sacrifica alguma flexibilidade, mas torna muito, muito mais fácil raciocinar sobre gerenciamento de memória e outros enfeites, o que permite otimizações.Um compilador que compila o código RPython para vários destinos e adiciona JIT. A plataforma padrão é C, ou seja, um compilador RPython-to-C, mas você também pode direcionar a JVM e outras.
Apenas para maior clareza neste guia de comparação do Python, vou me referir a eles como PyPy (1) e PyPy (2).
Por que você precisa dessas duas coisas, e por que sob o mesmo teto? Pense desta forma: PyPy (1) é um interpretador escrito em RPython. Portanto, ele pega o código Python do usuário e o compila para bytecode. Mas o próprio interpretador (escrito em RPython) deve ser interpretado por outra implementação do Python para ser executado, certo?
Bem, poderíamos usar o CPython para executar o interpretador. Mas isso não seria muito rápido.
Em vez disso, a ideia é que usemos o PyPy (2) (referido como RPython Toolchain) para compilar o interpretador do PyPy para codificar para outra plataforma (por exemplo, C, JVM ou CLI) para executar em nossa máquina, adicionando JIT como Nós vamos. É mágico: PyPy adiciona JIT dinamicamente a um interpretador, gerando seu próprio compilador! ( De novo, isso é loucura: estamos compilando um interpretador, adicionando outro compilador independente e separado. )
No final, o resultado é um executável autônomo que interpreta o código-fonte do Python e explora as otimizações JIT. Que é exatamente o que queríamos! É um bocado, mas talvez este diagrama ajude:
Para reiterar, a verdadeira beleza do PyPy é que podemos escrever vários interpretadores Python diferentes no RPython sem nos preocupar com o JIT. O PyPy então implementaria o JIT para nós usando o RPython Toolchain/PyPy (2).
Na verdade, se ficarmos ainda mais abstratos, você poderia teoricamente escrever um interpretador para qualquer idioma, alimentá-lo para o PyPy e obter um JIT para esse idioma. Isso ocorre porque o PyPy se concentra em otimizar o intérprete real, em vez dos detalhes do idioma que está interpretando.
Como uma breve digressão, gostaria de mencionar que o JIT em si é absolutamente fascinante. Ele usa uma técnica chamada rastreamento, que é executada da seguinte maneira:
- Execute o interpretador e interprete tudo (sem JIT).
- Faça alguns perfis leves do código interpretado.
- Identifique as operações que você executou antes.
- Compile esses bits de código em código de máquina.
Para mais, este artigo é altamente acessível e muito interessante.
Para encerrar: usamos o compilador RPython-to-C do PyPy (ou outra plataforma de destino) para compilar o interpretador implementado pelo RPython do PyPy.
Empacotando
Após uma longa comparação das implementações do Python, tenho que me perguntar: por que isso é tão bom? Por que vale a pena perseguir essa ideia maluca? Acho que Alex Gaynor colocou bem em seu blog: “[PyPy é o futuro] porque [ele] oferece melhor velocidade, mais flexibilidade e é uma plataforma melhor para o crescimento do Python”.
Resumidamente:
- É rápido porque compila o código-fonte para o código nativo (usando JIT).
- É flexível porque adiciona o JIT ao seu intérprete com muito pouco trabalho adicional.
- É flexível (novamente) porque você pode escrever seus intérpretes em RPython , que é mais fácil de estender do que, digamos, C (na verdade, é tão fácil que há um tutorial para escrever seus próprios intérpretes).
Apêndice: Outros nomes Python que você pode ter ouvido
Python 3000 (Py3k): uma nomenclatura alternativa para o Python 3.0, uma versão principal do Python incompatível com versões anteriores que chegou ao palco em 2008. A equipe do Py3k previu que levaria cerca de cinco anos para que essa nova versão fosse totalmente adotada. E enquanto a maioria dos desenvolvedores Python (aviso: alegação anedótica) continua usando Python 2.x, as pessoas estão cada vez mais conscientes do Py3k.
- Cython: um superconjunto de Python que inclui ligações para chamar funções C.
- Objetivo: permitir que você escreva extensões C para seu código Python.
- Também permite adicionar digitação estática ao seu código Python existente, permitindo que ele seja compilado e alcance desempenho semelhante ao C.
- Isso é semelhante ao PyPy, mas não o mesmo. Nesse caso, você está impondo a digitação do código do usuário antes de passá-lo para um compilador. Com o PyPy, você escreve o Python antigo e o compilador lida com todas as otimizações.
Numba: um “compilador especializado just-in-time” que adiciona JIT ao código Python anotado . Em termos mais básicos, você dá algumas dicas e acelera partes do seu código. Numba vem como parte da distribuição Anaconda, um conjunto de pacotes para análise e gerenciamento de dados.
IPython: muito diferente de qualquer outra coisa discutida. Um ambiente de computação para Python. Interativo com suporte para kits de ferramentas GUI e experiência de navegador, etc.
- Psyco: um módulo de extensão do Python e um dos primeiros esforços do Python JIT. No entanto, já foi marcado como "não mantido e morto". Na verdade, o principal desenvolvedor do Psyco, Armin Rigo, agora trabalha no PyPy.
Ligações da linguagem Python
RubyPython: uma ponte entre as VMs Ruby e Python. Permite incorporar código Python em seu código Ruby. Você define onde o Python inicia e para, e o RubyPython organiza os dados entre as VMs.
PyObjc: ligações de linguagem entre Python e Objective-C, atuando como uma ponte entre eles. Praticamente, isso significa que você pode utilizar bibliotecas Objective-C (incluindo tudo o que você precisa para criar aplicativos OS X) de seu código Python e módulos Python de seu código Objective-C. Nesse caso, é conveniente que CPython seja escrito em C, que é um subconjunto de Objective-C.
PyQt: enquanto o PyObjc oferece vinculação para os componentes da GUI do OS X, o PyQt faz o mesmo para o framework da aplicação Qt, permitindo criar interfaces gráficas ricas, acessar bancos de dados SQL, etc. Outra ferramenta que visa trazer a simplicidade do Python para outros frameworks.
Estruturas JavaScript
pyjs (Pyjamas): um framework para criar aplicações web e desktop em Python. Inclui um compilador Python para JavaScript, um conjunto de widgets e mais algumas ferramentas.
Brython: uma VM Python escrita em JavaScript para permitir que o código Py3k seja executado no navegador.