Python'ın import sistemi, döngüsel bağımlılıkları sys.modules içinde kısmen başlatılmış modülleri hemen önbelleğe alarak çözer. Bu mekanizma, A modülünün B'yi içe aktardığı ve B'nin aynı anda A'yı içe aktardığı durumlarda sonsuz geri çağrımı önler, ancak niteliklerin erişilemez olduğu bir pencere oluşturur.
Temel sorun, Python'ın yürütme modelinden kaynaklanmaktadır; bu model, modül ad alanlarını içe aktarma sırasında ardışık olarak doldurur. İki modül düşünün, burada module_a.py içinde import module_b var ve ardından def func(): pass ifadesi bulunuyor ve module_b.py module_a.func() çağrısı yapmaya çalışıyor; nitelik arama başarısız oluyor çünkü module_a sys.modules içinde mevcut ama func henüz bağlanmamış durumda.
# module_a.py import module_b # Yürütme burada durur, A önbelleğe alınmış ama boş def important_function(): return "kritik veri" # module_b.py import module_a # Raise AttributeError: module 'module_a' has no attribute 'important_function' result = module_a.important_function()
Çözüm, döngüleri ortadan kaldırmayı veya tembel değerlendirme desenlerini kullanmayı gerektirir. Geliştiriciler, içe aktarma ifadelerini fonksiyon tanımları içine alabilir, dinamik içe aktarım için importlib kullanabilir veya ortak bağımlılıkları her iki tarafın da içe aktardığı üçüncü bir modüle yapılandırabilir.
Bizim FastAPI mikroservisimiz, database.py (bağlantı havuzlarını içeren) ile models.py (SQLAlchemy ORM sınıflarını tanımlayan) arasında döngüsel içe aktarım sorunlarıyla karşılaştı. Veritabanı modülü, ilk şema ayarını gerçekleştirmek için modelleri içe aktardı, oysa modeller, tablo oluşturmak için veritabanından motoru içe aktardı; bu, uygulama başlatılırken ImportError'a neden oldu ve dağıtımı engelledi.
Üç farklı çözümü değerlendirdik. İçe aktarma ifadesini create_tables() fonksiyonu içine almak, anlık hatayı çözmesine rağmen, çalışma zamanı sırasında yeniden içe aktarma mantığını yürütmekle ilgili performans aşamasına ve bağımlılıkları gizleyerek kod okunabilirliğini azaltmaya neden oldu. Ortak bağımlılıkları içeren bir interfaces.py modülü oluşturarak, bağımlılık tersine çevirme yoluyla döngüyü kırdık, ancak bu önemli bir yeniden yapılandırma gerektirdi ve küçük bir hizmet için dolaylılık karmaşıklığını artırdı. Python'ın typing.Protocol kullanarak bir bağımlılık enjeksiyon kabı uygulamak, her iki modül yüklendikten sonra veritabanı motorunu kaydetmemize izin vererek, gerçek bağlantı kurma işlemini uygulama başlatılana kadar erteledi.
Bağımlılık enjeksiyonunu seçtik çünkü temiz mimari prensiplerini koruyarak performanstan ödün vermiyordu. Çözüm, tüm modüller başlatıldıktan sonra rota yöneticilerine veritabanı oturumunu enjekte etmek için FastAPI'nin Depends() mekanizmasını kullandı. Bu, döngüsel bağımlılığı ortadan kaldırarak, mock enjeksiyon yoluyla test edilebilirliği artırdı, başlangıç hatalarını %100 oranında azalttı ve entegrasyon testi kurulum süresini %60 oranında azalttı.
Neden if __name__ == "__main__" modül düzeyinde döngüsel içe aktarma hatalarını önlemiyor?
Bu koruma maddesi yalnızca ana betik bağlamında kod yürütmesini kontrol eder, içe aktarma mekanizmasını değil. Python, import module ile karşılaştığında, modül dosyasının tamamını yükleyip çalıştırır, bunu tamamladıktan sonra geri döner, mevcut herhangi bir __name__ kontrolüne bakılmaksızın. Döngüsel içe aktarma hatası, bu yükleme aşamasında meydana gelir, özellikle yorumlayıcı, kısmen inşa edilmiş ad alanındaki sembolleri çözmeye çalışırken, yani koruma ifadesinin yürütülmesi veya hatayı hafifletecek bir fırsatı yoktur.
from module import name ve import module arasında döngüsel bağımlılıkları çözümlemede ne fark var?
from ifadesi, modül nesnesi sys.modules'dan alındıktan sonra fakat modül tamamlanmadan önce hemen bir nitelik araması yapar. import module kullandığınızda, yorumlayıcı modül nesnesinin kendisine bir referans döner, böylece döngüsel içe aktarma zinciri tamamlana kadar bekletilmiş nitelik erişimine izin verir. Bu ayrım, import module sonrası module.name erişiminin başarılı olduğu, ancak from module import name ifadesinin başarısız olduğu durumları açıklar; çünkü nokta notasyonu erişim zamanında ad alanını yeniden değerlendirir, başlangıçta bağlama yapmaz.
Python 3.3+ ile birlikte ad alanı paketleri ve bunların döngüsel içe aktarma çözümüne etkisi nedir?
PEP 420, __init__.py dosyası olmayan örtük ad alanı paketlerini tanıtarak Python'ın modül nesnelerini içe aktarım sırasında nasıl oluşturduğunu değiştirmiştir. Geleneksel paketler __init__.py kodunu hemen yürütür, bu da net bir başlatma sınırı sağlarken, ad alanı paketleri yol girişlerinde farklı yükleme dizilerini tetikleyebilir. Adaylar sıklıkla, ad alanı paketlerini içeren döngüsel içe aktarmaların, aynı mantıksal modülü temsil eden birden fazla modül nesnesi (her bir yol girişi için bir tane) oluşturabileceğini unuturlar ve bu da farklı dosyalardaki içe aktarmaların, aynı içe aktarma ifadeleri olmasına rağmen, farklı modül örneklerini almasına neden olarak durum parçalanmasına yol açabilir.