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ò untilun'istruzione a Python.
Tutta la codifica per questo articolo è stata eseguita contro il ramo Py3k all'avanguardia nel mirror del repository Python Mercurial .
La untildichiarazione
Alcuni linguaggi, come Ruby, hanno untilun'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 untilun'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' untilaffermazione. Ho trovato dove è whilestata definita l' istruzione ( while_stmt) e aggiunta di until_stmtseguito [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 whiledeve essere fatto anche per until, funge da linea guida piuttosto buona.
Nota che ho deciso di escludere la elseclausola dalla mia definizione di until, solo per renderla un po 'diversa (e perché francamente non mi piace la elseclausola dei cicli e non penso che si adatti bene allo Zen di Python).
La seconda modifica consiste nel modificare la regola per compound_stmtincludere until_stmt, come puoi vedere nello snippet sopra. È subito dopo while_stmt, di nuovo.
Quando si esegue makedopo la modifica Grammar/Grammar, notare che il pgenprogramma viene eseguito per rigenerare Include/graminit.he 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.asdlche definisce la struttura degli AST di Python e aggiungeremo un nodo AST per la nostra nuova untilistruzione, 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.pyviene 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.pygenerazione del codice per gestire il nostro nodo AST appena definito (nei file Include/Python-ast.he 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_stmtconverte i nodi dell'albero di analisi per le istruzioni in nodi AST. Di nuovo, guidati dal nostro vecchio amico while, saltiamo subito alla grande switchper 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 untilho deciso di non supportare la elseclausola. Come previsto, l'AST viene creato in modo ricorsivo, utilizzando altre funzioni di creazione AST come ast_for_exprper l'espressione della condizione e ast_for_suiteper il corpo untildell'istruzione. Infine, Untilviene restituito un nuovo nodo denominato .
Si noti che accediamo al nodo dell'albero sintetico nutilizzando alcune macro come NCHe 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' untilistruzione, 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 Untilnodo in ast_for_until_stmt, avrei potuto creare un Notnodo con un Whilenodo 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_kindsia, è una costante (in realtà un valore _stmt_kinddell'enumerazione) generata automaticamente dal file di definizione AST in Include/Python-ast.h. Ad ogni modo, chiamiamo compiler_untilche, 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 VISITmacro definita in Python/compile.c:
#define VISIT(C, TYPE, V) {\
if (!compiler_visit_ ## TYPE((C), (V))) \
return 0; \
È usato per invocare compiler_visit_stmtin 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_whilefunzione kin . Leggendolo attentamente, tuttavia, tenendo presente che la VM Python è basata su stack e dando uno sguardo alla documentazione del dismodulo, 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 untilistruzione:
>>> until num == 0:
... print(num)
... num -= 1
...
3
2
1
Voilà, funziona! Vediamo il bytecode creato per la nuova istruzione utilizzando il dismodulo 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_Buildin PyAST_Compilechiama 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_stmtfunzione in Python/symtable.c, aggiungendo codice per la gestione delle untilistruzioni, dopo il codice simile per le whileistruzioni [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_kindvalore di enumerazione non viene gestito nell'istruzione switch di symtable_visit_stmte 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