Debug, Logging ve Profiling
Yazılımda "bug" (böcek) terimi 1947'den gelir. Harvard'daki bilgisayarın içinde gerçek bir böcek bulunmuş ve günlüğe "First actual case of bug being found" diye yazılmış. O günden beri yazılım hatalarına "bug", hata bulmaya da "debugging" diyoruz.
Debug yapmak bir dedektiflik işidir. Olay yeri var (hatalı çıktı), ipuçları var (log mesajları, hata mesajları), ve sen suçluyu (bug) bulmaya çalışıyorsun. Bazen suçlu apaçık ortada olur, bazen saatlerce iz sürersin.
Bu derste print'ten başlayıp, logging modülüne, pdb debugger'a ve profiling araçlarına kadar tüm silahları öğreneceğiz.
Print Debugging — En Eski Silah
Evet, print() ile debug yapmak "amatörce" görünebilir. Ama gerçek şu ki: herkes yapıyor. Senior geliştiriciler de, Google mühendisleri de. Hızlı ve etkili.
def calculate_discount(price, discount_percent, is_member):
print(f"DEBUG: price={price}, discount={discount_percent}, member={is_member}")
if is_member:
discount_percent += 10
print(f"DEBUG: member discount applied, new percent={discount_percent}")
discount = price * discount_percent / 100
print(f"DEBUG: discount amount={discount}")
final_price = price - discount
print(f"DEBUG: final_price={final_price}")
return final_price
# Nerede hata olduğunu görmek çok kolay
result = calculate_discount(100, 20, True)Çıktı:
DEBUG: price=100, discount=20, member=True
DEBUG: member discount applied, new percent=30
DEBUG: discount amount=30.0
DEBUG: final_price=70.0f-string ile Hızlı Debug (Python 3.8+)
Python 3.8'den itibaren = operatörü var. Bu harika bir kısayol:
x = 42
name = "Python"
items = [1, 2, 3]
print(f"{x=}") # x=42
print(f"{name=}") # name='Python'
print(f"{len(items)=}") # len(items)=3
print(f"{items[0]=}") # items[0]=1Değişken adını tekrar yazmana gerek yok!
Print Debug'ın Sorunları
Temizlemeyi unutursun: Üretime print'ler kaçar
Performans: Çok fazla print yavaşlatır
Kontrol yok: Bazı print'leri açıp kapatmak zor
Kalıcı değil: Kapattığında kaybolur
İşte bu sorunları çözen araç: logging.
logging Modülü — Profesyonel Kayıt Tutma
logging modülü print'in profesyonel versiyonudur. Mesajları seviyelerine göre ayırır, dosyaya yazabilir, formatlar ve kolayca açılıp kapatılabilir.
Log Seviyeleri
DEBUG → En detaylı, geliştirme sırasında (10)
INFO → Normal akış bilgileri (20)
WARNING → Dikkat edilmesi gereken durumlar (30)
ERROR → Bir şey yanlış gitti ama program devam ediyor (40)
CRITICAL → Ciddi hata, program çalışamaz (50)Düşün ki bir evin alarm sistemisin:
DEBUG: "Salon ışığı açıldı" (çok detaylı)
INFO: "Ev sahibi eve geldi" (normal bilgi)
WARNING: "Arka kapı 5 dakikadır açık" (dikkat)
ERROR: "Duman dedektörü arızalı" (sorun var)
CRITICAL: "Yangın tespit edildi!" (acil)
Basit Kullanım
import logging
# Basit config
logging.basicConfig(level=logging.DEBUG)
logging.debug("Bu çok detaylı bir bilgi")
logging.info("Program başladı")
logging.warning("Disk %90 dolu")
logging.error("Dosya bulunamadı: config.json")
logging.critical("Veritabanı bağlantısı koptu!")Çıktı:
DEBUG:root:Bu çok detaylı bir bilgi
INFO:root:Program başladı
WARNING:root:Disk %90 dolu
ERROR:root:Dosya bulunamadı: config.json
CRITICAL:root:Veritabanı bağlantısı koptu!Level Filtreleme
import logging
# Sadece WARNING ve üstünü göster
logging.basicConfig(level=logging.WARNING)
logging.debug("Bu görünmez") # ❌ Gizli
logging.info("Bu da görünmez") # ❌ Gizli
logging.warning("Bu görünür!") # ✅
logging.error("Bu da görünür!") # ✅Geliştirme ortamında DEBUG, production'da WARNING ayarlarsan, tek satır değişiklikle detay seviyesini kontrol edersin.
Logger ile Format Ayarlama
import logging
# Detaylı format
logging.basicConfig(
level=logging.DEBUG,
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
datefmt="%Y-%m-%d %H:%M:%S"
)
logger = logging.getLogger("my_app")
logger.debug("Uygulama başlatılıyor...")
logger.info("Kullanıcı giriş yaptı: ahmet@example.com")
logger.warning("API yanıt süresi yüksek: 2.5s")Çıktı:
2024-01-15 10:30:45 - my_app - DEBUG - Uygulama başlatılıyor...
2024-01-15 10:30:45 - my_app - INFO - Kullanıcı giriş yaptı: ahmet@example.com
2024-01-15 10:30:45 - my_app - WARNING - API yanıt süresi yüksek: 2.5sArtık her mesajda tarih, modül adı ve seviye var. Print'le bunu yapabilir misin?
Farklı Modüllerde Logger
# database.py
import logging
logger = logging.getLogger("my_app.database")
def connect():
logger.info("Veritabanına bağlanılıyor...")
logger.debug("Host: localhost, Port: 5432")
# ...
logger.info("Bağlantı başarılı")
# api.py
import logging
logger = logging.getLogger("my_app.api")
def handle_request(path):
logger.info(f"İstek alındı: {path}")
# ...
logger.debug(f"Response status: 200")Her modülün kendi logger'ı var. Hangi modülden geldiğini hemen anlarsın.
File Handler — Dosyaya Log Yazma
Log'ları terminale değil dosyaya yazmak isteyebilirsin. Özellikle production'da bu şart:
import logging
# File handler oluştur
logger = logging.getLogger("my_app")
logger.setLevel(logging.DEBUG)
# Konsol handler
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.WARNING) # Konsola sadece WARNING+
# Dosya handler
file_handler = logging.FileHandler("app.log")
file_handler.setLevel(logging.DEBUG) # Dosyaya her şeyi yaz
# Format
formatter = logging.Formatter(
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
)
console_handler.setFormatter(formatter)
file_handler.setFormatter(formatter)
# Handler'ları logger'a ekle
logger.addHandler(console_handler)
logger.addHandler(file_handler)
# Kullanım
logger.debug("Bu sadece dosyada görünür")
logger.warning("Bu hem dosyada hem konsolda")
logger.error("Bu da hem dosyada hem konsolda")Bu yapıda:
Geliştirici konsolda sadece önemli mesajları görür
Dosyada tüm detaylar kayıtlı olur
Problem olursa dosyayı inceleyebilirsin
RotatingFileHandler — Dosya Boyutu Kontrolü
Log dosyaları çok büyüyebilir. RotatingFileHandler dosyayı belirli boyuta ulaşınca döndürür:
import logging
from logging.handlers import RotatingFileHandler
logger = logging.getLogger("my_app")
handler = RotatingFileHandler(
"app.log",
maxBytes=5_000_000, # 5 MB
backupCount=3 # En fazla 3 yedek dosya
)
handler.setFormatter(logging.Formatter(
"%(asctime)s - %(levelname)s - %(message)s"
))
logger.addHandler(handler)Dosya 5 MB olunca app.log → app.log.1 olur, yeni app.log oluşturulur. En fazla 3 yedek tutulur, en eski silinir.
💡 İpucu: Production'da mutlaka
RotatingFileHandlerveyaTimedRotatingFileHandlerkullan. Yoksa log dosyan gigabyte'larca büyüyüp diski doldurabilir.
pdb — Python Debugger
print ve logging iyi ama bazen kodun içine girip adım adım çalıştırmak istersin. İşte pdb bunun için var.
breakpoint() ile Başla
Python 3.7+'dan itibaren breakpoint() fonksiyonu var. Koda bu satırı koyduğunda, program o noktada durur ve interaktif bir debug ortamına düşersin:
def find_bug(items):
total = 0
for i, item in enumerate(items):
if item > 10:
total += item
breakpoint() # Buraya gelince duracak
return total
result = find_bug([5, 15, 3, 20, 8])Program breakpoint()'e gelince:
> /home/user/script.py(6)find_bug()
-> return total
(Pdb) Artık interaktif debug modundasın!
Temel pdb Komutları
n (next) → Sonraki satıra git (fonksiyon içine GİRME)
s (step) → Sonraki satıra git (fonksiyon içine GİR)
c (continue) → Sonraki breakpoint'e kadar devam et
p expr → İfadeyi yazdır (print)
pp expr → Pretty print
l (list) → Çevredeki kaynak kodu göster
ll → Tüm fonksiyon kodunu göster
w (where) → Call stack'i göster
q (quit) → Debugger'dan çık
b N → N. satıra breakpoint koy
cl N → N. satırdaki breakpoint'i kaldırpdb Pratik Kullanım
def process_orders(orders):
results = []
for order in orders:
subtotal = order["price"] * order["quantity"]
tax = subtotal * 0.18
total = subtotal + tax
# Hmm, total neden yanlış?
breakpoint()
results.append({
"order_id": order["id"],
"total": total
})
return results
orders = [
{"id": 1, "price": 100, "quantity": 2},
{"id": 2, "price": 50, "quantity": "3"}, # Bug! String quantity
{"id": 3, "price": 75, "quantity": 1},
]
process_orders(orders)Debug session:
(Pdb) p order
{'id': 1, 'price': 100, 'quantity': 2}
(Pdb) p subtotal
200
(Pdb) p total
236.0
(Pdb) c
(Pdb) p order
{'id': 2, 'price': 50, 'quantity': '3'}
(Pdb) p subtotal
'505050' # AHA! String çarpımı!
(Pdb) p type(order["quantity"])
<class 'str'> # Buldum seni!Gördün mü? quantity string olarak gelmiş. 50 * "3" yapınca Python string tekrarı yapıyor: "505050". Bug bulundu!
breakpoint() Ortam Değişkeni ile Kontrol
# breakpoint()'leri devre dışı bırak (production)
PYTHONBREAKPOINT=0 python script.py
# Farklı debugger kullan
PYTHONBREAKPOINT=ipdb.set_trace python script.py
PYTHONBREAKPOINT=pudb.set_trace python script.py⚠️ Dikkat:
breakpoint()satırlarını commit etme! Code review'da bu satırı görürsen, muhtemelen unutulmuştur. Bazı takımlar pre-commit hook ilebreakpointkelimesini arayan kontrol koyar.
VS Code Debugger
VS Code'un built-in debugger'ı pdb'den çok daha kullanıcı dostu. Grafik arayüzle:
Breakpoint: Satır numarasının yanına tıklayarak koyarsın
Variables panel: Tüm değişkenleri gösterir
Watch: Belirli ifadeleri izleyebilirsin
Call Stack: Fonksiyon çağrı zincirini gösterir
Step Over / Step Into / Step Out: Butonlarla gezinirsin
launch.json Ayarı
{
"version": "0.2.0",
"configurations": [
{
"name": "Python: Current File",
"type": "debugpy",
"request": "launch",
"program": "${file}",
"console": "integratedTerminal",
"args": ["--verbose"]
},
{
"name": "Python: pytest",
"type": "debugpy",
"request": "launch",
"module": "pytest",
"args": ["tests/", "-v"]
}
]
}VS Code debugger'ın en güzel yanı: conditional breakpoint koyabilmen. Sağ tıklayıp "Add Conditional Breakpoint" → order["id"] == 42 yazarsan, sadece o koşul sağlandığında durur. Binlerce iterasyonluk bir döngüde çok işe yarar.
Profiling — Performans Dedektifliği
Kodun doğru çalışıyor ama yavaş mı? O zaman profiling zamanı. Profiling, kodun hangi kısmının ne kadar zaman harcadığını ölçer.
Analoji: Doktorun yaptığı kan testi gibi. Bir sorun var ama nerede? Kan testi sonuçlarına bakıp sorunu tespit ediyorsun.
cProfile — Built-in Profiler
import cProfile
def slow_function():
total = 0
for i in range(1000000):
total += i ** 2
return total
def fast_function():
return sum(i ** 2 for i in range(1000000))
def main():
slow_function()
fast_function()
cProfile.run("main()")Çıktı:
5 function calls in 0.542 seconds
Ordered by: standard name
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 0.542 0.542 <string>:1(<module>)
1 0.321 0.321 0.321 0.321 script.py:3(slow_function)
1 0.220 0.220 0.221 0.221 script.py:8(fast_function)
1 0.000 0.000 0.542 0.542 script.py:11(main)ncalls: Kaç kez çağrıldıtottime: Bu fonksiyonda harcanan toplam süre (alt fonksiyonlar hariç)cumtime: Bu fonksiyonda harcanan toplam süre (alt fonksiyonlar dahil)
cProfile'ı Komut Satırından Kullanma
python -m cProfile -s cumtime my_script.py-s cumtime sonuçları cumulative time'a göre sıralar. En çok zaman harcayan fonksiyon en üstte.
Sonuçları Dosyaya Kaydetme ve Analiz
import cProfile
import pstats
# Profil çıktısını dosyaya kaydet
cProfile.run("main()", "profile_output.prof")
# Sonuçları analiz et
stats = pstats.Stats("profile_output.prof")
stats.sort_stats("cumulative")
stats.print_stats(10) # En yavaş 10 fonksiyonsnakeviz ile Görsel Analiz
pip install snakeviz
python -m cProfile -o output.prof my_script.py
snakeviz output.profBu tarayıcıda interaktif bir görselleştirme açar. Hangi fonksiyon ne kadar zaman harcamış, sunburst chart ile görürsün.
line_profiler — Satır Satır Profiling
cProfile fonksiyon bazında ölçüm yapar. Ama bazen fonksiyonun hangi satırının yavaş olduğunu bilmek istersin:
pip install line_profiler# slow_code.py
from line_profiler import profile
@profile
def process_data(data):
# Satır satır ne kadar sürdüğünü göreceksin
cleaned = [item.strip().lower() for item in data]
unique = list(set(cleaned))
sorted_data = sorted(unique)
result = {}
for item in sorted_data:
result[item] = cleaned.count(item) # Bu satır yavaş!
return resultkernprof -l -v slow_code.pyÇıktı:
Line # Hits Time Per Hit % Time Line Contents
=======================================================
4 1 50 50.0 0.1 cleaned = [item.strip()...
5 1 30 30.0 0.1 unique = list(set(cleaned))
6 1 20 20.0 0.0 sorted_data = sorted(unique)
7 1 1 1.0 0.0 result = {}
8 500 5000 10.0 10.0 for item in sorted_data:
9 500 45000 90.0 89.8 result[item] = cleaned.count(item)%89.8 zamanı 9. satırda harcıyor! list.count() O(n) olduğu için, döngü içinde kullanmak O(n²) yapıyor. Counter kullanmak çözüm:
from collections import Counter
@profile
def process_data_fast(data):
cleaned = [item.strip().lower() for item in data]
result = dict(Counter(cleaned)) # O(n) — çok daha hızlı!
return dict(sorted(result.items()))timeit — Mikro Benchmark
İki yaklaşımdan hangisi daha hızlı? timeit bunu ölçer:
import timeit
# String birleştirme: + vs join
setup = "words = ['hello'] * 1000"
time_plus = timeit.timeit(
"result = ''\nfor w in words:\n result += w + ' '",
setup=setup,
number=10000
)
time_join = timeit.timeit(
"result = ' '.join(words)",
setup=setup,
number=10000
)
print(f"+ operatörü: {time_plus:.4f}s")
print(f"join(): {time_join:.4f}s")
print(f"join {time_plus/time_join:.1f}x daha hızlı")+ operatörü: 1.2340s
join(): 0.0890s
join 13.9x daha hızlıKomut Satırından timeit
python -m timeit "''.join(str(i) for i in range(100))"
# 10000 loops, best of 5: 20.5 usec per loop
python -m timeit "''.join([str(i) for i in range(100)])"
# 20000 loops, best of 5: 17.8 usec per looptimeit ile Fonksiyon Karşılaştırma
import timeit
def approach_1():
"""List append"""
result = []
for i in range(1000):
result.append(i ** 2)
return result
def approach_2():
"""List comprehension"""
return [i ** 2 for i in range(1000)]
def approach_3():
"""Map"""
return list(map(lambda i: i ** 2, range(1000)))
# Her birini ölç
for func in [approach_1, approach_2, approach_3]:
time = timeit.timeit(func, number=10000)
print(f"{func.__doc__.strip():25s}: {time:.4f}s")List append : 1.0234s
List comprehension : 0.7891s
Map : 0.8567sList comprehension kazandı! Ama fark çok büyük değilse, okunabilirliği tercih et.
💡 İpucu:
timeitmikro benchmark içindir. Gerçek uygulamalarda darboğazıcProfileile bul, sonra o noktayıtimeitile optimize et. Tüm kodutimeitile ölçmeye çalışma — zaman kaybı.
Pratik: Yavaş Fonksiyonu Tespit ve İyileştirme
Bir dosya işleme script'imiz var ve çok yavaş çalışıyor. Adım adım sorunu bulup düzeltelim:
Sorunlu Kod
import time
def read_data(filename):
"""Dosyadan veri oku (simülasyon)"""
time.sleep(0.01) # I/O simülasyonu
return [f"item_{i}" for i in range(10000)]
def process_items(items):
"""Her öğeyi işle"""
results = []
for item in items:
processed = item.upper().replace("_", "-")
# Her seferinde tüm listeyi kontrol etme (O(n²)!)
if processed not in results:
results.append(processed)
return results
def generate_report(items):
"""Rapor oluştur"""
report = ""
for item in items:
report += f"- {item}\n" # String concatenation anti-pattern
return report
def find_duplicates(items):
"""Tekrarları bul"""
duplicates = []
for i in range(len(items)):
for j in range(i + 1, len(items)):
if items[i] == items[j] and items[i] not in duplicates:
duplicates.append(items[i])
return duplicates
def main():
data = read_data("data.txt")
processed = process_items(data)
report = generate_report(processed)
dupes = find_duplicates(data[:1000]) # İlk 1000
return len(report), len(dupes)Adım 1: cProfile ile Darboğazı Bul
import cProfile
cProfile.run("main()", sort="cumulative") ncalls tottime cumtime filename:lineno(function)
1 0.000 8.542 script.py:main
1 5.234 5.234 script.py:find_duplicates ← !!!
1 2.890 2.890 script.py:process_items ← !!
1 0.398 0.398 script.py:generate_report ← !
1 0.020 0.020 script.py:read_dataEn yavaş: find_duplicates (5.2s), sonra process_items (2.9s).
Adım 2: Optimize Et
def process_items_fast(items):
"""Set kullanarak O(n) karmaşıklık"""
seen = set()
results = []
for item in items:
processed = item.upper().replace("_", "-")
if processed not in seen: # Set lookup O(1)!
seen.add(processed)
results.append(processed)
return results
def generate_report_fast(items):
"""join kullanarak string birleştirme"""
lines = [f"- {item}" for item in items]
return "\n".join(lines)
def find_duplicates_fast(items):
"""Counter kullanarak O(n) karmaşıklık"""
from collections import Counter
counts = Counter(items)
return [item for item, count in counts.items() if count > 1]Adım 3: Benchmark Karşılaştırma
import timeit
# process_items karşılaştırma
items = [f"item_{i}" for i in range(5000)]
old_time = timeit.timeit(lambda: process_items(items), number=5)
new_time = timeit.timeit(lambda: process_items_fast(items), number=5)
print(f"process_items: {old_time:.3f}s → {new_time:.3f}s ({old_time/new_time:.0f}x hızlı)")
# generate_report karşılaştırma
old_time = timeit.timeit(lambda: generate_report(items), number=100)
new_time = timeit.timeit(lambda: generate_report_fast(items), number=100)
print(f"generate_report: {old_time:.3f}s → {new_time:.3f}s ({old_time/new_time:.0f}x hızlı)")
# find_duplicates karşılaştırma
small = [f"item_{i % 500}" for i in range(1000)]
old_time = timeit.timeit(lambda: find_duplicates(small), number=5)
new_time = timeit.timeit(lambda: find_duplicates_fast(small), number=5)
print(f"find_duplicates: {old_time:.3f}s → {new_time:.3f}s ({old_time/new_time:.0f}x hızlı)")process_items: 2.890s → 0.012s (240x hızlı)
generate_report: 0.398s → 0.008s (50x hızlı)
find_duplicates: 5.234s → 0.001s (5234x hızlı)Toplam: 8.5 saniyeden 0.02 saniyeye! 425x hızlanma. Ve tek yaptığımız doğru veri yapılarını kullanmak.
Debug Stratejileri — Sistem Yaklaşımı
Rastgele print koyarak debug yapma. Sistematik ol:
1. Hatayı Yeniden Üret
Hatayı güvenilir şekilde tekrarlayamıyorsan, düzeltmen çok zor. Önce minimal bir örnek oluştur.
# Kötü: "Bazen çalışmıyor"
# İyi: "input=[1, 2, None, 4] olduğunda line 15'te TypeError veriyor"2. İkiye Bölme (Binary Search Debug)
Kodun ortasına bir print koy. Sorun öncesinde mi sonrasında mı? Bulduğun yarıyı tekrar ikiye böl. Logaritmik hızda daralt.
def complex_pipeline(data):
step1 = transform(data)
print(f"After step1: {step1[:3]}") # Burada doğru mu?
step2 = filter_data(step1)
print(f"After step2: {step2[:3]}") # Burada doğru mu?
step3 = aggregate(step2)
print(f"After step3: {step3}") # Burada doğru mu?
return format_output(step3)3. Rubber Duck Debugging
Ciddiyim. Masana bir lastik ördek koy (veya herhangi bir nesne). Sorunu ona sesli olarak anlat. Problemi açıklarken çoğu zaman çözümü de bulursun. Bu gerçekten işe yarıyor çünkü sorunu formüle etmek, düşünce sürecini düzenler.
4. Versiyona Dön (Git Bisect)
# Sorunsuz çalışan son commit'i bul
git bisect start
git bisect bad # Şu anki commit bozuk
git bisect good abc123 # Bu commit çalışıyordu
# Git otomatik olarak ortadaki commit'i checkout eder
# Test et, bad veya good de
git bisect bad # veya good
# Tekrarla...Git logaritmik arama ile bug'ı tanıtan commit'i bulur.
Debug Araçları Karşılaştırma
| Araç | Ne Zaman? | Avantaj | Dezavantaj |
|---|---|---|---|
print() | Hızlı kontrol | Kolay, hızlı | Temizlemek lazım |
logging | Production, uzun süreli | Kalıcı, seviyeli | Setup gerekir |
breakpoint()/pdb | Karmaşık bug | İnteraktif, adım adım | Terminal-based |
| VS Code Debugger | Günlük geliştirme | Görsel, kolay | IDE gerekli |
cProfile | Performans sorunu | Built-in, genel bakış | Satır detayı yok |
line_profiler | Detaylı performans | Satır bazında | Kurulum gerekli |
timeit | Karşılaştırma | Doğru ölçüm | Mikro seviye |
⚠️ Dikkat: "Premature optimization is the root of all evil" — Donald Knuth. Önce kodun doğru çalışsın, sonra profiling yap, sonra sadece darboğazları optimize et. Her satırı optimize etmeye çalışmak hem zaman kaybı hem de kodu okunmaz yapar.
Özet
Print debugging basit ama etkilidir, f-string
=operatörü (Python 3.8+) ile daha da pratiklogging modülü print'in profesyonel versiyonudur: seviyeler (DEBUG→CRITICAL), dosyaya yazma, format kontrolü
pdb (
breakpoint()) ile kodun içine girip adım adım çalıştırabilir, değişkenleri inceleyebilirsincProfile ile hangi fonksiyonun yavaş olduğunu, line_profiler ile hangi satırın yavaş olduğunu bulursun
timeit ile iki yaklaşımı mikro benchmark ile karşılaştırabilirsin
Debug yaparken sistematik ol: hatayı yeniden üret, ikiye böl, rubber duck dene
AI Asistan
Sorularını yanıtlamaya hazır