Dentro da máquina virtual Python. Parte 1



Olá a todos. Finalmente decidi descobrir como o interpretador Python funciona. Para fazer isso, ele começou a estudar um livro de artigos e concebeu ao mesmo tempo para traduzi-lo para o russo. O fato é que as traduções não permitem que você perca uma frase incompreensível e a qualidade da assimilação do material aumenta).Peço desculpas antecipadamente por possíveis imprecisões. Eu sempre tento traduzir o mais corretamente possível, mas um dos principais problemas: simplesmente não há menção de alguns termos no equivalente russo.

Nota de tradução
Python , «code object», ( ) . , .

— Python, - , : , , ( ! ) , , - ( ) .. — () - str (, Python3, bytes).

Introdução


A linguagem de programação Python já existe há algum tempo. O desenvolvimento da primeira versão foi iniciado por Guido Van Rossum em 1989 e, desde então, a linguagem cresceu e se tornou uma das mais populares. O Python é usado em várias aplicações: de interfaces gráficas a aplicativos de análise de dados.

O objetivo deste artigo é conhecer os bastidores do intérprete e fornecer uma visão geral conceitual de como um programa escrito em Python é executado. O CPython será considerado no artigo, porque, no momento da redação deste documento, é a implementação Python mais popular e básica.

Python e CPython são usados ​​como sinônimos neste texto, mas qualquer menção a Python significa CPython (a versão python implementada em C). Outras implementações incluem PyPy (python implementado em um subconjunto limitado de Python), Jython (implementação na Java Virtual Machine), etc.

Gosto de dividir a execução de um programa Python em duas ou três etapas principais (listadas abaixo), dependendo de como o intérprete é chamado. Essas etapas serão abordadas em vários graus neste artigo:

  1. Inicialização - esta etapa envolve a configuração das várias estruturas de dados exigidas pelo processo python. Provavelmente isso acontecerá quando o programa for executado no modo não interativo através do shell do intérprete.
  2. — , : , , .
  3. — .

O mecanismo para gerar árvores de "análise", bem como árvores de sintaxe abstrata (ASD), é independente da linguagem. Portanto, não abordaremos muito esse tópico, porque os métodos usados ​​no Python são semelhantes aos métodos de outras linguagens de programação. Por outro lado, o processo de construção de tabelas de símbolos e objetos de código do ADS no Python é mais específico e, portanto, merece atenção especial. Ele também discute a interpretação de objetos de código compilados e todas as outras estruturas de dados. Os tópicos abordados por nós incluem, entre outros: o processo de construção de tabelas de símbolos e criação de objetos de código, objetos Python, objetos de quadro, objetos de código, objetos funcionais, opcodes, loops de interpretador, geradores e classes de usuário.

Este material é destinado a qualquer pessoa interessada em aprender como a máquina virtual CPython funciona. Supõe-se que o usuário já esteja familiarizado com python e entenda o básico da linguagem. Ao estudar a estrutura de uma máquina virtual, encontraremos uma quantidade significativa de código C, para que seja mais fácil para um usuário que tenha um conhecimento básico da linguagem C entender o material. E então, basicamente, o que você precisa para se familiarizar com esse material: o desejo de aprender mais sobre a máquina virtual CPython.

Este artigo é uma versão ampliada de anotações pessoais feitas no estudo do trabalho interno do intérprete. Há muita coisa de qualidade nos vídeos do PyCon , nas palestras da escola e neste blog.. Meu trabalho não teria sido concluído sem essas fontes fantásticas de conhecimento.

No final deste livro, o leitor será capaz de entender os meandros de como o interpretador Python executa seu programa. Isso inclui os vários estágios da execução do programa e estruturas de dados que são críticas no programa. Para começar, teremos uma visão geral do que acontece quando um programa trivial é executado, quando o nome do módulo é passado para o intérprete na linha de comando. O código executável do CPython pode ser instalado a partir da fonte, seguindo o Python Developer's Guide .

Este livro usa a versão Python 3.

Vista de 30.000 pés


Este capítulo fala sobre como o intérprete executa um programa Python. Nos capítulos seguintes, examinaremos as várias partes desse “quebra-cabeça” e forneceremos uma descrição mais detalhada de cada parte. Independentemente da complexidade de um programa escrito em Python, esse processo é sempre o mesmo. A excelente explicação dada por Yaniv Aknin em sua série de artigos sobre o Python Internal define o tópico para nossa discussão.

O módulo de origem test.py pode ser executado na linha de comando (ao transmiti-lo como argumento para o programa interpretador Python na forma de $ python test.py). Esta é apenas uma maneira de chamar o executável Python. Também podemos iniciar um intérprete interativo, executar linhas de um arquivo como código, etc. Mas este e outros métodos não nos interessam. É a transferência do módulo como argumento (dentro da linha de comando) para o arquivo executável (Figura 2.1) que reflete melhor o fluxo de várias ações envolvidas na execução real do código.


Figura 2.1: Fluxo em tempo de execução.

