Neden önemli?
Anadolu Market'in kampanya ekibi her hafta yeni kural tanımlıyor: "500 TL üstüne yüzde 10", "Ankara'ya kargo 30 TL indirimli", "ilk siparişe 50 TL". Ilgaz Bank'ta ise her kart için farklı günlük harcama limiti var. Bu kuralları tek tek if bloklarıyla yazarsan kod büyüdükçe yönetilemez olur. Python'ın cevabı şudur: kuralı bir fonksiyon olarak üret, ayarını içine göm, ihtiyaç olunca çağır. Ayarını içinde taşıyan bu fonksiyona kapanış (closure) denir.
Fonksiyonel araçlar dersinde kapanışla ilk kez tanıştın (sayaç ve komisyon hesaplayıcı yazdın). Bu derste perdenin arkasına geçiyoruz: Python bir adı nerede arar, iç fonksiyon dış değişkeni nasıl "hatırlar", hatırladığı şey değer mi yoksa değişkenin kendisi mi, ve bu farkın döngülerde neden can sıktığı. Bu bilgi dekoratörlerin (decorator), geri çağırma (callback) fonksiyonlarının ve PCAP sınavının 5.3 konusunun temelidir.
LEGB tekrar: bir ad nerede aranır?
Kapsam ve modüller dersinde gördüğün kural: Python bir adı okurken sırayla dört yere bakar ve ilk bulduğunu kullanır.
| Harf | Kapsam | Örnek |
|---|---|---|
| L | Yerel (Local): fonksiyonun kendi adları | Parametreler, fonksiyon içinde atanan adlar |
| E | Kapsayan (Enclosing): dıştaki fonksiyonların adları | İç fonksiyonun gördüğü dış değişken |
| G | Genel (Global): modül düzeyindeki adlar | Dosyanın en üstünde tanımlanan sabitler |
| B | Yerleşik (Built-in): Python'ın hazır adları | len, sum, print |
kdv = 0.20 # G: genel
def fatura(tutar): # tutar: L (fatura için)
kargo = 30 # L (fatura için)
def satir(adet): # adet: L (satir için)
# kargo E'den, kdv G'den, round B'den gelir
return round(adet * tutar * (1 + kdv) + kargo, 2)
return satir(2)
print(fatura(100))
Önemli ayrıntı: Bir adın hangi kapsama ait olduğuna Python derleme anında karar verir, çalışma anında değil. Fonksiyonun gövdesinde bir ada herhangi bir yerde atama yapılıyorsa, o ad fonksiyonun tamamı boyunca yereldir. Bu yüzden aşağıdaki kod, atama satırı okumadan sonra gelse bile hata verir:
toplam = 0
def ekle(tutar):
try:
toplam += tutar # toplam = toplam + tutar: önce okur!
except UnboundLocalError as h:
print("Hata:", h)
ekle(50)
toplam += tutar bir atama olduğu için toplam yerel sayılır; okumaya çalıştığında yerel toplam henüz bağlanmamıştır (unbound). Hata adı da buradan gelir: UnboundLocalError.
global ve nonlocal: hangi kutuya yazıyorsun?
Okumak için bildirim gerekmez; atamak istediğin ad yerel değilse Python'a söylemen gerekir. İki anahtar kelime vardır ve farklı kutulara yazar:
global ad | nonlocal ad | |
|---|---|---|
| Nereye yazar? | Modül düzeyindeki ada | En yakın kapsayan fonksiyondaki ada |
| Ad önceden var olmalı mı? | Hayır, yoksa oluşturur | Evet, yoksa SyntaxError |
| Modül düzeyinde kullanılır mı? | Anlamsız ama hata değil | Hayır, SyntaxError |
| Tipik kullanım | Nadiren; yapılandırma, önbellek | Kapanışta sayaç, bakiye, durum |
| Test edilebilirlik | Kötü: gizli paylaşılan durum | İyi: durum kapanışa özel |
sayac = 0 # genel
def genel_artir():
global sayac
sayac += 1
def kasa_ac():
sayac = 100 # kasa_ac'a ait, genel sayac'tan bağımsız
def islem():
nonlocal sayac # kasa_ac'taki sayac'a yazar
sayac += 1
return sayac
return islem
genel_artir(); genel_artir()
k = kasa_ac()
print(k(), k()) # 101 102
print("genel sayac:", sayac) # 2: nonlocal genele dokunmadı
nonlocal yalnızca kapsayan fonksiyonlara bakar, modül düzeyine asla çıkmaz. Dış fonksiyonda o ad yoksa kod daha çalışmadan SyntaxError: no binding for nonlocal verir.Kapanış neyi yakalar: değeri mi, değişkeni mi?
İç fonksiyon dıştaki bir adı kullandığında ve dış fonksiyon bittikten sonra da yaşamaya devam ettiğinde kapanış oluşur. Python o adı bir hücre (cell) nesnesine koyar; dış fonksiyon da iç fonksiyon da aynı hücreyi görür. Yakalanan şey o anki değer değil, hücrenin kendisidir. Hücredeki değer sonradan değişirse iç fonksiyon yeni değeri görür.
def carpan(k):
def uygula(x):
return x * k
k = k + 1 # dönmeden önce k değişti
return uygula
c = carpan(3)
print(c(10)) # 40, 30 değil
print(c.__code__.co_freevars) # serbest değişken adları
print(c.__closure__) # hücre demeti
print(c.__closure__[0].cell_contents) # hücredeki güncel değer
__code__.co_freevars iç fonksiyonun dışarıdan kullandığı serbest değişkenlerin (free variables) adlarını, __closure__ ise aynı sırayla hücreleri verir. Hiç serbest değişken kullanmayan fonksiyonda __closure__ None olur. Bu özellikler hata ayıklamada (debugging) ve PCAP sorularında karşına çıkar.
Hücre paylaşıldığı için aynı dış fonksiyondan dönen iki iç fonksiyon tek bir durumu ortak kullanabilir. Her yeni çağrı ise yeni hücre ve bağımsız durum üretir:
def hesap(bakiye):
def yatir(tutar):
nonlocal bakiye
bakiye += tutar
def goster():
return bakiye
return yatir, goster
yatir_a, goster_a = hesap(100)
yatir_b, goster_b = hesap(0)
yatir_a(50)
yatir_b(10)
print(goster_a(), goster_b()) # 150 10
print(yatir_a.__closure__[0] is goster_a.__closure__[0]) # aynı hücre
Geç bağlama (late binding) tuzağı
"Hücreyi yakalar" kuralının en ünlü sonucu budur. Döngüde fonksiyon üretirsen hepsi döngü değişkeninin aynı hücresini paylaşır. Değer, fonksiyon çağrıldığında okunur; o anda döngü çoktan bitmiştir.
oranlar = [0.05, 0.10, 0.20]
kurallar = []
for oran in oranlar:
kurallar.append(lambda tutar: tutar * oran)
print([k(100) for k in kurallar]) # üçü de 20.0!
# Çözüm 1: varsayılan argüman, değeri TANIM anında kopyalar
kurallar = [lambda tutar, oran=oran: tutar * oran for oran in oranlar]
print([k(100) for k in kurallar])
# Çözüm 2: fabrika fonksiyonu, her çağrı yeni hücre açar
def kural_uret(oran):
return lambda tutar: tutar * oran
kurallar = [kural_uret(o) for o in oranlar]
print([k(100) for k in kurallar])
# Çözüm 3: functools.partial, argümanı o anda sabitler
from functools import partial
def kural(tutar, oran):
return tutar * oran
kurallar = [partial(kural, oran=o) for o in oranlar]
print([k(100) for k in kurallar])
k(100, oran=5)). Ekip kodunda fabrika fonksiyonu ya da partial daha temiz ve niyeti daha açıktır.Fabrika fonksiyonları: kural üreten kural
Fabrika fonksiyonu (factory function), ayarları alıp bu ayarlarla çalışan yeni bir fonksiyon döndürür. Doğrulamayı fabrikada bir kez yaparsın; üretilen fonksiyon her çağrıda hızlı ve sade kalır.
def indirim_kurali(oran, esik=0):
if not 0 < oran < 1:
raise ValueError("oran 0 ile 1 arasında olmalı")
def uygula(tutar):
if tutar >= esik:
return round(tutar * (1 - oran), 2)
return tutar
uygula.__name__ = f"indirim_{int(oran * 100)}"
return uygula
hafta_sonu = indirim_kurali(0.10, esik=500)
ogrenci = indirim_kurali(0.15)
for tutar in [300, 750]:
print(tutar, hafta_sonu(tutar), ogrenci(tutar))
print(hafta_sonu.__name__, ogrenci.__name__)
Ilgaz Bank'ta kart başına günlük limit de aynı kalıptır; bu kez kapanış durumu değiştirir, bu yüzden nonlocal gerekir:
def gunluk_limit(limit):
kalan = limit
def harca(tutar):
nonlocal kalan
if tutar > kalan:
return f"RED: {tutar} TL, kalan {kalan} TL"
kalan -= tutar
return f"ONAY: {tutar} TL, kalan {kalan} TL"
return harca
kart = gunluk_limit(1000)
for t in [400, 500, 200]:
print(kart(t))
Kapanış mı, sınıf mı?
Kapanış "tek metotlu küçük bir nesne" gibidir. Durum büyüdükçe ve davranış çoğaldıkça sınıf daha okunur olur.
| Ölçüt | Kapanış | Sınıf |
|---|---|---|
| Davranış sayısı | Bir (ya da birkaç iç fonksiyon) | Çok sayıda metot |
| Durumu dışarıdan görmek | Zor: __closure__ ile | Kolay: nesne.alan |
Kalıtım (inheritance), __repr__, karşılaştırma | Yok | Var |
| Satır sayısı | Az | Daha fazla |
| Tipik yer | Dekoratör, callback, key=, kural | Hesap, sepet, bağlantı |
class Carpan:
def __init__(self, k):
self.k = k
def __call__(self, x): # nesneyi fonksiyon gibi çağrılabilir yapar
return x * self.k
def carpan(k):
return lambda x: x * k
kdv_sinif = Carpan(1.2)
kdv_kapanis = carpan(1.2)
print(kdv_sinif(100), kdv_kapanis(100))
print(kdv_sinif.k, kdv_kapanis.__closure__[0].cell_contents)
functools.partial ile ilişkisi
partial, "argümanı önceden doldur" işini kapanış yazmadan yapar ve ayarı açıkça gösterir: .func, .args, .keywords. Kendi iç mantığı yoksa, sadece argüman sabitliyorsan partial seç; fabrikada doğrulama, hesap ya da durum varsa kapanış seç.
from functools import partial
def kargo_ucreti(desi, sehir, ekspres=False):
tarife = {"İstanbul": 40, "Ankara": 45, "Van": 70}
ucret = tarife[sehir] + desi * 5
return ucret * 1.5 if ekspres else ucret
ankara = partial(kargo_ucreti, sehir="Ankara")
print(ankara(4), ankara(4, ekspres=True))
print(ankara.func.__name__, ankara.args, ankara.keywords)
def sehir_tarifesi(sehir): # aynı işin kapanış hâli
def hesapla(desi, ekspres=False):
return kargo_ucreti(desi, sehir, ekspres)
return hesapla
print(sehir_tarifesi("Ankara")(4))
lambda gördüğünde ilk sorulacak soru şudur: "Bu değişken çağrı anında ne olacak?"Hücreler ve bellek
Kapanış dış fonksiyonun yalnızca kullandığı adları tutar, tüm yerel değişkenleri değil. Yine de büyük bir listeyi yakalayan kapanış yaşadıkça liste de bellekte kalır. Uzun ömürlü callback'lerde yalnızca gerekeni yakala: adet = len(buyuk_liste) deyip adet'i kullan.
Değiştirilebilir nesneyle nonlocal'sız durum
Hücredeki ad yeniden bağlanmıyorsa nonlocal gerekmez: onbellek = {} yakalayıp onbellek[k] = v demek bir atama değil, nesneyi değiştirmektir. Hafızalı (memoize) fonksiyonlar ve lru_cache'in basit hâli böyle yazılır. Ama onbellek = {} diye yeniden atarsan yine nonlocal ister.
Kapanış ve eşzamanlılık
Paylaşılan hücreyi birden fazla iş parçacığı (thread) değiştiriyorsa sayac += 1 atomik değildir; Eşzamanlılık (concurrency) dersinde bunun için kilit (lock) kullanacaksın. Ayrıca kapanışlar pickle ile serileştirilemez; çok işlemcili (multiprocessing) kodda modül düzeyi fonksiyon ya da partial tercih edilir.
PCAP 5.3 için kontrol listesi
- İç fonksiyon dış adı okuyorsa bildirim gerekmez; atıyorsa
nonlocalgerekir. - Dış fonksiyon döndükten sonra da iç fonksiyon dış değeri görür: kapanış.
- Döngüde üretilen lambdalar son değeri görür;
i=iile düzelir. __closure__yoksaNone; varsa hücre demeti, değercell_contents'te.