Python Bytecode’a Giriş(Python Nasıl Çalışır ?- Python Virtual Machine)
Geliştiriciler tarafından yazılan kodların, bilgisayar tarafından nasıl anlaşıldığını hiç düşündünüz mü ?…Yazılımla ilgilenen veya bu mesleği icra eden kişilerin aslına bakarsanız cevabı oldukça net olacaktır, zira bu yazımda bir parça daha yazdığımız kodların bilgisayar diline nasıl çevirildiğine Python tarafındaki sürecine deyinmiş olacağız…
Çoğu kişininde tahmin edeceği üzere, Python tarafındaki süreç diğer programlama dillerine kıyasla üç aşağı beş yukarı genel mantığı aynı fakat mimari olarak kendi içlerinde özelleşmiş oluyorlar..Kısaca tabiri ile bytecode bir program koduna verilen isimdir ve sanal makinaların anlayabilmesi için oluşturulan taşınabilir kodlara verilen isimdir…Daha da genel tabiriyle ele almak istersek genel aşamaları ile şu şekilde olmalıdır :
1)Geliştirici tarafıdan kod yazılır
2 )Yazılan kod derleyici tarafından derlenir
3 )Derleyici tarafından derlenen ve sonucunda bytecode’u adı verilen bir tür sanal makine kodu ortaya çıkar
Aslına bakarsanız platform bağımsızlığını sağlayan şey bu bytecode’dur…Nedeni ise bir bytecode oluşturulduktan sonra yazılım tüm işletim sistemlerinde çalışabilir…
Birden fazla bytecode syntax’ı görebilmek mümkün.Örnek verebilmek gerekirse Java Sanal Makinası’nın(Java Virtual Machine-JVM) okuyabilmesi adına derlenen bytecode ile Python Sanal Makinası’nın(Python Virtual Machine-PVM) okuyabilmesi için oluşturulan bytecode’u karşılaştırırsanız syntax’larının farklı olduğunu görebilmeniz mümkündür.
…
Python ile daha önce ilgilendiyseniz yüksek bir ihtimal ile Python kaynak kodu dosyalarını görmeye alışkınsınızdır, sonu .py ile biten dosyalar geliştiriciler tarafından oluşturulan ve python kodlarının olduğu dosyalardır.Sonu .pyc ile biten başka bir dosya türü ile karşılaştığınızda ise bunların Python ‘bytecode’ dosyaları olduğunu duymuş olabilirsiniz…
NOT:Bunları Python 3'te görmek biraz daha zordur .py dosyalarınızla aynı dizine girmek yerine __pycache__ adlı bir alt dizine girerler.
Python Nasıl Çalışıyor ?
Python genel olarak, program çalışırken kaynak kodunuzun birebir CPU’nun anlayabileceği şekilde çevirilmiş ve yorumlanmış bir dil olarak tanımlanır…Bu yalnızca ve yalnızca kısmen doğrudur,Python yorumlanmış(interpreted languages) birçok dil gibi aslında sanal bir makine için bir dizi talimat için kaynak kodunu derler ve bu Python yorumlayıcısı(Python Interpreter) bu bahsi geçen sanal makinanın bir uygulayıcısıdır.Bu ara biçime ise bytecode adı verilir…
Yani başka bir değişle,Python’ın bıraktığı bu .pyc dosyaları kaynak kodunuzun sadece ‘daha hızlı’ ve ‘optimizasyonu yapılmış’ versiyonları değildir;bunlar programınız çalışırken Python’ın sanal makinesi tarafıdan yürütülecek bytecode talimatlarıdır.
Kısa ve klasik bir örnek ile göz atmak istersek
import disdef hello():
print(“Hello, World!”)
dis.dis(hello)
Yukarıdaki kod bloğunu çalıştırdığımızda bytecode insanlarında okuyabileceği bir çevrilmiş bytecode çıkmakta
hello() fonksiyonunu yazıp çalıştırmak istersek CPython yorumlayıcısı(interpreter) yukarıdaki listeyi yürütür…Buraya kadar biraz karmaşık gelebilir, bu yüzden bir parça daha derine inebiliriz.
CPython,standart Python yorumlayıcısıdır.CPython dışında farklı interpreter’larda mevcut(Örnek:IronPython,Jython,PyPy,PythonNet,Stackless Python. vb..)
CPython, yığın tabanlı bir sanal makine kullanır ve üç tür yığın mevcuttur:
- Çağrı Yığını(Call Stack) : Bu, çalışan bir Python programının ana yapısıdır. Halihazırda etkin olan her işlev çağrısı için bir öğe bir “çerçeve”(frame) vardır ve yığının alt kısmı programın giriş noktasıdır. Her işlev çağrısı, çağrı yığınına yeni bir çerçeve iter(push) ve bir işlev çağrısı her döndüğünde, çerçevesi açılır.
- Değerlendirme Yığını/Veri Yığını(Evaluation/Data Stack):Her karede bir değerlendirme yığını (veri yığını olarak da adlandırılır) vardır. Bu yığın, bir Python fonksiyonunun yürütülmesinin gerçekleştiği yerdir ve Python kodunu çalıştırmak, çoğunlukla bu yığının üzerine bir şeyler itmek, onları manipüle etmek ve geri atmaktan oluşur.
- Blok Yığını(Block Stack): Her çerçevede(framede) bir blok yığını vardır.Bu, Python tarafından belirli kontrol yapılarının türlerini takip etmek için kullanılır: loop’lar, blokları try / catch bloklarla tüm girişler blok yığınına itilir ve blok yığını bu yapılardan herhangi bir çıkış yaptığında atar. Bu, Python’un belirli bir anda hangi blokların aktif olduğunu bilmesine yardımcı olur, örneğin continue veya break ifadesi doğru bloğu etkileyebilir.
…
Elimizde bir fonksiyonu çağıran bir kod olduğunu varsayalım,Örneğin:my_func(my_var,2)
Python bunu 4 byte’lık komut dizisine çevirecektir :
- my_func fonksiyon nesnesini arayan ve bunu değerlendirme yığınının üstüne iten bir LOAD_NAME komutu
- Değişkeninini aramak ve değerlendirme yığınının üstüne itmek için başka bir LOAD_NAME komutu
- Değişmez tamsayı değeri 2'yi değerlendirme yığınının üstüne itmek için bir LOAD_CONST komutu
- Ve son olarak CALL_FUNCTION komutu
CALL_FUNCTION komutu, Python’ın yığının üstünden iki konum bağımsız değişkeni çıkarması gerektiğini belirten 2 bağımsız değişkenine sahip olacaktır; çağrılacak işlev en üstte olacaktır ve ayrıca (anahtar kelime bağımsız değişkenlerini içeren işlevler için farklı bir komut — CALL_FUNCTION_KW — kullanılır, ancak benzer bir çalışma prensibiyle ve üçüncü bir komut olan CALL_FUNCTION_EX kullanılır. * veya ** operatörleri ile argüman açmayı içeren fonksiyon çağrıları içindir…). Python tüm bunlara sahip olduğunda, çağrı yığınına yeni bir çerçeve tahsis edecek, işlev çağrısı için yerel değişkenleri dolduracak ve bu çerçevenin içindeki my_func fonksiyonu’nun byte code’unu çalıştıracaktır.
Bu olayı daha iyi anlayabilmek için, Python standart kütüphanesindeki dis modülü bize bu konuda yardımcı olacaktır.dis, Python byte code’u için bir “disassembler” sağlar yani türkçeye çevirmeye çalışırsak aslında bir “sökücü” sağlar, bu da insan tarafından okunabilir bir sürüm almayı ve çeşitli byte code komutlarını aramayı kolaylaştırır. Dis modülünün belgeleri içeriğinin üzerinden geçer ve yaptıkları ve aldıkları argümanlarla birlikte byte code komutlarının tam bir listesini sağlar.
Yukarıda paylaşmış olduğum kod bloğunda,dis.dis() fonksitonu , kaynak kod içeren bir işlevi, yöntemi, sınıfı, modülü, derlenmiş Python kod nesnesini veya dize değişmezini ayıracak ve insan tarafından okunabilen bir sürümünü yazdıracaktır.
Python’un her fonksiyon için oluşturduğu derlenmiş kod nesnelerine bakmak da yararlıdır, çünkü bir fonksiyonu çalıştırmak bu kod nesnelerinin niteliklerini kullamaktır. hello() fonksiyonu için bir örnek :
hello() fonksiyonu için listelenmiş bytecode listesini incelemek istersek :
- LOAD_GLOBAL 0: Python’a, co_names (yazdırma işlevi olan) 0 dizinindeki adla başvurulan global nesneyi aramasını ve bunu değerlendirme yığınına itmesini söyler
- LOAD_CONST 1 : co_consts dizin 1'deki değişmez değeri alır ve iter (0 dizinindeki değer, açık dönüş ifadesine ulaşılmadığında Python işlev çağrılarının örtük bir Dönüş değeri olduğundan, co_consts’ta bulunan hiçbiri değişmezidir).
- CALL_FUNCTION 1: Python’a bir fonksiyon çağırmasını söyler; yığından bir konumsal argüman açması gerekir, ardından yeni yığının en üstünde çağrılacak fonksiyon olacaktır.
Takipte Kalın
Arda Batuhan Demir