PEP 810 – Açık Gecikmeli İçe Aktarma
(pep-previews--4622.org.readthedocs.build)- Python'da modül düzeyinde tüm import'ları bildirmek yaygın bir gelenektir
- Ancak program çalışırken gereksiz bağımlılık modülleri de hemen yüklenir ve bu da başlangıç hızı ile bellek kullanımı sorunlarına yol açar
- Daha önce fonksiyon içi import gibi yöntemlerle elle gecikmeli import yaygın olarak kullanılıyordu, ancak bunun bakım ve bağımlılık yönetimini zorlaştırma gibi dezavantajları vardı
- Bu PEP 810, local, explicit, controlled, granular niteliklerine sahip yeni
lazyanahtar sözcüğüyle açık gecikmeli içe aktarma söz dizimini tanıtıyor - Bu özellikle birlikte modüller yalnızca gerçekten gerektiğinde yüklenir; böylece başlangıç gecikmesi ve bellek israfı azaltılırken aynı anda kod yapısının şeffaflığı da korunur
Python import sisteminin mevcut durumu ve sorunları
- Python'da genel olarak import ifadelerini modülün en üstünde yazmak yaygın bir pratiktir
- Bu yaklaşım tekrarları azaltır, import bağımlılık yapısını tek bakışta görmeyi sağlar ve yalnızca bir kez import ederek çalışma zamanı ek yükünü en aza indirir
- Ancak program çalıştırıldığında ilk modül (main) yüklendiğinde, gerçekte kullanılmayan pek çok bağımlılık modülünün de hemen okunmasına yol açan zincirleme importlar kolayca oluşabilir
- Özellikle CLI araçlarında, yalnızca yardım çıktısı çağrılsa bile onlarca modülün önceden yüklenmesi gibi, her alt komut için gereksiz ek yük oluşur
Mevcut alternatifler ve sorunları
- Import'ları fonksiyon içine taşımak gibi yöntemlerle import zamanını elle geciktirme sık kullanılır
- Ancak bu yaklaşım; tutarlılığın ve bakım yapılabilirliğin düşmesi, toplam bağımlılıkları anlamanın zorlaşması gibi önemli dezavantajlar taşır
- Standart kütüphane analizi, performansa duyarlı kodlarda toplam import'ların yaklaşık %17'sinin zaten import geciktirme amacıyla fonksiyon veya metot içinde kullanıldığını gösteriyor
- Import geciktirme araçları olarak
importlib.util.LazyLoaderve üçüncü taraflazy_loaderpaketi gibi seçenekler bulunsa da, tüm durumları karşılamıyorlar veya tek bir standart bulunmuyor
PEP 810: açık gecikmeli içe aktarmanın eklenmesi
-
Yeni bir
lazysoft keyword tanıtılıyor (yalnızca belirli bağlamlarda anlam taşır, değişken adı olarak da kullanılabilir) -
lazyyalnızca import ifadesinin önünde kullanılabilir; fonksiyon/sınıf/with/try bloklarında veya star import ile kullanılamaz -
Her import ifadesi bazında açık biçimde ayrıştırılarak, kullanım anına kadar modül yüklemesi ertelenir
lazy import 모듈명 lazy from 모듈명 import 이름
Açık gecikmeli import'un çalışma biçimi ve sözdizimsel kuralları
-
Sözdizimi hatası oluşturan durumlar:
- Fonksiyon içinde, sınıf içinde, try/with içinde ve star import (
*) ile kullanımın tamamı geçersizdir
- Fonksiyon içinde, sınıf içinde, try/with içinde ve star import (
-
Kullanım örneği:
import sys lazy import json print('json' in sys.modules) # False (henüz yüklenmedi) result = json.dumps({"hello": "world"}) # ilk kullanımda yüklenir print('json' in sys.modules) # True (gecikmeli modül yükleme tamamlandı) -
Modül düzeyinde
__lazy_modules__özniteliğine string listesi vererek lazy hedefleri belirtmek mümkündür__lazy_modules__ = ["json"] import json # lazy olarak işlenir
Global bayraklar ve filtrelerle davranış kontrolü
-
Global bayraklar veya filtre fonksiyonları kullanılarak modül bazında ya da tüm sistemde lazy uygulaması kontrol edilebilir
-
Filtre fonksiyonu ile yalnızca belirli modüller için eager import istisnası tanımlanabilir
def my_filter(importer, name, fromlist): if name in {'problematic_module'}: return False # eager import return True # lazy import sys.set_lazy_imports_filter(my_filter)
Çalışma zamanı davranışı ve hata işleme
-
lazy import kullanıldığında gerçek import, import satırında değil isme ilk erişildiği anda gerçekleşir
-
Import başarısız olursa, exception chain (traceback chaining) sayesinde hem tanım yeri hem de hatanın ortaya çıktığı yer açıkça gösterilir
lazy from json import dumsp # yazım hatası result = dumsp({"key": "value"}) # gerçek erişim anında ImportError oluşur
Bellek ve performans avantajları
- Geciktirilmiş modüller yalnızca sys.lazy_modules kümesinde görünür; gerçek kullanım öncesinde sys.modules içine kaydedilmez
- Kullanım sonrasında normal modül nesnesiyle değiştirilir ve ek performans cezası olmadan kullanılabilir
- Gerçek iş yükü ortamlarında başlangıç gecikmesinde %50–70 azalma, bellekte %30–40 tasarruf sağlandığı görülmüştür
Çalışma şeklinin özeti
- lazy nesnesine ilk erişimde reification (gerçek import ve yer değiştirme) gerçekleşir
- Dış kod modülün
__dict__alanına erişirse tüm lazy nesneler zorla yüklenir (reification) globals()ile sözlük çıkarıldığında lazy proxy korunur; doğrudan erişim gerekir
Tip anotasyonları ve TYPE_CHECKING optimizasyonu
lazy from 모듈 import 이름ile yalnızca tip için kullanılan import'larda çalışma zamanı maliyeti SIFIR olur- Bu yaklaşım, mevcut
from typing import TYPE_CHECKINGkoşullu yapılarının yerini alarak kodu daha kısa ve daha açık hale getirir
Mevcut PEP 690'dan farkları ve uygulama özellikleri
- PEP 810, açık, tek tek import bazında, basit proxy nesnesi temelli bir opt-in yapısı sunar
- Buna karşılık PEP 690, global ve örtük lazy import yapısına sahipti
Dikkat edilmesi gerekenler ve modüller arası etkileşim
- star import (
*) lazy olarak desteklenmez (her zaman eager) - Özel import hook'ları ve loader'lar, reification zamanında aynı şekilde çalışır
- Çok iş parçacıklı ortamlarda da thread-safe biçimde yalnızca bir kez import ve güvenli bağlama garanti edilir
- Aynı modül için lazy ve eager birlikte kullanılırsa her zaman eager taraf önceliklidir
Kod uygulaması ve geçiş rehberi
- Mevcut kodda uygulama yapılırken, profiling ile gerçekten gerekli import'lar belirlenip yalnızca onlar lazy'ye çevrilmeli; kademeli geçiş önerilir
__lazy_modules__kullanılırsa Python 3.15 altı sürümlerle de uyumluluk sağlanır
Diğer önemli soru-cevap noktaları
- Import zamanı yan etkileri (ör. kayıt desenleri vb.) ilk erişime kadar ertelenir. Yan etki zorunluysa açık bir başlatma fonksiyonu deseni önerilir
- circular import (döngüsel import) sorunu lazy import ile tamamen çözülemez (yalnızca erişim gecikirse hafifleyebilir)
- Hot path performansı, ilk kullanımdan sonra lazy kontrolünün tamamen ortadan kalkmasıyla otomatik olarak optimize edilir (bytecode adaptive specialization)
- Gerçek modül yalnızca reification'dan (ilk kullanım) sonra
sys.modulesiçine kaydedilir importlib.util.LazyLoader'dan farklı olarak ek yapılandırma gerektirmez, performansı korur ve standart söz dizimiyle daha nettir
Sonuç
- PEP 810, Python import ifadelerine
lazyanahtar sözcüğünü ekleyerek; alt komutlu CLI'lar, büyük uygulamalar ve tip anotasyonları gibi alanlarda gereksiz modül yüklemesinden kaynaklanan performans sorunlarını kısa ve öngörülebilir biçimde optimize etmeyi mümkün kılar - Yeni anahtar sözcük, ne zaman ve neyin devreye gireceğini ayrıntılı biçimde belirlemeye izin verdiği için gerçek servislerde kademeli benimseme ve performans ayarı için uygundur
- Python import sisteminin pratik bir evrimi olarak görünürlük, bakım yapılabilirlik ve performans gereksinimlerini aynı anda karşılar
1 yorum
Hacker News görüşü
llm.datasette.io için yazdığım CLI aracı eklenti destekliyor, ancak
llm --helpgibi komutlarda bile başlangıç süresinin çok yavaş olduğuna dair çok şikayet alıyordum; inceleyince popüler eklentilerin varsayılan olarak pytorch gibi ağır paketleri import ettiği ve bunun tüm başlangıcı bloke ettiği ortaya çıktı, bu yüzden eklenti yazarı dokümantasyonuna bağımlılıkların yalnızca gerektiğinde, fonksiyon içinde import edilmesini öneren bir not ekledim (ilgili doküman bağlantısı), ama bunun Python dil seviyesinde desteklenmesi çok daha iyi olurdu diye düşünüyorumBu özellik bugün bile araçlarla uygulanabilir (açıklama bağlantısı), ancak bu yaklaşım süreç genelinde global olarak uygulandığı için numpy’yi gecikmeli import ederseniz alt modül importları da gecikiyor; sonuçta numpy’nin tamamına ihtiyaç yoksa hiç import edilmeyebilir ama ihtiyaç anında modüllerin parça parça yüklenmesi, bu gecikmeyi çalışma zamanı boyunca öngörülemez biçimde dağıtabilir; ek deneylerde
import foo.bar.bazşeklinde import edildiğinde foo ve foo.bar’ın yine hemen yüklendiğini, yalnızca foo.bar.baz’ın gecikmeli kaldığını gördüm; sanırım PEP’teki "mostly" ifadesinin nedenlerinden biri bu, uygulamamı daha da geliştirirsem bunu çözebilirim gibi geliyorÖnce komut satırını parse edip
--helpgibi seçenekleri import yapmadan işleme yöntemini öneririm; importları yalnızca gerçekten gerektiğinde yapın, ya da daha basit söylemek gerekirse kolay komut seçenekleri işlendikten ve hâlâ yapılacak iş kaldıktan sonra import edecek şekilde tasarlayınLazy import önerileri geçmişte de vardı, en sonuncusu da 2022’de reddedildi (ilgili tartışma bağlantısı); hatırladığım kadarıyla lazy import, Meta’nın CPython varyantı Cinder’da zaten mevcut ve bu PEP’i de Cinder üzerinde çalışan kişiler yönlendiriyor; tartışmanın odağı “opt-in mi opt-out mu?”, “kapsam tam olarak ne olacak?”, “CPython build flag’i olarak mı gelmeli?” gibi konulardı; sonuçta Steering Council, import davranışının ikiye ayrılmasının yaratacağı karmaşıklık nedeniyle bunu reddetti; bu teklifin bu kez mutlaka geçmesini umuyorum, bu özelliği gerçekten kullanmak istiyorum
Özellikle opt-in olması, ince taneli seviyelerde uygulanabilmesi ve global bir kapatma anahtarı içermesi hoşuma gidiyor; pek çok kısıt içinde çok iyi kurgulanmış bir spesifikasyon
Ben de bu teklifin geçmesini isterim ama pek iyimser değilim; bu, sayısız kodu bozacak ve beklenmedik sorunlar doğuracak;
importifadesi doğası gereği yan etkilere sahip ve bunun ne zaman çalıştığı değişirse insanlar uzun süre sebebini bulamadıkları hatalarla uğraşacak; bu korku tellallığı değil, gerçek gerekçeleri olan bir endişe; lazy import’un yalnızca Meta’da bulunmasının da bir sebebi var—çünkü bunu yönetmek için Meta kadar kaynağa sahip olmak gerekebilir; birçok kişi sadece “pandas, numpy ya da karmakarışık weird module’üm çok yavaş, hızlansa iyi olur” tarafına bakıyor ama bence Python’un import sisteminin gerçekten nasıl çalıştığını bilen insan sayısı çok az; hatta lazy import’un nasıl uygulanacağını bilmeden destekleyen görüşler bile var; PEP 690’a bakarsanız bir sürü dezavantaj var—örneğin decorator kullanarak fonksiyonları merkezi bir registry’ye ekleyen kodlar bozuluyor; Dash kütüphanesi buna tipik bir örnek, JavaScript tabanlı arayüzle Python callback’lerini import anında decorator’larla birbirine bağlıyor, import lazy olursa bu tür frontend’ler tamamen çöker; çok sayıda kullanıcısı olan servisler de anında bozulabilir; “opt-in sonuçta, uymuyorsa lazy import’u kapatırsın” deniyor ama ya import geçişkense (transitive)? Ya frontend tamamen initialize edildikten sonra kritik bir süreci başlatmak gerekiyorsa? Birden fazla kişinin kodu ve kütüphanelerinin iç içe geçtiği bir ekosistemde etkilerin ne olacağını kim bilebilir? Type hint’lerden farklı olarak bu, çalışma zamanı davranışını gerçek anlamda değiştiren bir değişiklik;importifadesi neredeyse tüm gerçek Python kodlarında var, dolayısıyla lazy geldiğinde çalışma modeli kökten değişmiş oluyor; ayrıca PEP’in değindiği başka tuhaf durumlar da var; sorun göründüğünden çok daha zorimport torch==2.6.0+cu124,import numpy>=1.2.6gibi sürüm belirterek import yapabilmek ve aynı Python ortamında bir paketin birden fazla sürümünü eşzamanlı kurup import edebilmek harika olurdu; artık conda/virtualenv/docker/bazel cehenneminin bitmesini istiyorumPek karşı değilim ama aşırı da hevesli değilim; bu haliyle neredeyse her
importönünelazyekleyecekmişiz gibi duruyor, birkaç gerçekten eager import edilmesi gereken durum dışında geriye kalan her yerelazyyazınca kod dağınık hale geliyor; üstelik bunun varsayılan davranış olması gibi bir plan da yok, yani bu kalabalık sonsuza kadar kalacak; ben modül tarafının lazy loading’e opt-in olduğunu ilan ettiği, amaimportsözdiziminin değişmediği bir sistemi tercih ederdim; o zaman yalnızca büyük kütüphanelerin tembellik davranışını düşünmesi gerekirdi; tabii bunun da yorumlayıcının import anında dosya sistemini taraması gibi başka dezavantajları olurduHerkes ciddi bir sorun yaşamadan lazy import’u yoğun biçimde kullanabiliyorsa bu, aslında lazy’nin varsayılan olması ve <i>eager</i>’ın isteğe bağlı anahtar sözcük olması gerektiği anlamına gelir; bu tür paradigma değişimleri Python’da ilk kez olmuyor, v2’de eager şekilde liste üreten pek çok yapı v3’te generator’a döndü ve ciddi bir sorun çıkmadı
Python genelindeki tüm modül importlarını lazy yapan bir komut satırı bayrağı olsa kesin kullanırım; pratikte script’ler ya da gerçekten çok basit kodlar dışında modül yükleme sırasında yan etki üretmek kaçınılması gereken bir desen
Lazy loading kararını modül tarafının vermesi doğru değil bence; lazy yüklemeye ihtiyaç olup olmadığını yalnızca çağıran taraf bilir, dolayısıyla seçeneğin import eden kod tarafında olması daha mantıklı; her modül lazy load edilebilir ve yan etkileri olsa bile çağıran taraf onları da ertelemek isteyebilir
pyproject.toml içinde regex ile lazy loading seçenekleri belirtilebilse güzel olurdu
Geçmişte type hint, walrus, asyncio, dataclasses gibi yeni özellikler geldiğinde de insanlar benzer kaygılar dile getirmişti ama pratikte çok büyük kitleler topluca bunları kullanmaya başlamadı ya da tüm mevcut desenleri değiştirmedi; birçok kullanıcı hâlâ modernleştirilmiş Python 2.4 seviyesindeki özelliklerle çalışıyor ve bu da yeterince üretken olabiliyor; 20 yıldır işler yürüyor, büyük bir sorun çıkacağını sanmıyorum
İlgilenenler için, context manager biçiminde çok kullanışlı lazy import sağlayan lazyimp adlı aracı önerebilirim; genelde
importifadesini birwithbloğuna sarmanız yetiyor, böylece mevcut araçlarla da iyi çalışıyor ve debug gerektiğinde kolayca eager import’a dönebiliyorsunuz; cext ile frame’inf_builtinsalanını değiştirerek importlib hook’undan daha güçlü hale geliyor; mükemmel değil ama thread-safe sürümü ve global handler sürümü de var; başta temkinliydim ama şimdi kod tabanımın neredeyse tamamını buna taşıdım ve modül bazlı kayıt işlemlerini atladığım durumlar dışında gerçek bir sorun yaşamadım, hız farkı da çok büyük olduğu için memnunumPython lint araçlarının importları dosyanın en üstünde tutmaya zorlaması gerçekten can sıkıcı; bariz lazy import yöntemlerini kullandığımda sürekli lint hatası alıyorum; bu yalnızca bir performans meselesi de değil, örneğin platforma özel bir kütüphane gerektiğinde onu sadece o platformda import etmek isteyebilirsiniz ama en üstte import zorunluluğu varsa import baştan başarısız olabilir
Böyle durumlarda linter’ı düzeltmekten başka çare yok bence
Çoğu linter bunu
#noqa E402gibi bir yorumla yok saymanıza izin veriyorBu şekilde meta path finder’ı saran bir wrapper ile değiştirip loader’ı LazyLoader ile ikame ediyorsunuz;
importçalıştığında modül adı gerçekte<class 'importlib.util._LazyModule'>olarak bağlanıyor, özelliklere erişildiği anda da gerçek modül yükleniyor; deney kodu:Ama PEP’teki "mostly" ifadesinin tam olarak neyi kastettiğini bilmiyorum
Lazy import konusunda thread safety risklerinin yeterince ciddiye alınmadığını düşünüyorum; importun ne zaman, hangi thread’de, hangi lock alınarak çalışacağını öngörmek mümkün değil ve importer lock dışında pek bir garanti yok; eskiden modül importu sırasında riskli kodlar çalışsa bile bunlar çoğunlukla tek thread’li initialization aşamasında olurdu, o yüzden büyük problem çıkmazdı; lazy olunca hatalar gerçekten öngörülemez biçimde, tam anlamıyla Heisenbug olarak ortaya çıkabilir; fonksiyon seviyesi importlarda da benzer risk ihtimali var ama en azından açıkça yazılmış kodun başında çalışacağı tahmin edilebiliyor
İyi bir özellik gibi görünüyor; anlatımı kolay, gerçek kullanım örnekleri var ve kapsamı da (global kullanım, basit anahtar sözcük yaklaşımı) uygun; hoşuma gitti
Son dönemde çıkan PEP’ler içinde kullanıcı açısından en temiz olanlardan biri gibi hissettiriyor; bu geleneksel syntax bikeshedding sürecinden sonra ortaya ne çıkacağını merakla bekliyorum
Gerçek dünya ve edge case örnekleriyle sınanmış, doğru uzlaşmaları yapan, aşırıya kaçmayan ve birkaç tur olgunlaştırılmış bir PEP gibi duruyor; özellikle dünya çapında birbirinden çok farklı toplulukları olan büyük bir dilin kemiğini oluşturan sisteme dokunmanın ne kadar riskli olduğu düşünülürse bu hazırlık etkileyici
Umarım PEP-690’ın neden reddedildiğinden yeterince ders çıkarılmıştır; biz de kod tabanımızda buna benzer bir özelliği kendimiz uygulamaya çalıştık ama hiçbir zaman gerçekten kullanılabilir düzeyde iyi çalıştıramadık
Lazy import’un uzun süre çalışan servislerde beklenmedik çalışma zamanı hatalarına yol açabilmesi riskli; hızlı başlangıç cazip görünse de bunun bedeli, kod çalışırken bir noktada import hatası yüzünden sistemin durabilmesi olabilir; ayrıca program başlarken ileride neyin import edileceğini öngöremediğiniz bazı edge case’ler de çıkabilir
Yine de bu mutlaka çözülmesi gereken gerçek bir problem; mesele sadece başlangıç hızı değil, büyük bağımlılıklar girince Python başlangıcı absürt derecede yavaşlayabiliyor; büyük projeler tüm kullanıcıların ihtiyaç duymadığı ağır kütüphaneleri paketleyip getiremez, bu yüzden geliştiriciler zaten daha da tuhaf geçici çözümler kullanıyor ve bu da ayrı saçma sorunlar üretiyor; yalnızca fonksiyon seviyesindeki importları tekrar tekrar saklama ve gizleme zahmetini ortadan kaldırsa bile büyük ilerleme olur; ayrıca bu en baştan isteğe bağlı bir dil özelliği olarak öneriliyor
Otomatik testlerle risk yeterince azaltılabilir; hızlı başlangıç için buna değer; başlangıç süresi asla sadece “görüntü” meselesi değil; ben Django monolitinde bunun yüzünden, sadece birkaç ağır kütüphane nedeniyle her management command, test ve container reload’da 10-15 saniye beklemek zorunda kalmıştım; lazy import ile erteleyince fark inanılmaz oldu
Biz açık ve dosyanın en üstünde duran importları tercih ediyoruz, çünkü bağımlılık sorunlarının program başlarken hemen ortaya çıkmasını istiyoruz; lazy import kullanınca bazı problemleri ancak belirli bir kod yolu çalıştığında—belki saatler ya da günler sonra—fark etme gibi can sıkıcı bir durum oluşuyor
pip’in çalışma hızı hemen artardıSürenin çoğu gerçekte hiç kullanılmayan vendor modüllerini import edip sonra boşaltmaya gidiyor (örneğin sadece Requests ile ilişkili neredeyse 100 modül); incelediğimde toplamda 500’den fazla modülün gereksiz yere import edildiğini gördüm
Kod üreticilerinin neden dosya başı importlar yerine fonksiyon içine local import koyan kodlar üretmeye başladığını bilmiyorum; bu deseni teşvik etmek istemem, çünkü modülün bağımlılıklarını anlamayı zorlaştırıyor ve ileride cyclic dependency riskini artırıyor
PEP’in tamamını henüz okumadım ama komut satırı bayrağı ya da harici araçlarla bir tür dependency validation yapılabilse güzel olurdu diye düşünüyorum; type hint’ler için olan araçlar gibi
“Biz” tam olarak kimi ifade ediyor, merak ettim
Bu, testlerle kapsanması gereken bir konu değil mi?