Python'da değişken kapsamı çözümü, yürütme sırasında dinamik olarak değil, derleme aşamasında statik olarak gerçekleştirilir. CPython derleyicisi bir fonksiyon tanımıyla karşılaştığında, soyut sözdizimsel ağaç üzerinden geçerek her ismi yerel, global veya hücre değişkeni olarak kategorize eden bir sembol tablosu oluşturur. Derleyici, fonksiyon gövdesindeki herhangi bir yerde bir isim için bir bağlılık işlemi—atama, artırılmış atama veya ithalat gibi—tespit ederse, o ismi tüm kapsam için yerel bir değişken olarak işaretler. Bu tasarım, sanal makinenin yavaş hash tablosu aramaları yerine sabit boyutlu bir dizide çalışan optimize edilmiş LOAD_FAST opcode'ları kullanmasını sağlar. Bu optimizasyon, Python'un fonksiyon çağrısı performansı için temeldir ancak katı bağlama gereklilikleri getirir.
Bir isim yerel olarak sınıflandırıldığında, derleyici o ismin tüm okuma işlemleri için LOAD_FAST bayt kodu talimatları üretir. Çalışma zamanında LOAD_FAST, çerçevenin yerel değişken dizisindeki karşılık gelen indeksten nesne referansını almaya çalışır. Eğer slot, henüz bir değer atanmadığını belirten bir null işaretçisi içeriyorsa, çalışma zamanı UnboundLocalError raise eder. Bu durum, aynı isimde bir global değişken olsa bile gerçekleşir, çünkü derleyici kasıtlı olarak LOAD_GLOBAL emrini üretmekten kaçınmıştır. Hata, bu statik kapsama kararını açıkça belirtir ve bunu NameError'dan ayırır.
Bunu çözmek için, derleyiciye ismin global isim alanını kastettiğinizi açıkça bildirmeniz gerekir; bunu yapmak için global <değişken_adı> tanımı yapmalısınız. Bu tanım, derleyicinin ismi modülün global sözlüğünde dinamik olarak aramak için LOAD_GLOBAL ve STORE_GLOBAL opcode'larına geçmesini sağlar. Alternatif olarak, kodunuzu yeniden yapılandırarak yerel değişkenlerinizi, herhangi bir koşullu mantıktan önce fonksiyonun başında tanımlandığından emin olun. İç içe kapsama alanları için nonlocal anahtar kelimesi, derleyicinin kapalı hücrelere erişmek için LOAD_DEREF kullanmasını zorlar. Bu tanımlar, derleme zamanında derleyicinin bağlama kararını değiştirir, böylece bağlı olmayan yerel durumu önler.
threshold = 100 def analyze(data): # Derleyici 'threshold = ...' ifadesini görür, yerel olarak işaretler if data > threshold: # UnboundLocalError raise eder return "high" threshold = 50 # Atama yerel yapar # 'global' kullanarak çözüm def analyze_fixed(data): global threshold if data > threshold: # LOAD_GLOBAL başarılı olur return "high" threshold = 50 # Global değişkeni günceller
Bir veri mühendisliği ekibi, Apache Airflow kullanarak bir ETL boru hattı oluşturuyordu. İşleme parametrelerinin kolayca ayarlanmasını sağlamak için modül seviyesinde bir varsayılan yapılandırma sözlüğü CONFIG = {"batch_size": 1000} tanımladılar. Ana dönüşüm fonksiyonu process_batch(), başlangıçta if len(records) > CONFIG["batch_size"]: kontrolü yaparak parçalama gerekip gerekmediğini belirlemeye çalışıyordu. Daha sonra fonksiyon içinde, belirli bir koşul altında, kod, CONFIG = {"batch_size": 500} ile parti boyutunu azaltmaya çalıştı. Bu desen istemeden bir kapsam çatışması tetikledi.
Boru hattı çalıştırıldığında, fonksiyonun ilk satırında UnboundLocalError ile çöktü: local variable 'CONFIG' referenced before assignment. Fonksiyonun sonundaki atama ifadesi, Python derleyicisinin CONFIG'i fonksiyon gövdesinin tamamı için yerel bir değişken olarak değerlendirmesine neden oldu. Sonuç olarak, başlangıçtaki karşılaştırma işlemi, başlatılmamış yerel değişken slotuna erişmek için LOAD_FAST kullandı. Bu hata, kritik bir üretim çalışması sırasında veri boru hattını durdurdu çünkü fonksiyon başlatılamadı.
Ekip, yerel yeniden atama işlemini local_config olarak yeniden adlandırmayı düşündü, böylece azaltılan parti işleme için yeni bir sözlük oluşturdu. Bu, gölgeleme sorununu tamamen önleyecek ve global yapılandırmayı değişmez hale getirecekti. Ancak, bu yaklaşım, CONFIG isminin mevcut sınırları yansıtmasını bekleyen aşağı akış kodunu yeniden yapılandırmayı gerektiriyordu. Eğer geliştirici, sonraki mantıkta yeni değişken ismini kullanmayı unutursa potansiyel tutarsızlıklar ortaya çıkıyordu. Aynı kavram için iki değişken ismini takip etmenin bilişsel yükü, bu çözümü daha az cazip hale getiriyordu.
Başka bir seçenek, fonksiyonun başında global CONFIG eklemekti, böylece derleyici tüm referansları global arama olarak değerlendirecekti. Bu hata oluşumunu önlese de, ekip bunu reddetti çünkü bir işlem sırasında global durumu değiştirmek tehlikeli bir anti-modeldir. Bu, fonksiyonun tekrar çağrılmasını önler ve birim testlerini ciddi şekilde karmaşıklaştırır. Ayrıca, eğer kod paralel bir şekilde iş parçacıklarına yayılırsa yarı zamanlı durumlar yaratır. Modül seviyesindeki durum üzerindeki yan etkiler, üretim veri boru hatları için kabul edilemez olarak değerlendirildi.
Üçüncü çözüm, mevcut sözlüğü yerinde değiştirerek CONFIG["batch_size"] = 500 kullanmaktı, değişken adını yeniden atamak yerine. Bu işlem, CONFIG ismi için yeni bir bağlama oluşturmadığından, derleyici bunu global bir referans olarak değerlendirmeye devam eder. Bu, UnboundLocalError'dan kaçınarak yapılandırma güncellemesinin sonraki çağrılarda devam etmesine izin verir. Bu, en iyi ani düzeltme olarak görülüyordu, ancak ekip daha sonra yapılandırmayı bir sınıf örneği haline getirmeyi planlıyordu. Mutasyon yaklaşımı mevcut API'yi korurken acil çöküşü çözmüştü.
Üçüncü çözüm uygulanarak yeniden atama işlemi CONFIG["batch_size"] = 500 şeklinde değiştirildi. Boru hattı hatasız bir şekilde çalışmaya devam etti ve yapılandırma değişikliği sonraki partilere doğru bir şekilde uygulandı. Daha sonra, yapılandırmayı fonksiyona enjekte edilen bir Pydantic ayar nesnesi kullanarak yeniden yapılandırdılar. Bu, modül seviyesi global değişkenlere olan bağımlılığı tamamen ortadan kaldırdı ve fonksiyonu saf ve test edilebilir hale getirdi. Olay, benzer gölgeleme kalıplarını ortadan kaldırmak için tüm Airflow operatörlerinde bir kod incelemesine yol açtı.
Bir fonksiyon içinde bir değişken üzerinde del ifadesi çalıştırıldığında, ardından onu okumaya çalışmak neden UnboundLocalError raise eder; global kapsam yerine?
Yerel bir değişken üzerinde del x çalıştırdığınızda, bu, çerçevenin f_locals'ından referansı kaldırır ancak x'in yerel olarak statik sınıflandırmasını değiştirmez. Derleyici, geri kalan okumalar için LOAD_FAST üretmeye devam etmiştir. Yorumlayıcı LOAD_FAST çalıştırdığında, slotun boş olduğunu gördüğü için UnboundLocalError raise eder, global değerlere geri dönmez. Yeniden silme sonrasında global x'e erişmek için, derleme zamanında global x tanımlamalısınız.
Varsayılan argüman ifadeleri UnboundLocalError tuzağından nasıl kaçınıyor, bu da değerlendirme zamanlamaları hakkında neyi ortaya çıkarıyor?
Varsayılan argümanlar, fonksiyon tanımı mevcut kapsamda yürütüldüğünde bir kerede değerlendirilir, fonksiyonun yerel kapsamı içinde değil. def f(val=CONFIG["key"]): yazdığınızda Python, CONFIG'i tanım zamanı boyunca LOAD_GLOBAL ile çözer. Fonksiyonun gövdesi daha sonra CONFIG'e atama yaparsa ve onu yerel hale getirirse, varsayılan değer zaten güvenli bir şekilde yakalanmıştır. Bu, varsayılanların tanım zamanı boyunca global kapsamı kullandığını ve fonksiyon gövdesinin yerel yürütmesinden ayrı olduğunu ortaya koyar. Böylece varsayılan değerler, fonksiyon gövdesinin atanmasından önce aynı erişim gerçekleşirse meydana gelebilecek UnboundLocalError'ı önler.
Neden UnboundLocalError sınıf gövdelerinde asla meydana gelmez ve bu hangi bayt kodu farkı bu durumu mümkün kılar?
Sınıf gövdeleri, değişken erişimi için LOAD_NAME kullanır. LOAD_NAME, sınıf sözlüğünde, ardından global sözlükte ve ardından, built-in'larda dinamik bir arama gerçekleştirir. Önceden tahsis edilmiş sabit bir slot kullanmadığı için asla "bağlı olmayan yerel" durumla karşılaşmaz. Eğer bir isim sınıf gövdesinde, atanmadan önce referans verilirse, LOAD_NAME basitçe onu global kapsamda bulmaya devam eder. Bu sözlük tabanlı yaklaşım, sınıf inşaatı sırasında gerekli esnekliği sağlarken, fonksiyon yerellerinin hızından feragat eder.