O executável python é um programa C regular; portanto, quando é chamado, ocorrem processos semelhantes aos que existem, por exemplo, no kernel do linux ou no programa hello world simples. Reserve um minuto do seu tempo para entender: o executável python é apenas outro programa que lança o seu. Tais "relacionamentos" existem entre a linguagem C e o assembler (ou llvm). O processo de inicialização padrão (que depende da plataforma em que a execução ocorre) inicia quando o executável python é chamado com o nome do módulo como argumento.

Este artigo pressupõe que você esteja usando um sistema operacional baseado em Unix; portanto, alguns recursos podem variar no Windows.

A linguagem C na inicialização executa toda a sua “mágica” de inicialização - carrega bibliotecas, verifica / define variáveis ​​de ambiente e, depois disso, o principal método do executável python é iniciado como qualquer outro programa C. O arquivo executável principal do Python está localizado em ./Programs/python.c e executa algumas inicializações (como fazer cópias dos argumentos da linha de comando do programa que foram passados ​​para o módulo). A função principal chama a função Py_Main localizada em ./Modules/main.c . Ele processa o processo de inicialização do intérprete: analisa os argumentos da linha de comando, define sinalizadores, lê variáveis ​​de ambiente, executa ganchos, randomiza funções de hash, etc. Também chamadoPy_Initialize do pylifecycle.c , que lida com a inicialização das estruturas de dados do interpretador e do estado do fluxo, são duas estruturas de dados muito importantes.

Examinar declarações de estruturas de dados de intérpretes e estados de fluxo deixa claro por que eles são necessários. O estado do intérprete e o fluxo são apenas estruturas com ponteiros para os campos que contêm as informações necessárias para executar o programa. Os dados do estado do intérprete são criados via typedef (pense nessa palavra-chave em C como uma definição de tipo, embora isso não seja totalmente verdade). O código para essa estrutura é mostrado na Listagem 2.1.

 1     typedef struct _is {
 2 
 3         struct _is *next;
 4         struct _ts *tstate_head;
 5 
 6         PyObject *modules;
 7         PyObject *modules_by_index;
 8         PyObject *sysdict;
 9         PyObject *builtins;
10         PyObject *importlib;
11 
12         PyObject *codec_search_path;
13         PyObject *codec_search_cache;
14         PyObject *codec_error_registry;
15         int codecs_initialized;
16         int fscodec_initialized;
17 
18         PyObject *builtins_copy;
19     } PyInterpreterState;

Listagem de código 2.1: Estrutura de dados do estado do intérprete

Qualquer pessoa que tenha usado a linguagem de programação Python por um longo tempo pode reconhecer vários campos mencionados nessa estrutura (sysdict, builtins, codec).

  1. O campo * next é uma referência a outra instância do intérprete, pois vários intérpretes Python podem existir no mesmo processo.
  2. O campo * tstate_head indica o encadeamento principal de execução (se o programa é multiencadeado, o interpretador é comum a todos os encadeamentos criados pelo programa). Discutiremos isso em mais detalhes em breve.
  3. modules, modules_by_index, sysdict, builtins e importlib falam por si. Todos eles são definidos como instâncias do PyObject , que é o tipo raiz de todos os objetos na máquina virtual Python. Os objetos Python serão discutidos em mais detalhes nos próximos capítulos.
  4. Os campos relacionados ao codec * contêm informações que ajudam no download de codificações. Isso é muito importante para decodificar bytes.

A execução do programa deve ocorrer em um encadeamento. A estrutura de estado do fluxo contém todas as informações necessárias para executar algum objeto de código. Parte da estrutura de dados do fluxo é mostrada na Listagem 2.2.

 1     typedef struct _ts {
 2         struct _ts *prev;
 3         struct _ts *next;
 4         PyInterpreterState *interp;
 5 
 6         struct _frame *frame;
 7         int recursion_depth;
 8         char overflowed; 
 9                         
10         char recursion_critical; 
11         int tracing;
12         int use_tracing;
13 
14         Py_tracefunc c_profilefunc;
15         Py_tracefunc c_tracefunc;
16         PyObject *c_profileobj;
17         PyObject *c_traceobj;
18 
19         PyObject *curexc_type;
20         PyObject *curexc_value;
21         PyObject *curexc_traceback;
22 
23         PyObject *exc_type;
24         PyObject *exc_value;
25         PyObject *exc_traceback;
26 
27         PyObject *dict;  /* Stores per-thread state */
28         int gilstate_counter;
29 
30         ... 
31     } PyThreadState;

Listagem 2.2: Parte da

estrutura de dados do estado do fluxo As estruturas de dados do interpretador e os estados do fluxo são discutidos em mais detalhes nos próximos capítulos. O processo de inicialização também configura mecanismos de importação e também o stdio elementar.

