Önce ölç, sonra düzelt
Ilgaz Bank'ın gece raporu 40 dakika sürüyor ve ekip "Python yavaş" diyor. Çoğu zaman yavaş olan Python değil, bir listede milyon kez in aramak ya da döngüde string biriktirmektir. Optimizasyonun ilk kuralı: tahmin etme, ölç. İkinci kuralı: en çok zaman geçen yüzde 5'lik kodu düzelt, gerisine dokunma.
timeit: küçük parçayı ölç
timeit bir ifadeyi çok kez çalıştırıp toplam süreyi verir; çöp toplayıcıyı kapatır, en küçük gürültüyle ölçer. İki alternatifi karşılaştırmak için idealdir.
import timeit
musteriler = list(range(50_000))
kume = set(musteriler)
aranan = [49_999, 25_000, -1, 10]
t_liste = timeit.timeit(lambda: [x in musteriler for x in aranan], number=50)
t_kume = timeit.timeit(lambda: [x in kume for x in aranan], number=50)
print(f"liste in: {t_liste:.4f} s set in: {t_kume:.6f} s")
print(f"set yaklaşık {t_liste / t_kume:.0f} kat hızlı")
cProfile: büyük programda nerede?
Bütün betikte hangi fonksiyonun süre yediğini cProfile gösterir. Bellek içinde çalıştırıp pstats ile en pahalı birkaç fonksiyonu sıralarsın. tottime fonksiyonun kendi süresi, cumtime çağırdıklarıyla birlikte toplam.
import cProfile, pstats, io
def musteri_bul(musteri_id, liste):
return musteri_id in liste # O(n)
def rapor(n):
liste = list(range(n))
bulunan = 0
for i in range(0, n, 7):
if musteri_bul(i, liste):
bulunan += 1
return bulunan
pr = cProfile.Profile()
pr.enable()
rapor(20_000)
pr.disable()
cikti = io.StringIO()
pstats.Stats(pr, stream=cikti).sort_stats("cumulative").print_stats(4)
satirlar = [s for s in cikti.getvalue().splitlines() if "musteri_bul" in s or "rapor" in s]
print("\n".join(satirlar))
Çıktıda musteri_bul binlerce kez çağrılıyor ve toplam sürenin neredeyse tamamını alıyor. Düzeltme fonksiyonu hızlandırmak değil, veri yapısını değiştirmek: listeyi bir kez set'e çevir.
Big-O sezgisi
Big-O, girdi n büyüyünce sürenin nasıl büyüdüğünü söyler. Sabitleri değil eğilimi önemser; 10 bin satırda fark etmeyen şey 10 milyonda saatlere dönüşür.
| İşlem | Karmaşıklık | Örnek |
|---|---|---|
x in set, d[k], lst[i], lst.append | O(1) | Hash ile sabit zaman |
x in list, lst.index, min/max/sum | O(n) | Baştan sona tarar |
lst.insert(0, x), lst.pop(0) | O(n) | Her elemanı kaydırır (deque kullan) |
sorted, lst.sort | O(n log n) | Timsort |
| İç içe iki döngü (her sipariş × her müşteri) | O(n·m) | Sözlükle O(n+m)'e iner |
Sıralı listede bisect | O(log n) | İkili arama |
import timeit
siparisler = [(i, i % 1000) for i in range(20_000)] # (siparis, musteri_id)
musteriler = [(m, f"Müşteri{m}") for m in range(1000)]
def birlestir_yavas(): # O(n·m)
return [(s, ad) for s, m in siparisler for mid, ad in musteriler if mid == m]
def birlestir_hizli(): # O(n+m)
ad = dict(musteriler)
return [(s, ad[m]) for s, m in siparisler]
assert birlestir_yavas()[:3] == birlestir_hizli()[:3]
print(f"yavaş: {timeit.timeit(birlestir_yavas, number=1):.3f} s")
print(f"hızlı: {timeit.timeit(birlestir_hizli, number=1):.4f} s")
Sık kazanımlar
String birleştirme
Döngüde s += parça her seferinde yeni string üretir. Parçaları listede biriktirip "".join(...) ya da f-string kullan.
Üreteçle bellek
[f(x) for x in range(10**7)] tüm listeyi belleğe koyar; (f(x) for x in ...) ya da yield tek seferde bir eleman üretir. sum, max, any gibi tüketiciler üreteçle (generator) çalışır. Büyük dosyayı satır satır okumak da aynı ilkedir.
lru_cache
Aynı girdiyle tekrar tekrar çağrılan saf (yan etkisiz) fonksiyonu functools.lru_cache ile ezberle. Özyinelemeli hesaplar ve pahalı sorgular için çarpıcıdır.
import timeit, sys
from functools import lru_cache
def birlestir_arti(n):
s = ""
for i in range(n):
s += f"{i},"
return s
def birlestir_join(n):
return ",".join(str(i) for i in range(n)) + ","
assert birlestir_arti(100) == birlestir_join(100)
print(f"+=: {timeit.timeit(lambda: birlestir_arti(20_000), number=5):.3f} s "
f"join: {timeit.timeit(lambda: birlestir_join(20_000), number=5):.3f} s")
liste = [x * x for x in range(100_000)]
uretec = (x * x for x in range(100_000))
print("liste:", sys.getsizeof(liste), "bayt | üreteç:", sys.getsizeof(uretec), "bayt")
@lru_cache(maxsize=None)
def taksit_katsayi(ay: int) -> float:
return 1.0 if ay == 0 else taksit_katsayi(ay - 1) * 1.02
t0 = timeit.default_timer()
print(round(taksit_katsayi(400), 4), f"{timeit.default_timer() - t0:.5f} s")
print(taksit_katsayi.cache_info())
numpy'a geçiş
Sayısal bir döngüyü ne kadar iyileştirsen de yorumlayıcı maliyeti kalır. Veri tek tip sayı ve işlem eleman eleman ise numpy'a geç: döngü C'de döner, bellek yan yana olur. Kural: "for ile milyon sayı" gördüğünde numpy/pandas düşün.
import timeit, numpy as np
tutarlar = [float(i % 500) for i in range(300_000)]
dizi = np.array(tutarlar)
def kdv_python():
return sum(t * 1.2 for t in tutarlar if t > 100)
def kdv_numpy():
return float((dizi[dizi > 100] * 1.2).sum())
assert abs(kdv_python() - kdv_numpy()) < 1e-3
print(f"python: {timeit.timeit(kdv_python, number=3):.3f} s numpy: {timeit.timeit(kdv_numpy, number=3):.4f} s")
Ölçüm araç kutusu: hangi soruya hangi araç?
Derste timeit ve cProfile ile tanıştın. Gerçek işte soru her zaman "ne kadar sürdü?" değildir; bazen "hangi satır?", bazen "çalışan bir sunucu neden takıldı?", bazen "bellek nereye gitti?" diye sorarsın. Her soru için ayrı bir araç vardır.
| Soru | Araç | Nerede çalışır? |
|---|---|---|
| İki kısa alternatiften hangisi hızlı? | timeit (repeat ile) | Standart kütüphane, tarayıcıda da |
| Bir iş bloğu kaç saniye sürdü? | time.perf_counter() | Standart kütüphane |
| Hangi fonksiyon zaman yiyor? | cProfile + pstats | Standart kütüphane |
| Fonksiyonun hangi satırı? | line_profiler (kernprof) | Kendi bilgisayarında, pip install line_profiler |
| Çalışan süreç neden yavaş? (kodu değiştirmeden) | py-spy | Kendi bilgisayarında, pip install py-spy |
| CPU + bellek birlikte, satır satır | scalene | Kendi bilgisayarında, pip install scalene |
| Hangi satır ne kadar bellek ayırdı? | tracemalloc | Standart kütüphane |
perf_counter, time ve process_time
time.time() duvar saatidir; sistem saati senkronize edilince geri bile gidebilir. Süre ölçmek için tekdüze (monotonic) ve yüksek çözünürlüklü time.perf_counter() kullan. time.process_time() yalnızca işlemcinin senin sürecin için harcadığı süreyi verir; ağ ya da disk beklerken artmaz. Fark büyükse kodun hesap yapmıyor, bekliyordur: çözüm algoritma değil, eşzamanlılıktır (bunu eszamanlilik ve async derslerinde gördün).
import time
from functools import wraps
def zamanla(fn):
@wraps(fn)
def sarmal(*args, **kwargs):
t0, c0 = time.perf_counter(), time.process_time()
try:
return fn(*args, **kwargs)
finally:
duvar = time.perf_counter() - t0
cpu = time.process_time() - c0
print(f"{fn.__name__}: duvar={duvar:.3f}s cpu={cpu:.3f}s")
return sarmal
@zamanla
def hesapla():
return sum(i * i for i in range(300_000))
@zamanla
def bekle():
time.sleep(0.2) # ağ/disk beklemesinin taklidi
hesapla()
bekle() # duvar ~0.2 s, cpu ~0: kod bekliyor, hesaplamıyor
timeit.repeat: gürültüye karşı en küçük değer
Tek bir ölçüm arka plandaki işlere, önbelleğe, işlemci frekansına göre oynar. timeit.repeat(..., repeat=5, number=N) ölçümü beş kez tekrarlar; ortalamayı değil en küçüğü al. En küçük değer "kodun gerçekte ne kadar sürdüğüne" en yakın olandır; daha büyük değerler kodun değil makinenin gürültüsüdür.
import timeit
veri = list(range(10_000))
sonuclar = timeit.repeat(lambda: sorted(veri, reverse=True), repeat=5, number=200)
print("ölçümler:", [round(s, 4) for s in sonuclar])
print(f"en iyi: {min(sonuclar) / 200 * 1e6:.1f} µs / çağrı")
Komut satırından profil: kurulum ve sık hatalar
Aşağıdaki araçlar tarayıcıda çalışmaz; kendi bilgisayarında, projenin sanal ortamı (virtual environment) etkinken dene.
# 1) cProfile'ı kod değiştirmeden çalıştır, kümülatif süreye göre sırala python -m cProfile -s cumtime gece_raporu.py | head -25 # 2) Sonucu dosyaya kaydedip sonra incele python -m cProfile -o profil.out gece_raporu.py python -m pstats profil.out # açılan kabukta: sort cumtime / stats 15 # 3) Satır satır: fonksiyonun üstüne @profile yaz, kernprof ile çalıştır pip install line_profiler kernprof -l -v gece_raporu.py # 4) Çalışan sürece dışarıdan bak (sunucuyu durdurmadan) pip install py-spy py-spy top --pid 12345 py-spy record -o profil.svg -- python gece_raporu.py # alev grafiği (flame graph)
| Belirti | Neden | Çözüm |
|---|---|---|
NameError: name 'profile' is not defined | @profile dekoratörünü (decorator) yalnızca kernprof tanımlar; betiği düz python ile çalıştırdın | kernprof -l -v dosya.py kullan ya da işin bitince dekoratörü sil |
py-spy: Permission denied | macOS ve Linux başka sürece bağlanmayı kısıtlar | macOS'ta sudo py-spy ...; Linux'ta sudo ya da kendi başlattığın süreci record -- python ... ile izle |
kernprof: command not found | Paket başka bir ortama kuruldu ya da Scripts/bin klasörü PATH'te değil | Sanal ortamı etkinleştir; python -m kernprof -l -v dosya.py |
cProfile çıktısında yüzlerce {built-in method ...} satırı | Sıralama yapılmadı | -s cumtime ekle, ilk 20-30 satıra bak |
Bellek profili: tracemalloc ile anlık görüntü karşılaştırma
tracemalloc Python'ın ayırdığı belleği izler. İki anlık görüntü (snapshot) alıp farkını satır satır listelersen "bellek nerede büyüdü?" sorusunu yanıtlarsın. get_traced_memory() ise (şu an, tepe) çiftini verir; bir fonksiyonun en yüksek bellek kullanımını ölçmenin en kolay yoludur.
import tracemalloc
def liste_ile(n):
return sum([i * 2 for i in range(n)])
def uretec_ile(n):
return sum(i * 2 for i in range(n))
for fn in (liste_ile, uretec_ile):
tracemalloc.start()
fn(200_000)
simdi, tepe = tracemalloc.get_traced_memory()
tracemalloc.stop()
print(f"{fn.__name__:11}: tepe {tepe / 1024:8.1f} KB")
Standart kütüphanenin hızlı yolları
Çoğu "hızlandırma" yeni bir şey yazmak değil, C ile yazılmış hazır aracı bulmaktır.
| Yavaş kalıp | Hızlı karşılığı | Neden |
|---|---|---|
sorted(x, reverse=True)[:10] | heapq.nlargest(10, x) | Tümünü sıralamaz: O(n log k) |
Sıralı listede for ile eşik arama | bisect.bisect_right | İkili arama: O(log n) |
| Elle sayaç sözlüğü | collections.Counter | Sayım döngüsü C'de |
Döngüde sonuc.append(f(x)) | Liste kapsamı ya da map | Metot araması her turda tekrarlanmaz |
Döngüde math.sqrt, self.oran | Döngü öncesi yerel ada bağla | Yerel ad araması nitelik aramasından ucuzdur |
import heapq, bisect
from collections import Counter
tutarlar = [120, 5400, 80, 990, 15000, 300, 7200]
print("en büyük 3:", heapq.nlargest(3, tutarlar))
# Kademeli komisyon: 0-999 %3, 1000-4999 %2, 5000+ %1
esikler = [1000, 5000]
oranlar = [0.03, 0.02, 0.01]
for t in (500, 1000, 7200):
print(t, "->", oranlar[bisect.bisect_right(esikler, t)])
sehirler = ["İzmir", "Ankara", "İzmir", "Bursa", "İzmir", "Ankara"]
print(Counter(sehirler).most_common(2))
Python yetmediğinde: seçenekler
Algoritmayı düzelttin, numpy'a geçtin, hâlâ yavaş mı? Sırayla şu seçeneklere bakılır: multiprocessing (CPU işini çekirdeklere dağıtır, eszamanlilik dersinde), Numba (pip install numba, sayısal fonksiyonu @njit ile makine koduna derler), Cython (Python'a C tipleri ekleyip derler), PyPy (JIT'li alternatif yorumlayıcı; saf Python döngülerinde çok hızlı, bazı C eklentileriyle sorunlu). Python 3.13 ile deneysel JIT ve GIL'siz (free-threaded) sürüm de geldi; üretimde kullanmadan önce bağımlılıklarının desteklediğini doğrula.
Optimizasyon sırası
- Ölç: cProfile ile en pahalı fonksiyonları bul, timeit ile alternatifi doğrula.
- Algoritma ve veri yapısı: O(n²)'yi O(n)'e indir; list → set/dict, pop(0) → deque, sıralı arama → bisect.
- Python düzeyi: üreteç, join, lru_cache, yerel değişken, gereksiz kopyadan kaçınma.
- Kütüphaneye devret: numpy/pandas, sqlite/veritabanı sorgusu, yerleşik fonksiyonlar (sum, sorted) C'de çalışır.
- Paralellik: I/O için thread/asyncio, CPU için süreçler. En son adım, çünkü karmaşıklık getirir.
Bellek de ölçülür
tracemalloc hangi satırın ne kadar bellek ayırdığını gösterir; sys.getsizeof tek nesnenin boyutudur (içindekileri saymaz). Büyük veri için pandas'ta küçük dtype ve category, saf Python'da __slots__ ve üreteç.
Ölçüm hataları
İlk çalıştırma önbellek yüzünden yavaştır; number ve repeat ile birkaç kez ölç, en küçüğü al. Dizüstünde pil modu, arka plan işleri ölçümü oynatır; karşılaştırmayı aynı oturumda ve arka arkaya yap.