Python betiğiniz API yanıtlarını, veritabanı sorgularını veya dosya işlemlerini beklerken, bu süre çoğu zaman boşa gider. Python async programlama sayesinde kodunuz aynı anda birden fazla görevi ele alabilir. Böylece bir işlem beklerken diğerleri ilerler; atıl anlar üretken çalışmaya dönüşür ve dakikalar süren beklemeleri çoğu zaman saniyelere indirir.
Bu rehberde, mini projeler üzerinden ilerleyerek Python'da async programlamanın temelini öğreteceğim. Coroutine'lerin, olay döngüsünün (event loop) ve async I/O'nun kodunuzu nasıl çok daha duyarlı hale getirdiğini göreceksiniz.
Eşzamansız web API'leri oluşturmayı öğrenmek istiyorsanız, FastAPI üzerindeki bu kursa mutlaka göz atın.
Python Async Programlama Nedir?
Geleneksel eşzamanlı (senkron) Python'da kodunuz satır satır çalışır. Örneğin bir API çağırdığınızda, programınız yanıtı bekler. Bu iki saniye sürerse, programınızın tamamı iki saniye boyunca boşta kalır. Eşzamansız programlama, kodunuzun bir API çağrısı başlatıp ardından diğer görevlerle devam etmesine olanak tanır.
Yanıt geldiğinde, kodunuz kaldığı yerden devam eder. Her işlemin bitmesini beklemek yerine, birden fazla işlemi aynı anda yürütebilirsiniz. Bu, kodunuzun veritabanları, API'ler veya dosya sistemleri gibi harici sistemlerden yanıt beklediği durumlarda en çok fark yaratır.
Bunun çalışması için Python'un async sistemi birkaç temel kavram kullanır:
Coroutine'ler: def yerine async def ile tanımlanan fonksiyonlar. Yürütmeyi duraklatıp sürdürebilirler; bu da bekleme içeren işlemler için idealdir.
await: Python'a şunu söyler: "Bu coroutine'i, bu işlem tamamlanana kadar duraklat; bu arada başka kodlar çalışabilsin."
Olay döngüsü (event loop): Tüm coroutine'lerinizi yöneten ve hangisinin ne zaman çalışacağına karar veren motor.
Görevler (tasks): Eşzamanlı yürütme için sarmalanmış coroutine'ler. Aynı anda birden fazla işlemi çalıştırmak için asyncio.create_task() ile oluşturursunuz.
Async programlamanın neleri yapabildiği (ve yapamadığı) konusunun karışmaması için şunları aklınızda tutun:
Async, I/O ağırlıklı işlerde en iyi sonucu verir — HTTP istekleri, veritabanı sorguları ve dosya işlemleri gibi kodunuzun harici sistemleri beklediği durumlarda.
Async, CPU ağırlıklı işlere yardımcı olmaz — karmaşık hesaplamalar veya veri işleme gibi kodunuzun aktif olarak hesap yaptığı durumlarda değil.
Bu kavramları içselleştirmenin en iyi yolu gerçek async kodu yazmaktır. Sıradaki bölümde, ilk async fonksiyonunuzu oluşturacak ve coroutine'lerin olay döngüsüyle nasıl birlikte çalıştığını tam olarak göreceksiniz.
İlk Python Async Fonksiyonunuz
Async koda geçmeden önce, bir şey yapmadan önce bekleyen normal bir senkron fonksiyona bakalım:
import time
defgreet_after_delay():print("Starting...")
time.sleep(2)# Blocks for 2 secondsprint("Hello!")
greet_after_delay()Altyapı:
Çıktı aynı görünür, ancak perde arkasında farklı bir şey olur. Üç değişiklik bunu async yaptı:
def yerine async def ile coroutine olarak bildirildi.
time.sleep(2) yerine await asyncio.sleep(2) ile bloklamadan duraklatıldı.
asyncio.run() olay döngüsünü başlatıp coroutine'i çalıştırdı.
asyncio.sleep()'in kendisinin de bir async fonksiyon olduğuna ve bu yüzden await gerektirdiğine dikkat edin. Bu kilit bir kuraldır: her async fonksiyon await ile çağrılmalıdır. İster asyncio.sleep() gibi yerleşik bir fonksiyon olsun, ister sizin yazdığınız bir fonksiyon; await'i unutursanız gerçekte çalışmaz.
Şu an için async sürüm daha hızlı görünmüyor. Bunun nedeni yalnızca bir görevimizin olması. Asıl fayda, aynı anda birden çok coroutine çalıştırdığınızda ortaya çıkar; bunu bir sonraki bölümde ele alacağız.
Bilmeniz gereken bir başka önemli şey: async bir fonksiyonu, normal bir fonksiyon gibi doğrudan çağıramazsınız. Deneyelim:
result = greet_after_delay()print(result)print(type(result))Altyapı:
<coroutine object greet_after_delay at 0x...>
<class 'coroutine'>Altyapı:
greet_after_delay() çağrısı, sonucu değil bir coroutine nesnesi döndürür. Fonksiyon aslında çalışmaz. Bunu yürütmek için asyncio.run() veya başka bir fonksiyon içinde await kullanmanız gerekir.
Olay döngüsü nasıl çalışır
Olay döngüsü async programlamanın motorudur. Coroutine'lerinizi yönetir ve neyin ne zaman çalışacağına karar verir. Async greet_after_delay() fonksiyonunu çalıştırdığınızda adım adım olanlar:
asyncio.run() bir olay döngüsü oluşturur.
Olay döngüsü greet_after_delay()'i başlatır.
"Starting..." yazdırılır.
await asyncio.sleep(2) satırına gelir → coroutine duraklar.
Olay döngüsü kontrol eder: "Çalışacak başka görev var mı?" (şimdilik yok).
2 saniye geçer, sleep tamamlanır.
Olay döngüsü greet_after_delay()'i sürdürür.
"Hello!" yazdırılır.
Fonksiyon biter → olay döngüsü çıkar.
Async'i ilginç kılan kısım 5. adımdır. Tek bir coroutine ile yapılacak başka bir şey yoktur. Ancak birden fazla coroutine olduğunda, olay döngüsü biri beklerken diğer işlere geçer. İki saniyelik bir sleep sırasında boşta kalmak yerine başka kodları çalıştırabilir.
Olay döngüsünü bir trafik yöneticisi gibi düşünün. Tek tek arabaları hızlandırmaz. Biri durduğunda, diğerlerine yol vererek akışı sürdürür.
Yaygın async hatası: await'i unutmak
Acemilerin sık yaptığı bir hata, başka bir async fonksiyon içinden coroutine çağırırken await'i unutmak:
Bir coroutine'in beklenmediğine dair bir RuntimeWarning gördüğünüzde, her async fonksiyon çağrısında await kullandığınızı kontrol edin.
Eşzamanlı Python Async Görevleri
Önceki bölümde senkron bir fonksiyonu async'e çevirdik. Ancak daha hızlı değildi. Bunun nedeni yalnızca bir coroutine çalıştırmış olmamız. Async'in gerçek gücü, birden fazla coroutine'i aynı anda çalıştırdığınızda ortaya çıkar.
Ardışık await neden hâlâ ardışık çalışır
Birden çok async fonksiyon çağırmanın onları otomatik olarak eşzamanlı çalıştıracağını düşünebilirsiniz. Ancak greet_after_delay()'i üç kez çağırdığımızda ne olduğuna bakın:
İki saniyelik üç görev için altı saniye. Her await, bir sonraki satıra geçmeden önce ilgili coroutine'in bitmesini bekler. Kod async olsa da ardışık çalışır.
asyncio.gather() kullanarak görevleri eşzamanlı çalıştırma
Coroutine'leri aynı anda çalıştırmak için asyncio.gather() kullanın. Birden fazla coroutine alır ve bunları eşzamanlı yürütür:
Altı yerine iki saniye. Üç coroutine de anında başladı, eşzamanlı olarak uyudu ve birlikte tamamlandı. Tek bir değişiklikle 3 kat hızlanma.
Çıktı sırasına dikkat edin: tüm "Starting..." mesajları, herhangi bir "Hello..." mesajından önce yazdırılıyor. Bu, coroutine'lerin birbirini beklemek yerine aynı iki saniyelik zaman penceresinde çalıştığını gösterir.
asyncio.gather(), sonuçları, coroutine'leri hangi sırayla verdiyseniz o sırada bir liste halinde döndürür. Coroutine'leriniz değer döndürüyorsa, bunları yakalayabilirsiniz:
Sonuçlar, gather()'a verilen coroutine'lerin sırasına uygun olarak [10, 20, 30] şeklinde döner.
aiohttp ile Python Async HTTP İstekleri
Şimdiye dek gecikmeleri simüle etmek için asyncio.sleep() kullandık. Şimdi gerçek HTTP istekleri yapalım. requests kütüphanesine yönelebilirsiniz, ancak burada işe yaramaz. requests senkron çalışır ve olay döngüsünü bloklar; async'in amacını boşa çıkarır.
Bunun yerine, bu amaç için geliştirilmiş bir async HTTP istemcisi olan aiohttp'u kullanın.
İç içe iki async with bloğuna dikkat edin. Her biri farklı bir kaynağı yönetir ve bunların ne yaptığını anlamak aiohttp'yu doğru kullanmanın anahtarıdır.
aiohttp'da ClientSession nasıl çalışır
aiohttp ile istek yaptığınızda adım adım olanlar:
aiohttp.ClientSession() bir bağlantı havuzu oluşturur (başta boştur).
session.get(url) havuzu kontrol eder: "Bu host'a (web sitesinin sunucusu) açık bir bağlantı var mı?"
Bir HTTP isteği gönderilir ve yanıt başlıklarını bekleriz.
Yanıt nesnesi bağlantıyı tutar.
await response.text() gövde verisini ağdan okur.
İçteki async with bloğundan çıkış: Bağlantı havuza geri döner (açık kalır!).
Aynı host'a yapılacak bir sonraki istek, havuzdaki bağlantıyı yeniden kullanır (3. adımı atlar).
Dıştaki async with bloğundan çıkış: Havuzdaki tüm bağlantılar kapanır.
7 ve 8. adımlar kilit önemdedir. Bağlantı havuzu, istekler arasında bağlantıları canlı tutar. Aynı host'a bir istek daha yaptığınızda, TCP ve SSL el sıkışması tamamen atlanır.
Bu önemlidir çünkü yeni bir bağlantı kurmak yavaştır. Bir TCP el sıkışması sunucuya gidiş-dönüş gerektirir. Bir SSL el sıkışması iki tane daha. Gecikmeye bağlı olarak, ilk baytı göndermeden önce 100-300 ms anlamına gelebilir.
Tüm istekler için paylaşılan bir oturum kullanma
Şimdi her istek için yeni bir oturum oluşturmanın neden sorun olduğunu görebilirsiniz:
# Wrong: new session for each requestasyncdeffetch_bad(url):asyncwith aiohttp.ClientSession()as session:asyncwith session.get(url)as response:returnawait response.text()asyncdefmain():
urls =["https://example.com"]*10
results =await asyncio.gather(*[fetch_bad(url)for url in urls])Altyapı:
fetch_bad()'in her çağrısı, boş bir havuzla yeni bir oturum oluşturur. Tüm istekler aynı host'a gitse bile her istek tam el sıkışma maliyetini öder.
Çözüm, tek bir oturum oluşturup fetch fonksiyonunuza geçirmektir:
# Right: reuse a single sessionasyncdeffetch_good(session, url):asyncwith session.get(url)as response:returnawait response.text()asyncdefmain():
urls =["https://example.com"]*10asyncwith aiohttp.ClientSession()as session:
results =await asyncio.gather(*[fetch_good(session, url)for url in urls])Altyapı:
Paylaşılan bir oturumla, ilk istek bağlantıyı kurar; kalan dokuz istek onu yeniden kullanır. On el sıkışma yerine bir.
Async HTTP isteği örneği: Hacker News kazıma
Bunu Hacker News API ile uygulayalım. Bu API, hikâyeleri çekmek için birden fazla istek gerektirdiğinden eşzamansız davranışı göstermek için idealdir. Python'da REST API'lerle çalışmaya yeniyseniz, temel kavramlar için Python API'leri: API Oluşturma ve Kullanma Rehberi'ne göz atın.
Hacker News API yapısı:
https://hacker-news.firebaseio.com/v0/topstories.json hikâye ID'lerinin bir listesini (sadece sayılar) döndürür
https://hacker-news.firebaseio.com/v0/item/{id}.json tek bir hikâye için ayrıntıları döndürür
10 hikâye almak için 11 istek gerekir: biri ID listesi için, ardından her hikâye için birer tane. Async programlamanın parladığı nokta tam da burası.
Önce, ilk hikâyeyi çekmeyi denersek API ne döndürüyor görelim:
import aiohttp
import asyncio
HN_API ="https://hacker-news.firebaseio.com/v0"asyncdefmain():asyncwith aiohttp.ClientSession()as session:# Get top story IDsasyncwith session.get(f"{HN_API}/topstories.json")as response:
story_ids =await response.json()print(f"Found {len(story_ids)} stories")print(f"First 5 IDs: {story_ids[:5]}")# Fetch first story details
first_id = story_ids[0]asyncwith session.get(f"{HN_API}/item/{first_id}.json")as response:
story =await response.json()print(f"\nStory structure:")for key, value in story.items():print(f" {key}: {repr(value)[:50]}")
asyncio.run(main())Altyapı:
Found 500 stories
First 5 IDs: [46051449, 46055298, 46021577, 46053566, 45984864]
Story structure:
by: 'mikeayles'
descendants: 22
id: 46051449
kids: [46054027, 46053889, 46053275, 46053515, 46053002,
score: 217
text: 'I got DOOM running in KiCad by rendering it with
time: 1764108815
title: 'Show HN: KiDoom – Running DOOM on PCB Traces'
type: 'story'
url: 'https://www.mikeayles.com/#kidoom'Altyapı:
API 500 hikâye ID'si döndürür ve her hikâyede title, url, score ve by (yazar) gibi alanlar bulunur.
Sequential: Fetched 10 stories in 2.41 secondsAltyapı:
Şimdi aynı hikâyeleri eşzamanlı olarak çekelim:
asyncdefmain():asyncwith aiohttp.ClientSession()as session:asyncwith session.get(f"{HN_API}/topstories.json")as response:
story_ids =await response.json()
start = time.perf_counter()
tasks =[fetch_story(session, story_id)for story_id in story_ids[:10]]
stories =await asyncio.gather(*tasks)
elapsed = time.perf_counter()- start
print(f"Concurrent: Fetched {len(stories)} stories in {elapsed:.2f} seconds")print("\nTop 3 stories:")for story in stories[:3]:print(f" - {story.get('title','No title')}")
asyncio.run(main())Altyapı:
Concurrent: Fetched 10 stories in 0.69 seconds
Top 3 stories:
- Show HN: KiDoom – Running DOOM on PCB Traces
- AWS is 10x slower than a dedicated server for the same price [video]
- Surprisingly, Emacs on Android is pretty goodAltyapı:
Eşzamanlı sürüm 3,5 kat daha hızlı. Her isteğin bitmesini bekleyip sonra yenisini başlatmak yerine, 10 isteğin tamamı aynı anda çalışır. Async programlama gerçek ağ I/O'sunda burada karşılığını verir.
Python Async Hata Yönetimi ve Oran Sınırlama
Verileri eşzamanlı çekerken birkaç şey ters gidebilir. Çok fazla istekle sunucuyu bunaltabilirsiniz. Bazı istekler sonsuza kadar asılı kalabilir. Bazıları ise doğrudan başarısız olabilir. Ve hatalar olduğunda, bir kurtarma stratejisine ihtiyacınız vardır.
Bu bölüm, isteklerin gönderimini kontrol etmekten zaman sınırları koymaya, hataları ele almaya ve uygun olduğunda yeniden denemeye kadar endişeleri oluş sırasıyla ele alır. Python'da istisna yönetiminin temellerine tazeleme gerekiyorsa, Python'da İstisna ve Hata Yönetimi'ne bakın. Aşağıdaki temel kurulum boyunca bunu kullanacağız:
Önceki bölümde 10 isteği birden gönderdik. Bu sorun olmadı. Peki 500 hikâye çekmeniz gerekirse ne olur? Ya da 10.000 sayfa kazımanız?
Çoğu API, oran sınırlaması uygular. Saniyede 10 isteğe veya 100 eşzamanlı bağlantıya izin verebilirler. Bu sınırları aşarsanız engellenir, yavaşlatılır veya yasaklanırsınız. API sınır dayatmasa bile, binlerce isteği aynı anda göndermek kendi sisteminizi veya sunucuyu zorlayabilir.
Her an "uçuşta" (in flight) olan istek sayısını kontrol etmenin bir yoluna ihtiyacınız var. Semaphore tam da bunu yapar.
Semaphore, izin sistemi gibi çalışır. Üç izniniz olduğunu hayal edin. İstek yapmak isteyen her görev önce bir izin almalıdır. Bittiğinde izni iade eder, böylece yeni bir istek onu kullanabilir. İzin yoksa, görev biri serbest kalana kadar bekler.
3 izin ve 4 veya daha fazla görevle nasıl işler:
Üç izin mevcuttur.
A Görevi bir izin alır (2 kaldı), isteğine başlar.
B Görevi bir izin alır (1 kaldı), isteğine başlar.
C Görevi bir izin alır (0 kaldı), isteğine başlar.
D Görevi izin ister, ancak yoktur—bekler.
A Görevi biter, iznini iade eder (1 mevcut).
D Görevi o izni alır ve isteğine başlar.
Tüm görevler bitene kadar bu şekilde devam eder.
5. adımdaki bekleme verimlidir. Görev "izin boşaldı mı?" diye dönüp durmaz. Duraklar ve başka koda çalışması için izin verir. Olay döngüsü, bir izin kullanılabilir olduğunda onu uyandırır.
Koda bakalım. asyncio'da asyncio.Semaphore(n) ile bir semaphore oluşturursunuz; n izin sayısıdır. Kullanmak için kodunuzu async with semaphore: içine sarın. Bloğa girerken bir izin alınır, çıkarken otomatik olarak serbest bırakılır:
asyncdeffetch_story_limited(session, story_id, semaphore):asyncwith semaphore:# Acquire permit (or wait if none available)asyncwith session.get(f"{HN_API}/item/{story_id}.json")as response:returnawait response.json()# Permit automatically released hereAltyapı:
30 hikâyeyi semaphore ile ve semaphoresuz çekmeyi karşılaştıralım:
asyncdefmain():asyncwith aiohttp.ClientSession()as session:asyncwith session.get(f"{HN_API}/topstories.json")as response:
story_ids =(await response.json())[:30]# Without rate limiting: all 30 at once
start = time.perf_counter()await asyncio.gather(*[fetch_story(session, sid)for sid in story_ids])print(f"No limit: {time.perf_counter()- start:.2f}s (30 concurrent)")# With Semaphore(5): max 5 at a time
semaphore = asyncio.Semaphore(5)
start = time.perf_counter()await asyncio.gather(*[fetch_story_limited(session, sid, semaphore)for sid in story_ids])print(f"Semaphore(5): {time.perf_counter()- start:.2f}s (5 concurrent)")
asyncio.run(main())Altyapı:
No limit: 0.62s (30 concurrent)
Semaphore(5): 1.50s (5 concurrent)Altyapı:
Semaphore sürümü daha yavaştır çünkü istekleri beşerli partiler halinde işler. Ancak bu bir ödünleşimdir: hızdan feragat edip öngörülebilir ve sunucu dostu davranış kazanırsınız.
Unutmayın: semaphore, zaman birimi başına istekleri değil, eşzamanlı istek sayısını sınırlar. Semaphore(10) "aynı anda en fazla 10 istek" demektir, "saniyede 10 istek" değil. Kesin zaman tabanlı oran sınırlaması (ör. tam olarak saniyede 10 istek) gerekiyorsa, semaphore'u partiler arası gecikmelerle birleştirebilir veya aiolimiter gibi bir kütüphane kullanabilirsiniz.
asyncio.wait_for() ile zaman aşımları
Eşzamanlılık kontrolünde olsanız bile, tekil istekler takılabilir. Sunucu bağlantınızı kabul eder ama hiç yanıt vermez. Zaman aşımı olmadan programınız süresiz bekler.
asyncio.wait_for() herhangi bir coroutine'i bir son tarih ile sarar. Coroutine'i ve saniye cinsinden bir timeout verirsiniz. İşlem zamanında tamamlanmazsa asyncio.TimeoutError fırlatır:
asyncdefslow_operation():print("Starting slow operation...")await asyncio.sleep(5)return"Done"asyncdefmain():try:
result =await asyncio.wait_for(slow_operation(), timeout=2.0)print(f"Success: {result}")except asyncio.TimeoutError:print("Operation timed out after 2 seconds")
asyncio.run(main())Altyapı:
Starting slow operation...
Operation timed out after 2 secondsAltyapı:
Zaman aşımı dolduğunda wait_for() coroutine'i iptal eder. TimeoutError'ı yakalayıp ne yapacağınıza karar verebilirsiniz: isteği atlamak, varsayılan bir değer döndürmek veya yeniden denemek.
Eşzamanlı isteklerde, her birini ayrı ayrı sarmalayın. İşte hata yerine bir hata sözlüğü döndüren bir yardımcı:
Bir coroutine iptal edildiğinde (zaman aşımı veya başka bir nedenle), Python içinde asyncio.CancelledError fırlatır. Coroutine'iniz dosya tanıtıcıları veya bağlantılar gibi kaynaklar tutuyorsa, try/finally kullanarak iptal durumunda bile temizliğin yapıldığından emin olun:
asyncdeffetch_with_cleanup(session, url):print("Starting fetch...")try:asyncwith session.get(url)as response:returnawait response.text()finally:print("Cleanup complete")# Runs even on cancellationAltyapı:
asyncio.gather() ile hata yönetimi
Zaman aşımları yavaş istekleri yakalar. Ancak bazı istekler hatayla anında başarısız olur. Bir partideki tek bir istek başarısız olursa ne olduğunu görelim.
Önce, geçersiz ID'lerde istisna fırlatan bir fetch_story() sürümüne ihtiyacımız var:
asyncdeffetch_story_strict(session, story_id):
story =await fetch_story(session, story_id)if story isNone:raise ValueError(f"Story not found: {story_id}")return storyAltyapı:
Şimdi dört geçerli hikâye ve bir geçersiz ID çekelim:
Tek bir geçersiz ID ile, dört başarılı sonucu da kaybediyoruz. Varsayılan olarak gather() hızlı-arıza (fail-fast) davranışı kullanır: bir istisna her şeyi iptal eder ve yukarı taşır.
Kısmi sonuçları korumak için return_exceptions=True ekleyin. Bu, gather()'ın davranışını değiştirir: istisna fırlatmak yerine, başarılı değerlerin yanında sonuç listesinde öğe olarak döndürür:
asyncdefmain():asyncwith aiohttp.ClientSession()as session:asyncwith session.get(f"{HN_API}/topstories.json")as response:
story_ids =await response.json()
ids_to_fetch = story_ids[:4]+[99999999999]
results =await asyncio.gather(*[fetch_story_strict(session, sid)for sid in ids_to_fetch],
return_exceptions=True# Don't raise, return exceptions in list)# Separate successes from failures using isinstance()
stories =[r for r in results ifnotisinstance(r, Exception)]
errors =[r for r in results ifisinstance(r, Exception)]print(f"Got {len(stories)} stories, {len(errors)} failed")
asyncio.run(main())Altyapı:
isinstance(result, Exception) kontrolü, başarılı sonuçları hatalardan ayırmanıza olanak tanır. Sonrasında çalışanları işleyebilir, hataları günlüğe alabilir veya yeniden deneyebilirsiniz.
Üstel geri çekilmeyle (exponential backoff) yeniden deneme mantığı
Bazı hatalar geçicidir. Sunucu kısa süreliğine aşırı yüklenmiş olabilir veya ağdaki bir aksaklık bağlantınızı düşürebilir. Bu durumlarda yeniden denemek mantıklıdır.
Ancak anında yeniden denemek işleri daha da kötüleştirebilir. Sunucu zorlanıyorsa, onu yeniden denemelerle bombardımana tutmak sorunu büyütür. Üstel geri çekilme, her deneme arasında bekleme süresini uzatarak bunu çözer.
Desen, bekleme sürelerini hesaplamak için 2 ** attempt kullanır: 0. deneme bir saniye (2⁰), 1. deneme iki saniye (2¹), 2. deneme dört saniye (2²) bekler, vb. Bu, sunucuya toparlanması için giderek daha fazla zaman tanır:
asyncdeffetch_with_retry(session, story_id, max_retries=3):for attempt inrange(max_retries):try:
story =await fetch_story(session, story_id)if story isNone:raise ValueError(f"Story {story_id} not found")return story
except(aiohttp.ClientError, ValueError):# Catch specific exceptionsif attempt == max_retries -1:print(f"Story {story_id}: Failed after {max_retries} attempts")returnNone
backoff =2** attempt # 1s, 2s, 4s...print(f"Story {story_id}: Attempt {attempt +1} failed, retrying in {backoff}s...")await asyncio.sleep(backoff)Altyapı:
Dikkat ederseniz, genel bir except yerine belirli istisnaları (aiohttp.ClientError, ValueError) yakalıyoruz. Bu, yalnızca geçici olabilecek hatalarda yeniden denediğimizden emin olur. Kötü koddaki bir KeyError yeniden denemeyi tetiklememelidir.
Geçerli ve geçersiz ID'lerin karışımıyla test edelim:
asyncdefmain():asyncwith aiohttp.ClientSession()as session:asyncwith session.get(f"{HN_API}/topstories.json")as response:
story_ids =await response.json()
test_ids =[story_ids[0],99999999999, story_ids[1],88888888888, story_ids[2]]
results =await asyncio.gather(*[fetch_with_retry(session, sid)for sid in test_ids])
successful =[r for r in results if r isnotNone]print(f"\nSuccessful: {len(successful)}, Failed: {len(test_ids)-len(successful)}")
asyncio.run(main())Altyapı:
Story 99999999999: Attempt 1 failed, retrying in 1s...
Story 88888888888: Attempt 1 failed, retrying in 1s...
Story 99999999999: Attempt 2 failed, retrying in 2s...
Story 88888888888: Attempt 2 failed, retrying in 2s...
Story 99999999999: Failed after 3 attempts
Story 88888888888: Failed after 3 attempts
Successful: 3, Failed: 2Altyapı:
Canlı sistemlerde, birden fazla başarısız isteğin tam olarak aynı anda yeniden denememesini sağlamak için titreşim (küçük rastgele gecikmeler) de eklersiniz. Ayrıca yalnızca geçici hataları (ör. 503 gibi sunucu tarafı ağ sorunları) yeniden dener, kalıcı olanlarda (ör. 404 veya 401) hemen vazgeçersiniz.
aiosqlite ile Python Async Veritabanı Depolama
Hacker News hikâyelerini uygun oran sınırlaması, zaman aşımları ve hata yönetimiyle çekiyorduk. Şimdi bunları bir veritabanına kaydedelim.
sqlite3 gibi normal bir senkron veritabanı kütüphanesi kullanmak, sorgular sırasında olay döngüsünü bloklayarak eşzamansız programlamanın amacını boşa çıkarır. Kodunuz veritabanını beklerken başka coroutine'ler çalışamaz. Async uygulamalar için async bir veritabanı kütüphanesine ihtiyaç vardır.
aiosqlite, Python'un yerleşik sqlite3'ünü async bir arayüzle sarmalar. Veritabanı işlemlerini bir iş parçacığı havuzunda çalıştırır, böylece olay döngüsünü bloklamaz. SQLite sunucu kurulumu gerektirmez — sadece bir dosyadır — bu nedenle bu kodu hemen çalıştırabilirsiniz. Python'da veritabanlarıyla çalışmaya yeniyseniz, Introduction to Databases in Python kursu, aiosqlite'ın üzerine inşa ettiği senkron temelleri kapsar.
Veritabanını kurma
Desen tanıdık gelmeli. Tıpkı aiohttp.ClientSession gibi, bağlantıyı yönetmek için async with kullanırsınız:
import aiosqlite
asyncdefinit_db(db_path):asyncwith aiosqlite.connect(db_path)as db:await db.execute("""
CREATE TABLE IF NOT EXISTS stories (
id INTEGER PRIMARY KEY,
title TEXT,
url TEXT,
score INTEGER,
fetched_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")await db.commit()
asyncio.run(init_db("stories.db"))Altyapı:
Temel fonksiyonlar:
aiosqlite.connect(path) bir veritabanı dosyası açar (veya oluşturur).
await db.execute(sql) bir SQL ifadesi çalıştırır.
await db.commit() değişiklikleri diske kaydeder.
Hikâyeleri kaydetme
İşte tek bir hikâyeyi kaydeden fonksiyon:
asyncdefsave_story(db, story):await db.execute("INSERT OR REPLACE INTO stories (id, title, url, score) VALUES (?, ?, ?, ?)",(story["id"], story.get("title",""), story.get("url",""), story.get("score",0)))Altyapı:
? yer tutucuları SQL enjeksiyonunu önler — değerleri SQL'e eklemek için asla f-string kullanmayın. INSERT OR REPLACE, hikâyeleri tekrar çekersek mevcut kayıtları günceller.
Python'da Tam Async Boru Hattı: Getir ve Kaydet
Şimdi bu eğitimdeki her şeyi bir araya getirerek eksiksiz bir boru hattı oluşturalım. 20 Hacker News hikâyesini oran sınırlamasıyla çekip bir veritabanına kaydedeceğiz:
import aiohttp
import aiosqlite
import asyncio
HN_API ="https://hacker-news.firebaseio.com/v0"asyncdeffetch_story(session, story_id):asyncwith session.get(f"{HN_API}/item/{story_id}.json")as response:returnawait response.json()asyncdeffetch_story_limited(session, story_id, semaphore):asyncwith semaphore:
story =await fetch_story(session, story_id)if story:return story
returnNoneasyncdefsave_story(db, story):await db.execute("INSERT OR REPLACE INTO stories (id, title, url, score) VALUES (?, ?, ?, ?)",(story["id"], story.get("title",""), story.get("url",""), story.get("score",0)))asyncdefmain():# Initialize databaseasyncwith aiosqlite.connect("hn_stories.db")as db:await db.execute("""
CREATE TABLE IF NOT EXISTS stories (
id INTEGER PRIMARY KEY,
title TEXT,
url TEXT,
score INTEGER,
fetched_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")# Fetch storiesasyncwith aiohttp.ClientSession()as session:asyncwith session.get(f"{HN_API}/topstories.json")as response:
story_ids =await response.json()
semaphore = asyncio.Semaphore(5)
tasks =[fetch_story_limited(session, sid, semaphore)for sid in story_ids[:20]]
stories =await asyncio.gather(*tasks)# Save to databasefor story in stories:if story:await save_story(db, story)await db.commit()# Query and display
cursor =await db.execute("SELECT id, title, score FROM stories ORDER BY score DESC LIMIT 5")
rows =await cursor.fetchall()print(f"Saved {len([s for s in stories if s])} stories. Top 5 by score:")for row in rows:print(f" [{row[2]}] {row[1][:50]}")
asyncio.run(main())Altyapı:
Saved 20 stories. Top 5 by score:
[671] Google Antigravity exfiltrates data via indirect p
[453] Trillions spent and big software projects are stil
[319] Ilya Sutskever: We're moving from the age of scali
[311] Show HN: We built an open source, zero webhooks pa
[306] FLUX.2: Frontier Visual IntelligenceAltyapı:
Boru hattı, her bölümdeki desenleri kullanır: bağlantı havuzu için ClientSession, oran sınırlaması için Semaphore(5), eşzamanlı getirme için gather() ve şimdi de async depolama için aiosqlite. Her bileşen, diğerlerini bloklamadan kendi işini yapar.
Bu iş akışını her çalıştırdığınızda, o günün en popüler hikâyelerini alırsınız.
Sonuç
Bu eğitim sizi temel async/await söz diziminden eksiksiz bir veri boru hattına taşıdı. Coroutine'lerin nasıl duraklayıp devam ettiğini, olay döngüsünün eşzamanlı görevleri nasıl yönettiğini ve asyncio.gather()'ın birden çok işlemi nasıl aynı anda çalıştırdığını öğrendiniz. aiohttp ile gerçek HTTP istekleri eklediniz, semaphore'larla eşzamanlılığı kontrol ettiniz, zaman aşımları ve yeniden denemelerle hataları yönettiniz ve aiosqlite ile sonuçları bir veritabanında sakladınız.
Kodunuz harici sistemleri beklediğinde async kullanın: HTTP API'leri, veritabanları, dosya I/O'su veya ağ soketleri. Veri işleme veya sayı hesaplama gibi CPU-yoğun işler için async yardımcı olmaz — bunun yerine multiprocessing veya concurrent.futures'a bakın. Daha ileri gitmek için asyncio dokümantasyonunu keşfedebilir ve async web API'leri oluşturmak için FastAPI'yi düşünebilirsiniz.
Python'da async ile sync programlama arasındaki fark nedir?⌄
Eşzamanlı programlamada kod satır satır çalışır ve her işlemin tamamlanmasını bekler. Async programlama, kodunuzun bir işlemi başlatıp beklerken diğer işlere devam etmesine, sonuç hazır olduğunda da kaldığı yerden sürdürmesine olanak tanır. Bunu, görevler arasında geçiş yapan bir olay döngüsü yönetir.
Düzenli Python yerine ne zaman async programlama kullanmalıyım?⌄
Async'i, kodunuzun harici sistemleri beklediği I/O ağırlıklı görevlerde kullanın: HTTP istekleri, veritabanı sorguları, dosya işlemleri veya ağ soketleri. Veri işleme veya hesaplama gibi CPU-yoğun işler için async fayda sağlamaz — bunun yerine multiprocessing veya concurrent.futures kullanın.
Neden "coroutine was never awaited" uyarısı alıyorum?⌄
Bu, bir async fonksiyonu await kullanmadan çağırdığınızda olur. get_data() gibi bir async fonksiyonu çağırmak, sonucu değil bir coroutine nesnesini döndürür. Gerçekten çalıştırıp dönüş değerini almak için await get_data() kullanmalısınız.
requests kütüphanesini asyncio ile kullanabilir miyim?⌄
Hayır, requests kütüphanesi senkron çalışır ve olay döngüsünü bloklar; bu da async'in amacını boşa çıkarır. Bunun yerine aiohttp kullanın — eşzamanlı istekler için tasarlanmış bir async HTTP istemcisidir. Bağlantı havuzu için tek bir ClientSession'ı yeniden kullanmayı unutmayın.
Bir API'yi bunaltmamak için eşzamanlı istekleri nasıl sınırlarım?⌄
Eşzamanlı çalışan istek sayısını kontrol etmek için asyncio.Semaphore kullanın. İstediğiniz sınırla (ör. asyncio.Semaphore(5)) bir semaphore oluşturun ve her isteği async with semaphore bloğuna alın. Bu, aynı anda yalnızca belirlediğiniz kadar isteğin "uçuşta" olmasını sağlar.
Algoritmalar, sistem tasarımı ve davranışsal senaryoları kapsayan bu temel sorularla teknik mülakat sürecine hakim olun. Uzman cevapları, kod örnekleri ve kanıtlanmış hazırlık stratejileri edinin.
Git deponuzu temiz tutmak için .gitignore’u nasıl kullanacağınızı öğrenin. Bu eğitim; temelleri, yaygın kullanım durumlarını ve başlamanıza yardımcı olacak pratik örnekleri kapsar!
API'leri ve açık kaynak kütüphanelerini kullanarak yapay zekayı yazılım uygulamalarına nasıl entegre edeceğinizi öğrenin. Yapay Zeka Mühendisi olma yolculuğunuza bugün başlayın!
Python betiğiniz API yanıtlarını, veritabanı sorgularını veya dosya işlemlerini beklerken, bu süre çoğu zaman boşa gider. Python async programlama sayesinde kodunuz aynı anda birden fazla görevi ele alabilir. Böylece bir işlem beklerken diğerleri ilerler; atıl anlar üretken çalışmaya dönüşür ve dakikalar süren beklemeleri çoğu zaman saniyelere indirir.
Bu rehberde, mini projeler üzerinden ilerleyerek Python'da async programlamanın temelini öğreteceğim. Coroutine'lerin, olay döngüsünün (event loop) ve async I/O'nun kodunuzu nasıl çok daha duyarlı hale getirdiğini göreceksiniz.
Eşzamansız web API'leri oluşturmayı öğrenmek istiyorsanız, FastAPI üzerindeki bu kursa mutlaka göz atın.
Python Async Programlama Nedir?
Geleneksel eşzamanlı (senkron) Python'da kodunuz satır satır çalışır. Örneğin bir API çağırdığınızda, programınız yanıtı bekler. Bu iki saniye sürerse, programınızın tamamı iki saniye boyunca boşta kalır. Eşzamansız programlama, kodunuzun bir API çağrısı başlatıp ardından diğer görevlerle devam etmesine olanak tanır.
Yanıt geldiğinde, kodunuz kaldığı yerden devam eder. Her işlemin bitmesini beklemek yerine, birden fazla işlemi aynı anda yürütebilirsiniz. Bu, kodunuzun veritabanları, API'ler veya dosya sistemleri gibi harici sistemlerden yanıt beklediği durumlarda en çok fark yaratır.
Bunun çalışması için Python'un async sistemi birkaç temel kavram kullanır:
Coroutine'ler:
defyerineasync defile tanımlanan fonksiyonlar. Yürütmeyi duraklatıp sürdürebilirler; bu da bekleme içeren işlemler için idealdir.await: Python'a şunu söyler: "Bu coroutine'i, bu işlem tamamlanana kadar duraklat; bu arada başka kodlar çalışabilsin."Olay döngüsü (event loop): Tüm coroutine'lerinizi yöneten ve hangisinin ne zaman çalışacağına karar veren motor.
Görevler (tasks): Eşzamanlı yürütme için sarmalanmış coroutine'ler. Aynı anda birden fazla işlemi çalıştırmak için
asyncio.create_task()ile oluşturursunuz.Async programlamanın neleri yapabildiği (ve yapamadığı) konusunun karışmaması için şunları aklınızda tutun:
Async, I/O ağırlıklı işlerde en iyi sonucu verir — HTTP istekleri, veritabanı sorguları ve dosya işlemleri gibi kodunuzun harici sistemleri beklediği durumlarda.
Async, CPU ağırlıklı işlere yardımcı olmaz — karmaşık hesaplamalar veya veri işleme gibi kodunuzun aktif olarak hesap yaptığı durumlarda değil.
Bu kavramları içselleştirmenin en iyi yolu gerçek async kodu yazmaktır. Sıradaki bölümde, ilk async fonksiyonunuzu oluşturacak ve coroutine'lerin olay döngüsüyle nasıl birlikte çalıştığını tam olarak göreceksiniz.
İlk Python Async Fonksiyonunuz
Async koda geçmeden önce, bir şey yapmadan önce bekleyen normal bir senkron fonksiyona bakalım:
Fonksiyon çalışır, ancak
time.sleep(2)tüm programınızı bloklar. Bu iki saniye boyunca başka hiçbir şey çalışamaz.Şimdi async sürümü:
Çıktı aynı görünür, ancak perde arkasında farklı bir şey olur. Üç değişiklik bunu async yaptı:
defyerineasync defile coroutine olarak bildirildi.time.sleep(2)yerineawait asyncio.sleep(2)ile bloklamadan duraklatıldı.asyncio.run()olay döngüsünü başlatıp coroutine'i çalıştırdı.asyncio.sleep()'in kendisinin de bir async fonksiyon olduğuna ve bu yüzdenawaitgerektirdiğine dikkat edin. Bu kilit bir kuraldır: her async fonksiyonawaitile çağrılmalıdır. İsterasyncio.sleep()gibi yerleşik bir fonksiyon olsun, ister sizin yazdığınız bir fonksiyon;await'i unutursanız gerçekte çalışmaz.Şu an için async sürüm daha hızlı görünmüyor. Bunun nedeni yalnızca bir görevimizin olması. Asıl fayda, aynı anda birden çok coroutine çalıştırdığınızda ortaya çıkar; bunu bir sonraki bölümde ele alacağız.
Bilmeniz gereken bir başka önemli şey: async bir fonksiyonu, normal bir fonksiyon gibi doğrudan çağıramazsınız. Deneyelim:
greet_after_delay()çağrısı, sonucu değil bir coroutine nesnesi döndürür. Fonksiyon aslında çalışmaz. Bunu yürütmek içinasyncio.run()veya başka bir fonksiyon içindeawaitkullanmanız gerekir.Olay döngüsü nasıl çalışır
Olay döngüsü async programlamanın motorudur. Coroutine'lerinizi yönetir ve neyin ne zaman çalışacağına karar verir. Async
greet_after_delay()fonksiyonunu çalıştırdığınızda adım adım olanlar:asyncio.run()bir olay döngüsü oluşturur.Olay döngüsü
greet_after_delay()'i başlatır."Starting..." yazdırılır.
await asyncio.sleep(2)satırına gelir → coroutine duraklar.Olay döngüsü kontrol eder: "Çalışacak başka görev var mı?" (şimdilik yok).
2 saniye geçer, sleep tamamlanır.
Olay döngüsü
greet_after_delay()'i sürdürür."Hello!" yazdırılır.
Fonksiyon biter → olay döngüsü çıkar.
Async'i ilginç kılan kısım 5. adımdır. Tek bir coroutine ile yapılacak başka bir şey yoktur. Ancak birden fazla coroutine olduğunda, olay döngüsü biri beklerken diğer işlere geçer. İki saniyelik bir sleep sırasında boşta kalmak yerine başka kodları çalıştırabilir.
Olay döngüsünü bir trafik yöneticisi gibi düşünün. Tek tek arabaları hızlandırmaz. Biri durduğunda, diğerlerine yol vererek akışı sürdürür.
Yaygın async hatası: await'i unutmak
Acemilerin sık yaptığı bir hata, başka bir async fonksiyon içinden coroutine çağırırken
await'i unutmak:awaitolmadan, dönüş değeri yerine coroutine nesnesi elde edersiniz. Python ayrıca coroutine'in hiç çalıştırılmadığı konusunda uyarır.Çözüm basit:
Bir coroutine'in beklenmediğine dair bir
RuntimeWarninggördüğünüzde, her async fonksiyon çağrısındaawaitkullandığınızı kontrol edin.Eşzamanlı Python Async Görevleri
Önceki bölümde senkron bir fonksiyonu async'e çevirdik. Ancak daha hızlı değildi. Bunun nedeni yalnızca bir coroutine çalıştırmış olmamız. Async'in gerçek gücü, birden fazla coroutine'i aynı anda çalıştırdığınızda ortaya çıkar.
Ardışık await neden hâlâ ardışık çalışır
Birden çok async fonksiyon çağırmanın onları otomatik olarak eşzamanlı çalıştıracağını düşünebilirsiniz. Ancak
greet_after_delay()'i üç kez çağırdığımızda ne olduğuna bakın:İki saniyelik üç görev için altı saniye. Her
await, bir sonraki satıra geçmeden önce ilgili coroutine'in bitmesini bekler. Kod async olsa da ardışık çalışır.asyncio.gather() kullanarak görevleri eşzamanlı çalıştırma
Coroutine'leri aynı anda çalıştırmak için
asyncio.gather()kullanın. Birden fazla coroutine alır ve bunları eşzamanlı yürütür:Altı yerine iki saniye. Üç coroutine de anında başladı, eşzamanlı olarak uyudu ve birlikte tamamlandı. Tek bir değişiklikle 3 kat hızlanma.
Çıktı sırasına dikkat edin: tüm "Starting..." mesajları, herhangi bir "Hello..." mesajından önce yazdırılıyor. Bu, coroutine'lerin birbirini beklemek yerine aynı iki saniyelik zaman penceresinde çalıştığını gösterir.
asyncio.gather(), sonuçları, coroutine'leri hangi sırayla verdiyseniz o sırada bir liste halinde döndürür. Coroutine'leriniz değer döndürüyorsa, bunları yakalayabilirsiniz:Sonuçlar,
gather()'a verilen coroutine'lerin sırasına uygun olarak[10, 20, 30]şeklinde döner.aiohttp ile Python Async HTTP İstekleri
Şimdiye dek gecikmeleri simüle etmek için
asyncio.sleep()kullandık. Şimdi gerçek HTTP istekleri yapalım.requestskütüphanesine yönelebilirsiniz, ancak burada işe yaramaz.requestssenkron çalışır ve olay döngüsünü bloklar; async'in amacını boşa çıkarır.Bunun yerine, bu amaç için geliştirilmiş bir async HTTP istemcisi olan
aiohttp'u kullanın.aiohttp'ya giriş
aiohttpile bir URL nasıl çekilir:İç içe iki
async withbloğuna dikkat edin. Her biri farklı bir kaynağı yönetir ve bunların ne yaptığını anlamakaiohttp'yu doğru kullanmanın anahtarıdır.aiohttp'da ClientSession nasıl çalışır
aiohttpile istek yaptığınızda adım adım olanlar:aiohttp.ClientSession()bir bağlantı havuzu oluşturur (başta boştur).session.get(url)havuzu kontrol eder: "Bu host'a (web sitesinin sunucusu) açık bir bağlantı var mı?"Bağlantı yoksa yeni bir TCP bağlantısı ve bir SSL el sıkışması oluşturulur.
Bir HTTP isteği gönderilir ve yanıt başlıklarını bekleriz.
Yanıt nesnesi bağlantıyı tutar.
await response.text()gövde verisini ağdan okur.İçteki
async withbloğundan çıkış: Bağlantı havuza geri döner (açık kalır!).Aynı host'a yapılacak bir sonraki istek, havuzdaki bağlantıyı yeniden kullanır (3. adımı atlar).
Dıştaki
async withbloğundan çıkış: Havuzdaki tüm bağlantılar kapanır.7 ve 8. adımlar kilit önemdedir. Bağlantı havuzu, istekler arasında bağlantıları canlı tutar. Aynı host'a bir istek daha yaptığınızda, TCP ve SSL el sıkışması tamamen atlanır.
Bu önemlidir çünkü yeni bir bağlantı kurmak yavaştır. Bir TCP el sıkışması sunucuya gidiş-dönüş gerektirir. Bir SSL el sıkışması iki tane daha. Gecikmeye bağlı olarak, ilk baytı göndermeden önce 100-300 ms anlamına gelebilir.
Tüm istekler için paylaşılan bir oturum kullanma
Şimdi her istek için yeni bir oturum oluşturmanın neden sorun olduğunu görebilirsiniz:
fetch_bad()'in her çağrısı, boş bir havuzla yeni bir oturum oluşturur. Tüm istekler aynı host'a gitse bile her istek tam el sıkışma maliyetini öder.Çözüm, tek bir oturum oluşturup fetch fonksiyonunuza geçirmektir:
Paylaşılan bir oturumla, ilk istek bağlantıyı kurar; kalan dokuz istek onu yeniden kullanır. On el sıkışma yerine bir.
Async HTTP isteği örneği: Hacker News kazıma
Bunu Hacker News API ile uygulayalım. Bu API, hikâyeleri çekmek için birden fazla istek gerektirdiğinden eşzamansız davranışı göstermek için idealdir. Python'da REST API'lerle çalışmaya yeniyseniz, temel kavramlar için Python API'leri: API Oluşturma ve Kullanma Rehberi'ne göz atın.
Hacker News API yapısı:
https://hacker-news.firebaseio.com/v0/topstories.jsonhikâye ID'lerinin bir listesini (sadece sayılar) döndürürhttps://hacker-news.firebaseio.com/v0/item/{id}.jsontek bir hikâye için ayrıntıları döndürür10 hikâye almak için 11 istek gerekir: biri ID listesi için, ardından her hikâye için birer tane. Async programlamanın parladığı nokta tam da burası.
Önce, ilk hikâyeyi çekmeyi denersek API ne döndürüyor görelim:
API 500 hikâye ID'si döndürür ve her hikâyede
title,url,scoreveby(yazar) gibi alanlar bulunur.Birden çok sonucu ardışık vs. eşzamanlı çekme
Şimdi 10 hikâyeyi ardışık olarak çekelim:
Şimdi aynı hikâyeleri eşzamanlı olarak çekelim:
Eşzamanlı sürüm 3,5 kat daha hızlı. Her isteğin bitmesini bekleyip sonra yenisini başlatmak yerine, 10 isteğin tamamı aynı anda çalışır. Async programlama gerçek ağ I/O'sunda burada karşılığını verir.
Python Async Hata Yönetimi ve Oran Sınırlama
Verileri eşzamanlı çekerken birkaç şey ters gidebilir. Çok fazla istekle sunucuyu bunaltabilirsiniz. Bazı istekler sonsuza kadar asılı kalabilir. Bazıları ise doğrudan başarısız olabilir. Ve hatalar olduğunda, bir kurtarma stratejisine ihtiyacınız vardır.
Bu bölüm, isteklerin gönderimini kontrol etmekten zaman sınırları koymaya, hataları ele almaya ve uygun olduğunda yeniden denemeye kadar endişeleri oluş sırasıyla ele alır. Python'da istisna yönetiminin temellerine tazeleme gerekiyorsa, Python'da İstisna ve Hata Yönetimi'ne bakın. Aşağıdaki temel kurulum boyunca bunu kullanacağız:
Semaphore'larla oran sınırlama
Önceki bölümde 10 isteği birden gönderdik. Bu sorun olmadı. Peki 500 hikâye çekmeniz gerekirse ne olur? Ya da 10.000 sayfa kazımanız?
Çoğu API, oran sınırlaması uygular. Saniyede 10 isteğe veya 100 eşzamanlı bağlantıya izin verebilirler. Bu sınırları aşarsanız engellenir, yavaşlatılır veya yasaklanırsınız. API sınır dayatmasa bile, binlerce isteği aynı anda göndermek kendi sisteminizi veya sunucuyu zorlayabilir.
Her an "uçuşta" (in flight) olan istek sayısını kontrol etmenin bir yoluna ihtiyacınız var. Semaphore tam da bunu yapar.
Semaphore, izin sistemi gibi çalışır. Üç izniniz olduğunu hayal edin. İstek yapmak isteyen her görev önce bir izin almalıdır. Bittiğinde izni iade eder, böylece yeni bir istek onu kullanabilir. İzin yoksa, görev biri serbest kalana kadar bekler.
3 izin ve 4 veya daha fazla görevle nasıl işler:
Üç izin mevcuttur.
A Görevi bir izin alır (2 kaldı), isteğine başlar.
B Görevi bir izin alır (1 kaldı), isteğine başlar.
C Görevi bir izin alır (0 kaldı), isteğine başlar.
D Görevi izin ister, ancak yoktur—bekler.
A Görevi biter, iznini iade eder (1 mevcut).
D Görevi o izni alır ve isteğine başlar.
Tüm görevler bitene kadar bu şekilde devam eder.
5. adımdaki bekleme verimlidir. Görev "izin boşaldı mı?" diye dönüp durmaz. Duraklar ve başka koda çalışması için izin verir. Olay döngüsü, bir izin kullanılabilir olduğunda onu uyandırır.
Koda bakalım. asyncio'da
asyncio.Semaphore(n)ile bir semaphore oluşturursunuz;nizin sayısıdır. Kullanmak için kodunuzuasync with semaphore:içine sarın. Bloğa girerken bir izin alınır, çıkarken otomatik olarak serbest bırakılır:30 hikâyeyi semaphore ile ve semaphoresuz çekmeyi karşılaştıralım:
Semaphore sürümü daha yavaştır çünkü istekleri beşerli partiler halinde işler. Ancak bu bir ödünleşimdir: hızdan feragat edip öngörülebilir ve sunucu dostu davranış kazanırsınız.
Unutmayın: semaphore, zaman birimi başına istekleri değil, eşzamanlı istek sayısını sınırlar.
Semaphore(10)"aynı anda en fazla 10 istek" demektir, "saniyede 10 istek" değil. Kesin zaman tabanlı oran sınırlaması (ör. tam olarak saniyede 10 istek) gerekiyorsa, semaphore'u partiler arası gecikmelerle birleştirebilir veyaaiolimitergibi bir kütüphane kullanabilirsiniz.asyncio.wait_for() ile zaman aşımları
Eşzamanlılık kontrolünde olsanız bile, tekil istekler takılabilir. Sunucu bağlantınızı kabul eder ama hiç yanıt vermez. Zaman aşımı olmadan programınız süresiz bekler.
asyncio.wait_for()herhangi bir coroutine'i bir son tarih ile sarar. Coroutine'i ve saniye cinsinden birtimeoutverirsiniz. İşlem zamanında tamamlanmazsaasyncio.TimeoutErrorfırlatır:Zaman aşımı dolduğunda
wait_for()coroutine'i iptal eder.TimeoutError'ı yakalayıp ne yapacağınıza karar verebilirsiniz: isteği atlamak, varsayılan bir değer döndürmek veya yeniden denemek.Eşzamanlı isteklerde, her birini ayrı ayrı sarmalayın. İşte hata yerine bir hata sözlüğü döndüren bir yardımcı:
Bir coroutine iptal edildiğinde (zaman aşımı veya başka bir nedenle), Python içinde
asyncio.CancelledErrorfırlatır. Coroutine'iniz dosya tanıtıcıları veya bağlantılar gibi kaynaklar tutuyorsa,try/finallykullanarak iptal durumunda bile temizliğin yapıldığından emin olun:asyncio.gather() ile hata yönetimi
Zaman aşımları yavaş istekleri yakalar. Ancak bazı istekler hatayla anında başarısız olur. Bir partideki tek bir istek başarısız olursa ne olduğunu görelim.
Önce, geçersiz ID'lerde istisna fırlatan bir
fetch_story()sürümüne ihtiyacımız var:Şimdi dört geçerli hikâye ve bir geçersiz ID çekelim:
Tek bir geçersiz ID ile, dört başarılı sonucu da kaybediyoruz. Varsayılan olarak
gather()hızlı-arıza (fail-fast) davranışı kullanır: bir istisna her şeyi iptal eder ve yukarı taşır.Kısmi sonuçları korumak için
return_exceptions=Trueekleyin. Bu,gather()'ın davranışını değiştirir: istisna fırlatmak yerine, başarılı değerlerin yanında sonuç listesinde öğe olarak döndürür:isinstance(result, Exception)kontrolü, başarılı sonuçları hatalardan ayırmanıza olanak tanır. Sonrasında çalışanları işleyebilir, hataları günlüğe alabilir veya yeniden deneyebilirsiniz.Üstel geri çekilmeyle (exponential backoff) yeniden deneme mantığı
Bazı hatalar geçicidir. Sunucu kısa süreliğine aşırı yüklenmiş olabilir veya ağdaki bir aksaklık bağlantınızı düşürebilir. Bu durumlarda yeniden denemek mantıklıdır.
Ancak anında yeniden denemek işleri daha da kötüleştirebilir. Sunucu zorlanıyorsa, onu yeniden denemelerle bombardımana tutmak sorunu büyütür. Üstel geri çekilme, her deneme arasında bekleme süresini uzatarak bunu çözer.
Desen, bekleme sürelerini hesaplamak için
2 ** attemptkullanır: 0. deneme bir saniye (2⁰), 1. deneme iki saniye (2¹), 2. deneme dört saniye (2²) bekler, vb. Bu, sunucuya toparlanması için giderek daha fazla zaman tanır:Dikkat ederseniz, genel bir
exceptyerine belirli istisnaları (aiohttp.ClientError,ValueError) yakalıyoruz. Bu, yalnızca geçici olabilecek hatalarda yeniden denediğimizden emin olur. Kötü koddaki birKeyErroryeniden denemeyi tetiklememelidir.Geçerli ve geçersiz ID'lerin karışımıyla test edelim:
Canlı sistemlerde, birden fazla başarısız isteğin tam olarak aynı anda yeniden denememesini sağlamak için titreşim (küçük rastgele gecikmeler) de eklersiniz. Ayrıca yalnızca geçici hataları (ör. 503 gibi sunucu tarafı ağ sorunları) yeniden dener, kalıcı olanlarda (ör. 404 veya 401) hemen vazgeçersiniz.
aiosqlite ile Python Async Veritabanı Depolama
Hacker News hikâyelerini uygun oran sınırlaması, zaman aşımları ve hata yönetimiyle çekiyorduk. Şimdi bunları bir veritabanına kaydedelim.
sqlite3gibi normal bir senkron veritabanı kütüphanesi kullanmak, sorgular sırasında olay döngüsünü bloklayarak eşzamansız programlamanın amacını boşa çıkarır. Kodunuz veritabanını beklerken başka coroutine'ler çalışamaz. Async uygulamalar için async bir veritabanı kütüphanesine ihtiyaç vardır.aiosqlite, Python'un yerleşiksqlite3'ünü async bir arayüzle sarmalar. Veritabanı işlemlerini bir iş parçacığı havuzunda çalıştırır, böylece olay döngüsünü bloklamaz. SQLite sunucu kurulumu gerektirmez — sadece bir dosyadır — bu nedenle bu kodu hemen çalıştırabilirsiniz. Python'da veritabanlarıyla çalışmaya yeniyseniz, Introduction to Databases in Python kursu,aiosqlite'ın üzerine inşa ettiği senkron temelleri kapsar.Veritabanını kurma
Desen tanıdık gelmeli. Tıpkı
aiohttp.ClientSessiongibi, bağlantıyı yönetmek içinasync withkullanırsınız:Temel fonksiyonlar:
aiosqlite.connect(path)bir veritabanı dosyası açar (veya oluşturur).await db.execute(sql)bir SQL ifadesi çalıştırır.await db.commit()değişiklikleri diske kaydeder.Hikâyeleri kaydetme
İşte tek bir hikâyeyi kaydeden fonksiyon:
?yer tutucuları SQL enjeksiyonunu önler — değerleri SQL'e eklemek için asla f-string kullanmayın.INSERT OR REPLACE, hikâyeleri tekrar çekersek mevcut kayıtları günceller.Python'da Tam Async Boru Hattı: Getir ve Kaydet
Şimdi bu eğitimdeki her şeyi bir araya getirerek eksiksiz bir boru hattı oluşturalım. 20 Hacker News hikâyesini oran sınırlamasıyla çekip bir veritabanına kaydedeceğiz:
Boru hattı, her bölümdeki desenleri kullanır: bağlantı havuzu için
ClientSession, oran sınırlaması içinSemaphore(5), eşzamanlı getirme içingather()ve şimdi de async depolama içinaiosqlite. Her bileşen, diğerlerini bloklamadan kendi işini yapar.Bu iş akışını her çalıştırdığınızda, o günün en popüler hikâyelerini alırsınız.
Sonuç
Bu eğitim sizi temel
async/awaitsöz diziminden eksiksiz bir veri boru hattına taşıdı. Coroutine'lerin nasıl duraklayıp devam ettiğini, olay döngüsünün eşzamanlı görevleri nasıl yönettiğini veasyncio.gather()'ın birden çok işlemi nasıl aynı anda çalıştırdığını öğrendiniz.aiohttpile gerçek HTTP istekleri eklediniz, semaphore'larla eşzamanlılığı kontrol ettiniz, zaman aşımları ve yeniden denemelerle hataları yönettiniz veaiosqliteile sonuçları bir veritabanında sakladınız.Kodunuz harici sistemleri beklediğinde async kullanın: HTTP API'leri, veritabanları, dosya I/O'su veya ağ soketleri. Veri işleme veya sayı hesaplama gibi CPU-yoğun işler için async yardımcı olmaz — bunun yerine
multiprocessingveyaconcurrent.futures'a bakın. Daha ileri gitmek için asyncio dokümantasyonunu keşfedebilir ve async web API'leri oluşturmak için FastAPI'yi düşünebilirsiniz.Bu bilginin üzerine inşa ederek akıllı uygulamalar tasarlamayı öğrenmek istiyorsanız, Geliştiriciler için Associate AI Engineer kariyer yoluna mutlaka göz atın.