Potresti trovarlo utile - Python internals: aggiunta di una nuova istruzione a Python , citata qui:
Questo articolo è un tentativo di capire meglio come funziona il front-end di Python. La semplice lettura della documentazione e del codice sorgente può essere un po 'noiosa, quindi sto adottando un approccio pratico qui: aggiungerò until
un'istruzione a Python.
Tutta la codifica per questo articolo è stata eseguita contro il ramo Py3k all'avanguardia nel mirror del repository Python Mercurial .
La until
dichiarazione
Alcuni linguaggi, come Ruby, hanno until
un'istruzione, che è il complemento di while
( until num == 0
è equivalente a while num != 0
). In Ruby posso scrivere:
num = 3
until num == 0 do
puts num
num -= 1
end
E stamperà:
3
2
1
Quindi, voglio aggiungere una funzionalità simile a Python. Cioè, essere in grado di scrivere:
num = 3
until num == 0:
print(num)
num -= 1
Una digressione sulla difesa della lingua
Questo articolo non cerca di suggerire l'aggiunta di until
un'istruzione a Python. Anche se penso che una dichiarazione del genere renderebbe più chiaro un po 'di codice, e questo articolo mostra quanto sia facile aggiungere, rispetto completamente la filosofia del minimalismo di Python. Tutto quello che sto cercando di fare qui, in realtà, è acquisire una visione del funzionamento interno di Python.
Modificare la grammatica
Python utilizza un generatore di parser personalizzato denominato pgen
. Questo è un parser LL (1) che converte il codice sorgente Python in un albero di analisi. L'input per il generatore di parser è il file Grammar/Grammar
[1] . Questo è un semplice file di testo che specifica la grammatica di Python.
[1] : Da qui in poi, i riferimenti ai file nel sorgente Python vengono forniti relativamente alla radice dell'albero dei sorgenti, che è la directory in cui si esegue configure e make per compilare Python.
È necessario apportare due modifiche al file della grammatica. Il primo è aggiungere una definizione per l' until
affermazione. Ho trovato dove è while
stata definita l' istruzione ( while_stmt
) e aggiunta di until_stmt
seguito [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] : Questo dimostra una tecnica comune che uso quando modifico il codice sorgente che non conosco: lavorare per somiglianza . Questo principio non risolverà tutti i tuoi problemi, ma può sicuramente facilitare il processo. Poiché tutto ciò per cui deve essere fatto while
deve essere fatto anche per until
, funge da linea guida piuttosto buona.
Nota che ho deciso di escludere la else
clausola dalla mia definizione di until
, solo per renderla un po 'diversa (e perché francamente non mi piace la else
clausola dei cicli e non penso che si adatti bene allo Zen di Python).
La seconda modifica consiste nel modificare la regola per compound_stmt
includere until_stmt
, come puoi vedere nello snippet sopra. È subito dopo while_stmt
, di nuovo.
Quando si esegue make
dopo la modifica Grammar/Grammar
, notare che il pgen
programma viene eseguito per rigenerare Include/graminit.h
e Python/graminit.c
, quindi diversi file vengono ricompilati.
Modifica del codice di generazione AST
Dopo che il parser Python ha creato un albero di analisi, questo albero viene convertito in un AST, poiché è molto più semplice lavorare con gli AST nelle fasi successive del processo di compilazione.
Quindi, visiteremo Parser/Python.asdl
che definisce la struttura degli AST di Python e aggiungeremo un nodo AST per la nostra nuova until
istruzione, di nuovo proprio sotto while
:
| While(expr test, stmt* body, stmt* orelse)
| Until(expr test, stmt* body)
Se ora esegui make
, nota che prima di compilare un gruppo di file, Parser/asdl_c.py
viene eseguito per generare codice C dal file di definizione AST. Questo (come Grammar/Grammar
) è un altro esempio del codice sorgente Python che utilizza un mini-linguaggio (in altre parole, un DSL) per semplificare la programmazione. Si noti inoltre che poiché Parser/asdl_c.py
è uno script Python, questa è una sorta di bootstrap : per costruire Python da zero, Python deve già essere disponibile.
Durante la Parser/asdl_c.py
generazione del codice per gestire il nostro nodo AST appena definito (nei file Include/Python-ast.h
e Python/Python-ast.c
), dobbiamo ancora scrivere manualmente il codice che converte un nodo di parse-tree rilevante in esso. Questo viene fatto nel file Python/ast.c
. Lì, una funzione denominata ast_for_stmt
converte i nodi dell'albero di analisi per le istruzioni in nodi AST. Di nuovo, guidati dal nostro vecchio amico while
, saltiamo subito alla grande switch
per la gestione delle dichiarazioni composte e aggiungiamo una clausola per until_stmt
:
case while_stmt:
return ast_for_while_stmt(c, ch);
case until_stmt:
return ast_for_until_stmt(c, ch);
Ora dovremmo implementare ast_for_until_stmt
. Ecco qui:
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;
}
Di nuovo, questo è stato codificato osservando attentamente l'equivalente ast_for_while_stmt
, con la differenza che until
ho deciso di non supportare la else
clausola. Come previsto, l'AST viene creato in modo ricorsivo, utilizzando altre funzioni di creazione AST come ast_for_expr
per l'espressione della condizione e ast_for_suite
per il corpo until
dell'istruzione. Infine, Until
viene restituito un nuovo nodo denominato .
Si noti che accediamo al nodo dell'albero sintetico n
utilizzando alcune macro come NCH
e CHILD
. Vale la pena comprenderli: il loro codice è in Include/node.h
.
Digressione: composizione AST
Ho scelto di creare un nuovo tipo di AST per l' until
istruzione, ma in realtà non è necessario. Avrei potuto risparmiare un po 'di lavoro e implementare la nuova funzionalità utilizzando la composizione dei nodi AST esistenti, poiché:
until condition:
# do stuff
È funzionalmente equivalente a:
while not condition:
# do stuff
Invece di creare il Until
nodo in ast_for_until_stmt
, avrei potuto creare un Not
nodo con un While
nodo da bambino. Poiché il compilatore AST sa già come gestire questi nodi, i passaggi successivi del processo potrebbero essere saltati.
Compilazione di AST in bytecode
Il passaggio successivo è la compilazione dell'AST in bytecode Python. La compilazione ha un risultato intermedio che è un CFG (Control Flow Graph), ma poiché lo gestisce lo stesso codice, per ora ignorerò questo dettaglio e lo lascerò per un altro articolo.
Il codice che vedremo dopo è Python/compile.c
. Seguendo l'esempio di while
, troviamo la funzione compiler_visit_stmt
, che è responsabile della compilazione delle istruzioni in bytecode. Aggiungiamo una clausola per Until
:
case While_kind:
return compiler_while(c, s);
case Until_kind:
return compiler_until(c, s);
Se ti chiedi cosa Until_kind
sia, è una costante (in realtà un valore _stmt_kind
dell'enumerazione) generata automaticamente dal file di definizione AST in Include/Python-ast.h
. Ad ogni modo, chiamiamo compiler_until
che, ovviamente, ancora non esiste. Ci arrivo un attimo.
Se sei curioso come me, noterai che compiler_visit_stmt
è strano. Nessuna quantità di grep
-ping dell'albero dei sorgenti rivela dove viene chiamato. Quando questo è il caso, rimane solo un'opzione: C macro-fu. Infatti, una breve indagine ci conduce alla VISIT
macro definita in Python/compile.c
:
#define VISIT(C, TYPE, V) {\
if (!compiler_visit_ ## TYPE((C), (V))) \
return 0; \
È usato per invocare compiler_visit_stmt
in compiler_body
. Ma torniamo al nostro lavoro ...
Come promesso, ecco 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;
}
Ho una confessione da fare: questo codice non è stato scritto sulla base di una profonda comprensione del bytecode Python. Come il resto dell'articolo, è stato fatto a imitazione della compiler_while
funzione kin . Leggendolo attentamente, tuttavia, tenendo presente che la VM Python è basata su stack e dando uno sguardo alla documentazione del dis
modulo, che ha un elenco di bytecode Python con descrizioni, è possibile capire cosa sta succedendo.
Ecco, abbiamo finito ... non è vero?
Dopo aver apportato tutte le modifiche ed eseguito make
, possiamo eseguire il Python appena compilato e provare la nostra nuova until
istruzione:
>>> until num == 0:
... print(num)
... num -= 1
...
3
2
1
Voilà, funziona! Vediamo il bytecode creato per la nuova istruzione utilizzando il dis
modulo come segue:
import dis
def myfoo(num):
until num == 0:
print(num)
num -= 1
dis.dis(myfoo)
Ecco il risultato:
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
L'operazione più interessante è la numero 12: se la condizione è vera, saltiamo dopo il ciclo. Questa è la semantica corretta per until
. Se il salto non viene eseguito, il corpo del loop continua a correre finché non torna alla condizione dell'operazione 35.
Sentendomi bene per la mia modifica, ho quindi provato a eseguire la funzione (esecuzione myfoo(3)
) invece di mostrare il suo bytecode. Il risultato è stato tutt'altro che incoraggiante:
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 ... questo non può essere buono. Allora cosa è andato storto?
Il caso della tabella dei simboli mancante
Uno dei passaggi che il compilatore Python esegue durante la compilazione dell'AST è la creazione di una tabella dei simboli per il codice che compila. La chiamata a PySymtable_Build
in PyAST_Compile
chiama nel modulo tabella dei simboli (Python/symtable.c
), che percorre l'AST in un modo simile alle funzioni di generazione del codice. Avere una tabella dei simboli per ogni ambito aiuta il compilatore a capire alcune informazioni chiave, come quali variabili sono globali e quali sono locali rispetto a un ambito.
Per risolvere il problema, dobbiamo modificare la symtable_visit_stmt
funzione in Python/symtable.c
, aggiungendo codice per la gestione delle until
istruzioni, dopo il codice simile per le while
istruzioni [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 proposito, senza questo codice c'è un avviso del compilatore per Python/symtable.c
. Il compilatore nota che il Until_kind
valore di enumerazione non viene gestito nell'istruzione switch di symtable_visit_stmt
e si lamenta. È sempre importante controllare gli avvisi del compilatore!
E ora abbiamo davvero finito. La compilazione dell'origine dopo questa modifica rende l'esecuzione del myfoo(3)
lavoro come previsto.
Conclusione
In questo articolo ho dimostrato come aggiungere una nuova istruzione a Python. Sebbene richiedesse un po 'di armeggiare nel codice del compilatore Python, la modifica non è stata difficile da implementare, perché ho usato un'istruzione simile ed esistente come linea guida.
Il compilatore Python è un sofisticato pezzo di software e non pretendo di essere un esperto in esso. Tuttavia, sono molto interessato agli interni di Python, e in particolare al suo front-end. Pertanto, ho trovato questo esercizio un compagno molto utile per lo studio teorico dei principi del compilatore e del codice sorgente. Servirà come base per articoli futuri che approfondiranno il compilatore.
Riferimenti
Ho usato alcuni ottimi riferimenti per la costruzione di questo articolo. Eccoli senza alcun ordine particolare:
- PEP 339: Design del compilatore CPython - probabilmente la parte più importante e completa della documentazione ufficiale per il compilatore Python. Essendo molto breve, mostra dolorosamente la scarsità di una buona documentazione degli interni di Python.
- "Python Compiler Internals" - un articolo di Thomas Lee
- "Python: Design and Implementation" - una presentazione di Guido van Rossum
- Python (2.5) Virtual Machine, Una visita guidata - una presentazione di Peter Tröger
fonte originale