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: until
Python'a bir ifade ekleyeceğim .
Bu makalenin tüm kodlaması, Python Mercurial depo aynasındaki son teknoloji Py3k şubesine karşı yapıldı .
until
açıklama
Ruby gibi bazı dillerin until
tamamlayıcısı olan while
( until num == 0
ile 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 until
Python'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 while
tanı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ı while
gerektiğinden until
, oldukça iyi bir kılavuz görevi görür.
Ben dışlamak için karar verdiğiniz Not else
benim tanımından maddesini until
sadece biraz farklı yapmak için, (ve açıkçası sevmediğim çünkü else
dö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 make
burada geliştirme sonra Grammar/Grammar
, bildirim bu pgen
programı çalıştırılır yeniden üretmek Include/graminit.h
ve Python/graminit.c
ve 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.asdl
Python'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.h
ve 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
, switch
bileş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ü until
ben else
cümleyi desteklememeye karar verdim . Beklendiği gibi, AST, ast_for_expr
koşul ifadesi ve ifadenin ast_for_suite
gövdesi gibi diğer AST oluşturma işlevleri kullanılarak yinelemeli olarak oluşturulur until
. Son olarak, adlı yeni bir düğüm Until
döndürülür.
Ayrıştırma ağacı düğümüne ve n
gibi bazı makroları kullanarak eriştiğimize dikkat edin . Bunlar anlaşılmaya değer - kodları burada .NCH
CHILD
Include/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
Until
Düğümü içinde oluşturmak yerine, çocukken ast_for_until_stmt
bir Not
düğümü olan bir While
düğü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 while
iş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_kind
numaralandırmanın bir değeri ), AST tanım dosyasından Include/Python-ast.h
. Her neyse, compiler_until
tabii ki hala var olmayan diyoruz . Ona bir an geleceğim.
Benim gibi merak ediyorsanız, compiler_visit_stmt
bunun tuhaf olduğunu fark edeceksiniz . grep
Kaynak 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_stmt
iç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_while
iş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! dis
Modü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_Build
olarak PyAST_Compile
simge tablo modülü (çağrıda Python/symtable.c
kod 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_stmt
işlevi değiştirmeliyiz :Python/symtable.c
until
while
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_kind
numaralandırma değerinin switch deyiminde işlenmediğini fark eder symtable_visit_stmt
ve ş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