Após concluir toda a inicialização, Py_Main chama a função run_file (também localizada no módulo main.c). A seguir, há uma série de chamadas de função: PyRun_AnyFileExFlags -> PyRun_SimpleFileExFlags -> PyRun_FileExFlags -> PyParser_ASTFromFileObject. PyRun_SimpleFileExFlagscria o espaço para nome __main__ no qual o conteúdo do arquivo será executado. Ele também verifica se a versão pyc do arquivo existe (o arquivo pyc é um arquivo simples que contém uma versão já compilada do código-fonte). Se existir uma versão pyc, será feita uma tentativa de lê-la como um arquivo binário e depois executá-la. Se o arquivo pyc estiver ausente, PyRun_FileExFlags é chamado etc. A função PyParser_ASTFromFileObject chama PyParser_ParseFileObject , que lê o conteúdo do módulo e cria árvores de análise a partir dele. Em seguida, a árvore criada é passada para PyParser_ASTFromNodeObject , que cria uma árvore de sintaxe abstrata a partir dela.

, Py_INCREF Py_DECREF. , . CPython : , , Py_INCREF. , , Py_DECREF.

AST é gerado quando run_mod é chamado . Essa função chama PyAST_CompileObject , que cria objetos de código do AST. Observe que o bytecode gerado durante a chamada PyAST_CompileObject é passado pelo otimizador de olho mágico simples , que executa baixa otimização do bytecode gerado antes de criar objetos de código. A função run_mod em seguida, aplica-se o PyEval_EvalCode função do ceval.c arquivo ao objeto código. Isso leva a outra série de chamadas de função: PyEval_EvalCode -> PyEval_EvalCode -> _PyEval_EvalCodeWithName -> _PyEval_EvalFrameEx. O objeto de código é passado como argumento para a maioria dessas funções de uma forma ou de outra. _PyEval_EvalFrameEx- Este é um loop de intérprete normal que lida com a execução de objetos de código. No entanto, é chamado não apenas com o objeto de código como argumento, mas com o objeto de quadro, que tem como atributo um campo que se refere ao objeto de código. Esse quadro fornece o contexto para a execução do objeto de código. Em palavras simples: o loop do interpretador lê continuamente a próxima instrução indicada pelo contador de instruções da matriz de instruções. Em seguida, ele executa esta instrução: adiciona ou remove objetos da pilha de valores no processo até esvaziar a matriz de instruções a serem executadas (bem, ou algo de excepcional acontece que interrompe o loop).

O Python fornece um conjunto de funções que você pode usar para examinar objetos de código reais. Por exemplo, um programa simples pode ser compilado em um objeto de código e desmontado para obter opcodes executados pela máquina virtual python. Isso é mostrado na Listagem 2.3.

1         >>> def square(x):
2         ...     return x*x
3         ... 
4 
5         >>> dis(square)
6         2           0 LOAD_FAST                0 (x)
7                     2 LOAD_FAST                0 (x)
8                     4 BINARY_MULTIPLY     
9                     6 RETURN_VALUE        

Listagem de código 2.3: Desmontando uma função no Python O

arquivo de cabeçalho ./Include/opcodes.h contém uma lista completa de todas as instruções / códigos de operação da máquina virtual Python. Opcode's são bem simples. Tomemos nosso exemplo na Listagem 2.3, que possui um conjunto de quatro instruções. LOAD_FAST carrega o valor de seu argumento (neste caso x) na pilha de valores. A máquina virtual python é baseada em pilha; portanto, os valores para operações de código de operação são "removidos" da pilha e os resultados do cálculo são enviados de volta à pilha para uso posterior por outros códigos de operação. Em seguida, BINARY_MULTIPLY exibe dois itens da pilha, executa multiplicação binária de ambos os valores e empurra o resultado de volta para a pilha. Instrução RETURN VALUErecupera um valor da pilha, define o valor de retorno do objeto para esse valor e sai do loop do interpretador. Se você observar a Listagem 2.3, fica claro que essa é uma simplificação bastante forte.

A explicação atual do loop do intérprete não leva em consideração vários detalhes, que serão discutidos nos capítulos subseqüentes. Por exemplo, aqui estão as perguntas para as quais não recebemos uma resposta:

  • De onde vieram os valores carregados pela instrução LOAD_FAST?
  • De onde vêm os argumentos, que são usados ​​como parte das instruções?
  • Como são tratadas as chamadas de função e método aninhadas?
  • Como o loop do interpretador lida com exceções?

Após concluir todas as instruções, a função Py_Main continua a execução, mas desta vez inicia o processo de limpeza. Se Py_Initialize for chamado para executar a inicialização durante o início do intérprete, Py_FinalizeEx será chamado para executar a limpeza. Esse processo inclui aguardar a saída dos encadeamentos, chamar quaisquer manipuladores de saída e liberar a memória ainda usada alocada pelo intérprete.

E assim, analisamos a descrição "de alto nível" dos processos que ocorrem no executável do Python quando um script é executado. Como observado anteriormente, há muitas perguntas que ainda precisam ser respondidas. No futuro, aprofundaremos o estudo do intérprete e consideraremos detalhadamente cada uma das etapas. E começaremos descrevendo o processo de compilação no próximo capítulo.

All Articles