Você pode achar isso útil - Internos do Python: adicionando uma nova declaração ao Python , citada aqui:
Este artigo é uma tentativa de entender melhor como funciona o front-end do Python. Apenas ler a documentação e o código-fonte pode ser um pouco chato, por isso estou adotando uma abordagem prática aqui: vou adicionar uma until
declaração ao Python.
Toda a codificação deste artigo foi feita na ramificação de ponta do Py3k no espelho do repositório do Python Mercurial .
A until
declaração
Alguns idiomas, como Ruby, têm uma until
declaração, que é o complemento de while
( until num == 0
é equivalente a while num != 0
). Em Ruby, eu posso escrever:
num = 3
until num == 0 do
puts num
num -= 1
end
E ele imprimirá:
3
2
1
Então, eu quero adicionar um recurso semelhante ao Python. Ou seja, ser capaz de escrever:
num = 3
until num == 0:
print(num)
num -= 1
Uma digressão de defesa da linguagem
Este artigo não tenta sugerir a adição de uma until
declaração ao Python. Embora eu ache que essa declaração tornaria algum código mais claro, e este artigo mostra como é fácil adicionar, eu respeito completamente a filosofia do minimalismo do Python. Tudo o que estou tentando fazer aqui, na verdade, é obter algumas informações sobre o funcionamento interno do Python.
Modificando a gramática
O Python usa um gerador de analisador personalizado chamado pgen
. Este é um analisador LL (1) que converte o código-fonte Python em uma árvore de análise. A entrada para o gerador do analisador é o arquivo Grammar/Grammar
[1] . Este é um arquivo de texto simples que especifica a gramática do Python.
[1] : A partir de agora, as referências aos arquivos na fonte Python são fornecidas relativamente à raiz da árvore de fontes, que é o diretório em que você executa o configure e o faz para criar o Python.
Duas modificações devem ser feitas no arquivo de gramática. O primeiro é adicionar uma definição para a until
declaração. Encontrei onde a while
declaração foi definida ( while_stmt
) e adicionei until_stmt
abaixo [2] :
compound_stmt: if_stmt | while_stmt | until_stmt | for_stmt | try_stmt | with_stmt | funcdef | classdef | decorated
if_stmt: 'if' test ':' suite ('elif' test ':' suite)* ['else' ':' suite]
while_stmt: 'while' test ':' suite ['else' ':' suite]
until_stmt: 'until' test ':' suite
[2] : Isso demonstra uma técnica comum que eu uso ao modificar o código-fonte que não estou familiarizado: trabalho por similaridade . Este princípio não resolverá todos os seus problemas, mas definitivamente pode facilitar o processo. Como tudo o que precisa ser feito while
também precisa ser feito until
, serve como uma boa orientação.
Observe que eu decidi excluir a else
cláusula da minha definição de until
, apenas para torná-la um pouco diferente (e porque francamente eu não gosto da else
cláusula de loops e não acho que ela se encaixa bem no Zen de Python).
A segunda alteração é modificar a regra para compound_stmt
incluir until_stmt
, como você pode ver no snippet acima. É logo depois while_stmt
, novamente.
Quando você executa make
após a modificação Grammar/Grammar
, observe que o pgen
programa é executado para gerar novamente Include/graminit.h
e Python/graminit.c
, em seguida, vários arquivos são recompilados.
Modificando o código de geração AST
Depois que o analisador Python cria uma árvore de análise, essa árvore é convertida em um AST, pois os ASTs são muito mais simples de trabalhar nos estágios subsequentes do processo de compilação.
Então, vamos visitar o Parser/Python.asdl
que define a estrutura dos ASTs do Python e adicionar um nó AST para nossa nova until
declaração, novamente logo abaixo do while
:
| While(expr test, stmt* body, stmt* orelse)
| Until(expr test, stmt* body)
Se você executar agora make
, observe que antes de compilar um monte de arquivos, Parser/asdl_c.py
é executado para gerar o código C a partir do arquivo de definição AST. Este Grammar/Grammar
exemplo é outro exemplo do código-fonte Python usando uma mini-linguagem (em outras palavras, uma DSL) para simplificar a programação. Observe também que, como Parser/asdl_c.py
é um script Python, esse é um tipo de inicialização - para criar o Python do zero, o Python já deve estar disponível.
Enquanto Parser/asdl_c.py
geramos o código para gerenciar nosso nó AST recém-definido (nos arquivos Include/Python-ast.h
e Python/Python-ast.c
), ainda precisamos escrever o código que converte um nó relevante da árvore de análise nele manualmente. Isto é feito no arquivo Python/ast.c
. Lá, uma função denominada ast_for_stmt
converte nós da árvore de análise para instruções em nós AST. Novamente, guiados por nosso velho amigo while
, saltamos direto para o grande ponto switch
de manipulação de declarações compostas e adicionamos uma cláusula para until_stmt
:
case while_stmt:
return ast_for_while_stmt(c, ch);
case until_stmt:
return ast_for_until_stmt(c, ch);
Agora devemos implementar ast_for_until_stmt
. Aqui está:
static stmt_ty
ast_for_until_stmt(struct compiling *c, const node *n)
{
/* until_stmt: 'until' test ':' suite */
REQ(n, until_stmt);
if (NCH(n) == 4) {
expr_ty expression;
asdl_seq *suite_seq;
expression = ast_for_expr(c, CHILD(n, 1));
if (!expression)
return NULL;
suite_seq = ast_for_suite(c, CHILD(n, 3));
if (!suite_seq)
return NULL;
return Until(expression, suite_seq, LINENO(n), n->n_col_offset, c->c_arena);
}
PyErr_Format(PyExc_SystemError,
"wrong number of tokens for 'until' statement: %d",
NCH(n));
return NULL;
}
Mais uma vez, isso foi codificado enquanto observava atentamente o equivalente ast_for_while_stmt
, com a diferença de que until
eu decidi não apoiar a else
cláusula. Como esperado, o AST é criado recursivamente, usando outras funções de criação do AST, como ast_for_expr
para a expressão da condição e ast_for_suite
para o corpo da until
instrução. Finalmente, um novo nó chamado Until
é retornado.
Observe que acessamos o nó da árvore de análise n
usando algumas macros como NCH
e CHILD
. Vale a pena entender - o código deles está inserido Include/node.h
.
Digressão: composição AST
Eu escolhi criar um novo tipo de AST para a until
declaração, mas na verdade isso não é necessário. Eu poderia ter poupado algum trabalho e implementado a nova funcionalidade usando a composição dos nós AST existentes, pois:
until condition:
# do stuff
É funcionalmente equivalente a:
while not condition:
# do stuff
Em vez de criar o Until
nó ast_for_until_stmt
, eu poderia ter criado um Not
nó com um While
nó como filho. Como o compilador AST já sabe como lidar com esses nós, as próximas etapas do processo podem ser ignoradas.
Compilando ASTs no bytecode
O próximo passo é compilar o AST no bytecode do Python. A compilação tem um resultado intermediário, que é um CFG (Control Flow Graph), mas como o mesmo código lida com isso, ignorarei esse detalhe por enquanto e o deixarei para outro artigo.
O código que veremos a seguir é Python/compile.c
. Seguindo o exemplo while
, encontramos a função compiler_visit_stmt
, que é responsável pela compilação de instruções no bytecode. Adicionamos uma cláusula para Until
:
case While_kind:
return compiler_while(c, s);
case Until_kind:
return compiler_until(c, s);
Se você quer saber o que Until_kind
é, é uma constante (na verdade, um valor da _stmt_kind
enumeração) gerada automaticamente do arquivo de definição AST para Include/Python-ast.h
. De qualquer forma, chamamos o compiler_until
que, é claro, ainda não existe. Vou chegar um momento.
Se você é curioso como eu, notará que isso compiler_visit_stmt
é peculiar. Nenhuma quantidade de grep
-ping na árvore de origem revela onde é chamada. Quando este for o caso, resta apenas uma opção - C macro-fu. De fato, uma breve investigação nos leva à VISIT
macro definida em Python/compile.c
:
#define VISIT(C, TYPE, V) {\
if (!compiler_visit_ ## TYPE((C), (V))) \
return 0; \
Ele é usado para invocar compiler_visit_stmt
em compiler_body
. De volta aos nossos negócios, no entanto ...
Como prometido, aqui está compiler_until
:
static int
compiler_until(struct compiler *c, stmt_ty s)
{
basicblock *loop, *end, *anchor = NULL;
int constant = expr_constant(s->v.Until.test);
if (constant == 1) {
return 1;
}
loop = compiler_new_block(c);
end = compiler_new_block(c);
if (constant == -1) {
anchor = compiler_new_block(c);
if (anchor == NULL)
return 0;
}
if (loop == NULL || end == NULL)
return 0;
ADDOP_JREL(c, SETUP_LOOP, end);
compiler_use_next_block(c, loop);
if (!compiler_push_fblock(c, LOOP, loop))
return 0;
if (constant == -1) {
VISIT(c, expr, s->v.Until.test);
ADDOP_JABS(c, POP_JUMP_IF_TRUE, anchor);
}
VISIT_SEQ(c, stmt, s->v.Until.body);
ADDOP_JABS(c, JUMP_ABSOLUTE, loop);
if (constant == -1) {
compiler_use_next_block(c, anchor);
ADDOP(c, POP_BLOCK);
}
compiler_pop_fblock(c, LOOP, loop);
compiler_use_next_block(c, end);
return 1;
}
Tenho uma confissão a fazer: esse código não foi escrito com base em um profundo entendimento do bytecode do Python. Como o restante do artigo, foi feito imitando a compiler_while
função de parentesco . Ao ler com cuidado, no entanto, lembrando que a VM do Python é baseada em pilha e olhando para a documentação do dis
módulo, que possui uma lista de bytecodes do Python com descrições, é possível entender o que está acontecendo.
É isso aí, terminamos ... Não estamos?
Depois de fazer todas as alterações e executar make
, podemos executar o Python recém-compilado e tentar nossa nova until
declaração:
>>> until num == 0:
... print(num)
... num -= 1
...
3
2
1
Voila, funciona! Vamos ver o bytecode criado para a nova instrução usando o dis
módulo da seguinte maneira:
import dis
def myfoo(num):
until num == 0:
print(num)
num -= 1
dis.dis(myfoo)
Aqui está o resultado:
4 0 SETUP_LOOP 36 (to 39)
>> 3 LOAD_FAST 0 (num)
6 LOAD_CONST 1 (0)
9 COMPARE_OP 2 (==)
12 POP_JUMP_IF_TRUE 38
5 15 LOAD_NAME 0 (print)
18 LOAD_FAST 0 (num)
21 CALL_FUNCTION 1
24 POP_TOP
6 25 LOAD_FAST 0 (num)
28 LOAD_CONST 2 (1)
31 INPLACE_SUBTRACT
32 STORE_FAST 0 (num)
35 JUMP_ABSOLUTE 3
>> 38 POP_BLOCK
>> 39 LOAD_CONST 0 (None)
42 RETURN_VALUE
A operação mais interessante é o número 12: se a condição for verdadeira, saltaremos para após o loop. Esta é a semântica correta para until
. Se o salto não for executado, o corpo do loop continuará em execução até retornar à condição na operação 35.
Sentindo-me bem com minha alteração, tentei executar a função (executando myfoo(3)
) em vez de mostrar seu bytecode. O resultado foi menos que encorajador:
Traceback (most recent call last):
File "zy.py", line 9, in
myfoo(3)
File "zy.py", line 5, in myfoo
print(num)
SystemError: no locals when loading 'print'
Whoa ... isso não pode ser bom. Então, o que deu errado?
O caso da tabela de símbolos ausente
Uma das etapas que o compilador Python executa ao compilar o AST é criar uma tabela de símbolos para o código que ele compila. A chamada para PySymtable_Build
em PyAST_Compile
chamadas para o módulo da tabela de símbolos ( Python/symtable.c
), que anda a AST de uma maneira semelhante às funções de geração de código. Ter uma tabela de símbolos para cada escopo ajuda o compilador a descobrir algumas informações importantes, como quais variáveis são globais e quais são locais em um escopo.
Para corrigir o problema, precisamos modificar a symtable_visit_stmt
função Python/symtable.c
, adicionando código para manipulação de until
instruções, após o código semelhante para while
instruções [3] :
case While_kind:
VISIT(st, expr, s->v.While.test);
VISIT_SEQ(st, stmt, s->v.While.body);
if (s->v.While.orelse)
VISIT_SEQ(st, stmt, s->v.While.orelse);
break;
case Until_kind:
VISIT(st, expr, s->v.Until.test);
VISIT_SEQ(st, stmt, s->v.Until.body);
break;
[3] : A propósito, sem esse código, há um aviso para o compilador Python/symtable.c
. O compilador percebe que o Until_kind
valor da enumeração não é tratado na instrução switch symtable_visit_stmt
e reclama. É sempre importante verificar se há avisos do compilador!
E agora realmente terminamos. Compilar a fonte após essa alteração faz a execução do myfoo(3)
trabalho conforme o esperado.
Conclusão
Neste artigo, demonstramos como adicionar uma nova declaração ao Python. Embora exigindo um pouco de ajustes no código do compilador Python, a mudança não foi difícil de implementar, porque usei uma declaração semelhante e existente como orientação.
O compilador Python é um pedaço sofisticado de software, e não pretendo ser um especialista nele. No entanto, estou realmente interessado nos elementos internos do Python, e particularmente no seu front-end. Portanto, achei este exercício um companheiro muito útil para o estudo teórico dos princípios e código fonte do compilador. Servirá de base para futuros artigos que se aprofundarão no compilador.
Referências
Eu usei algumas excelentes referências para a construção deste artigo. Aqui eles estão em nenhuma ordem particular:
- PEP 339: Projeto do compilador CPython - provavelmente a parte mais importante e abrangente da documentação oficial do compilador Python. Por ser muito curto, mostra dolorosamente a escassez de boa documentação dos internos do Python.
- "Internos do compilador Python" - um artigo de Thomas Lee
- "Python: Design e Implementação" - uma apresentação de Guido van Rossum
- Python (2.5) Virtual Machine, Uma visita guiada - uma apresentação por Peter Tröger
fonte original