Bunu yararlı bulabilirsiniz - Python internals: burada alıntı yapılan Python'a yeni bir ifade ekleme :
Bu makale, Python'un ön ucunun nasıl çalıştığını daha iyi anlamaya yönelik bir girişimdir. Sadece belgeleri ve kaynak kodunu okumak biraz sıkıcı olabilir, bu yüzden burada uygulamalı bir yaklaşım benimsiyorum: untilPython'a bir ifade ekleyeceğim .
Bu makalenin tüm kodlaması, Python Mercurial depo aynasındaki son teknoloji Py3k şubesine karşı yapıldı .
untilaçıklama
Ruby gibi bazı dillerin untiltamamlayıcısı olan while( until num == 0ile eşdeğer olan while num != 0) bir deyimi vardır . Ruby'de şunu yazabilirim:
num = 3
until num == 0 do
puts num
num -= 1
end
Ve yazdıracak:
3
2
1
Bu yüzden, Python'a benzer bir yetenek eklemek istiyorum. Yani, yazabilmek:
num = 3
until num == 0:
print(num)
num -= 1
Bir dil savunuculuğu araştırması
Bu makale untilPython'a bir ifade eklemeyi önermiyor . Böyle bir ifadenin bazı kodları daha açık hale getireceğini düşünmeme ve bu makale eklemenin ne kadar kolay olduğunu göstermesine rağmen, Python'un minimalizm felsefesine tamamen saygı duyuyorum. Burada gerçekten yapmaya çalıştığım tek şey Python'un iç işleyişi hakkında biraz fikir edinmek.
Dilbilgisini değiştirmek
Python, adında özel bir ayrıştırıcı oluşturucu kullanır pgen. Bu, Python kaynak kodunu bir ayrıştırma ağacına dönüştüren bir LL (1) ayrıştırıcısıdır. Ayrıştırıcı oluşturucunun girdisi Grammar/Grammar[1] dosyasıdır . Bu, Python'un gramerini belirten basit bir metin dosyasıdır.
[1] : Bundan sonra, Python kaynağındaki dosyalara referanslar, Python'u oluşturmak için configure ve make'i çalıştırdığınız dizin olan kaynak ağacının köküne göre verilir.
Dilbilgisi dosyasında iki değişiklik yapılmalıdır. Birincisi, ifade için bir tanım eklemektir until. İfadenin nerede whiletanımlandığını ( while_stmt) buldum ve [2]until_stmt altına ekledim :
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] : Bu, aşina olmadığım kaynak kodunu değiştirirken kullandığım yaygın bir tekniği gösteriyor: benzerlikle çalışmak . Bu ilke tüm sorunlarınızı çözmez, ancak süreci kesinlikle kolaylaştırabilir. Bunun için yapılması gereken her şeyin de yapılması whilegerektiğinden until, oldukça iyi bir kılavuz görevi görür.
Ben dışlamak için karar verdiğiniz Not elsebenim tanımından maddesini untilsadece biraz farklı yapmak için, (ve açıkçası sevmediğim çünkü elsedöngü maddesini ve Python Zen uymaktadır sanmıyorum).
İkinci değişiklik, yukarıdaki kod parçacığında görebileceğiniz gibi compound_stmt, dahil edilecek kuralı değiştirmektir until_stmt. Hemen sonra while_stmt, yine.
Eğer çalıştırdığınızda makeburada geliştirme sonra Grammar/Grammar, bildirim bu pgenprogramı çalıştırılır yeniden üretmek Include/graminit.hve Python/graminit.cve ardından birkaç dosya yeniden derlenmiş olsun.
AST oluşturma kodunu değiştirme
Python ayrıştırıcısı bir ayrıştırma ağacı oluşturduktan sonra, bu ağaç bir AST'ye dönüştürülür, çünkü AST'lerin derleme işleminin sonraki aşamalarında çalışmak çok daha kolaydır .
Öyleyse, Parser/Python.asdlPython'un AST'lerinin yapısını tanımlayan ziyaret edeceğiz ve yeni ifademiz için bir AST düğümü ekleyeceğiz until, yine şunun hemen altına while:
| While(expr test, stmt* body, stmt* orelse)
| Until(expr test, stmt* body)
Şimdi çalıştırırsanız make, bir grup dosyayı derlemeden önce Parser/asdl_c.py, AST tanım dosyasından C kodu oluşturmak için çalıştırıldığına dikkat edin. Bu (benzeri Grammar/Grammar), programlamayı basitleştirmek için mini bir dil (başka bir deyişle bir DSL) kullanan Python kaynak kodunun başka bir örneğidir. Ayrıca Parser/asdl_c.py, bir Python betiği olduğundan, bunun bir tür önyükleme olduğunu unutmayın - Python'u sıfırdan oluşturmak için, Python'un zaten mevcut olması gerekir.
İken Parser/asdl_c.py(dosyalar halinde bizim yeni tanımlanan AST düğümü yönetmek kodunu oluşturduktan Include/Python-ast.hve Python/Python-ast.c) için de kod yazmak zorunda dönüştürür o elle içine alakalı bir ayrıştırma ağacı düğümü. Bu dosyada yapılır Python/ast.c. Orada, adlı bir işlev ast_for_stmt, ifadeler için ayrıştırma ağaç düğümlerini AST düğümlerine dönüştürür. Yine, eski dostumuzun rehberliğinde while, switchbileşik ifadeleri işlemek için büyüklere atlıyoruz ve aşağıdakiler için bir cümle ekliyoruz until_stmt:
case while_stmt:
return ast_for_while_stmt(c, ch);
case until_stmt:
return ast_for_until_stmt(c, ch);
Şimdi uygulamalıyız ast_for_until_stmt. İşte burada:
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;
}
Yine, bu, eşdeğerine yakından bakarken kodlandı ast_for_while_stmt, çünkü untilben elsecümleyi desteklememeye karar verdim . Beklendiği gibi, AST, ast_for_exprkoşul ifadesi ve ifadenin ast_for_suitegövdesi gibi diğer AST oluşturma işlevleri kullanılarak yinelemeli olarak oluşturulur until. Son olarak, adlı yeni bir düğüm Untildöndürülür.
Ayrıştırma ağacı düğümüne ve ngibi bazı makroları kullanarak eriştiğimize dikkat edin . Bunlar anlaşılmaya değer - kodları burada .NCHCHILDInclude/node.h
Arasöz: AST bileşimi
İfade için yeni bir AST türü oluşturmayı seçtim until, ancak aslında bu gerekli değil. Bazı işlerden tasarruf edebilir ve mevcut AST düğümlerinin bileşimini kullanarak yeni işlevi uygulayabilirdim, çünkü:
until condition:
# do stuff
İşlevsel olarak eşdeğerdir:
while not condition:
# do stuff
UntilDüğümü içinde oluşturmak yerine, çocukken ast_for_until_stmtbir Notdüğümü olan bir Whiledüğüm oluşturabilirdim. AST derleyicisi bu düğümleri nasıl kullanacağını zaten bildiğinden, işlemin sonraki adımları atlanabilir.
AST'leri bayt koduna derleme
Bir sonraki adım, AST'yi Python bayt koduna derlemektir. Derlemenin bir CFG (Kontrol Akışı Grafiği) olan bir ara sonucu vardır, ancak aynı kod onu işlediği için şimdilik bu detayı göz ardı edip başka bir makaleye bırakacağım.
Bundan sonra bakacağımız kod Python/compile.c. Öncülüğünü takiben, ifadeleri bayt koduna derlemekten sorumlu olan whileişlevi buluruz compiler_visit_stmt. Aşağıdakiler için bir madde ekliyoruz Until:
case While_kind:
return compiler_while(c, s);
case Until_kind:
return compiler_until(c, s);
Ne olduğunu merak ediyorsanız Until_kind, bu bir sabittir (aslında _stmt_kindnumaralandırmanın bir değeri ), AST tanım dosyasından Include/Python-ast.h. Her neyse, compiler_untiltabii ki hala var olmayan diyoruz . Ona bir an geleceğim.
Benim gibi merak ediyorsanız, compiler_visit_stmtbunun tuhaf olduğunu fark edeceksiniz . grepKaynak ağacının hiçbir miktarı -ping nerede çağrıldığını göstermez. Durum bu olduğunda, geriye yalnızca bir seçenek kalır - C makro-fu. Aslında, kısa bir araştırma bizi VISITşurada tanımlanan makroya götürür Python/compile.c:
#define VISIT(C, TYPE, V) {\
if (!compiler_visit_ ## TYPE((C), (V))) \
return 0; \
Çağırmak için kullanılan compiler_visit_stmtiçinde compiler_body. Yine de işimize dönelim ...
Söz verdiğimiz gibi, işte 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;
}
Bir itirafım var: bu kod Python bayt kodunun derinlemesine anlaşılmasına dayalı olarak yazılmadı. Makalenin geri kalanı gibi, kin compiler_whileişlevini taklit ederek yapıldı . Bununla birlikte, dikkatlice okuyarak, Python VM'nin yığın tabanlı olduğunu ve açıklamaları olan Python bayt kodlarının bir listesinidis içeren modülün belgelerine göz atarak , neler olduğunu anlamak mümkündür.
İşte bu, bitirdik ... Değil mi?
Tüm değişiklikleri yaptıktan ve çalıştırdıktan sonra make, yeni derlenmiş Python'u çalıştırabilir ve yeni ifademizi deneyebiliriz until:
>>> until num == 0:
... print(num)
... num -= 1
...
3
2
1
Voila, işe yarıyor! disModül kullanılarak yeni ifade için oluşturulan bayt kodunu aşağıdaki gibi görelim :
import dis
def myfoo(num):
until num == 0:
print(num)
num -= 1
dis.dis(myfoo)
İşte sonuç:
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
En ilginç işlem 12 numara: eğer koşul doğruysa, döngüden sonra atlarız. Bu, için doğru anlambilimdir until. Atlama gerçekleştirilmezse, döngü gövdesi işlem 35'teki duruma geri dönene kadar çalışmaya devam eder.
Yaptığım değişiklikle ilgili iyi hissettiğimden myfoo(3), bayt kodunu göstermek yerine işlevi çalıştırmayı (yürütmeyi ) denedim . Sonuç cesaret verici olmaktan çok uzaktı:
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 ... bu iyi olamaz. Peki yanlış olan ne?
Eksik sembol tablosu durumu
Python derleyicisinin AST'yi derlerken gerçekleştirdiği adımlardan biri, derlediği kod için bir sembol tablosu oluşturmaktır. Çağrısı PySymtable_Buildolarak PyAST_Compilesimge tablo modülü (çağrıda Python/symtable.ckod oluşturma işlevlerine benzer bir şekilde AST yürür). Her kapsam için bir sembol tablosuna sahip olmak, derleyicinin hangi değişkenlerin genel ve hangilerinin bir kapsam için yerel olduğu gibi bazı önemli bilgileri anlamasına yardımcı olur.
Sorunu çözmek için, ifadeler [3] için benzer koddan sonra, ifadeleri işlemek için kod ekleyerek symtable_visit_stmtişlevi değiştirmeliyiz :Python/symtable.cuntilwhile
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] : Bu arada, bu kod olmadan için bir derleyici uyarısı var Python/symtable.c. Derleyici, Until_kindnumaralandırma değerinin switch deyiminde işlenmediğini fark eder symtable_visit_stmtve şikayet eder. Derleyici uyarılarını kontrol etmek her zaman önemlidir!
Ve şimdi gerçekten bitirdik. Bu değişiklikten sonra kaynağı derlemek, myfoo(3)işin beklendiği gibi yürütülmesini sağlar .
Sonuç
Bu makalede Python'a nasıl yeni bir ifade ekleneceğini gösterdim. Python derleyicisinin kodunda epeyce düzeltme gerektirse de, değişikliğin uygulanması zor olmadı çünkü kılavuz olarak benzer ve mevcut bir ifade kullandım.
Python derleyicisi karmaşık bir yazılım parçası ve bu konuda uzman olduğumu iddia etmiyorum. Bununla birlikte, Python'un iç kısımlarıyla ve özellikle ön ucuyla gerçekten ilgileniyorum. Bu nedenle, bu alıştırmayı derleyicinin ilkelerinin ve kaynak kodunun teorik çalışmasında çok yararlı bir yol arkadaşı buldum. Derleyicide daha derinlere inecek olan gelecekteki makaleler için bir temel oluşturacaktır.
Referanslar
Bu makalenin yapımı için birkaç mükemmel referans kullandım. İşte buradalar, belirli bir sıra yok:
- PEP 339: CPython derleyicisinin tasarımı - Python derleyicisi için muhtemelen en önemli ve kapsamlı resmi dokümantasyon parçası . Çok kısa olması, Python'un iç kısımlarının iyi dokümantasyonunun yetersizliğini acı verici bir şekilde gösterir.
- "Python Compiler Internals" - Thomas Lee'nin bir makalesi
- "Python: Tasarım ve Uygulama" - Guido van Rossum'un sunumu
- Python (2.5) Sanal Makine, Rehberli tur - Peter Tröger'in sunumu
orjinal kaynak