Scope, LEGB Kuralı ve Closure
Hiç şöyle bir hata aldın mı: "Bu değişkeni az önce tanımladım, neden bulanamıyor?" Ya da "Fonksiyon içinde değiştirdim ama dışarıda değişmedi!" Bunların hepsinin arkasında scope (kapsam) kavramı var.
Scope, bir değişkenin kodun neresinde erişilebilir olduğunu belirler. Bu derste Python'un scope kurallarını, global ve nonlocal keyword'lerini, ve güçlü bir kavram olan closure'ları öğreneceksin.
LEGB Kuralı
Python bir değişken adıyla karşılaştığında, onu bulmak için belirli bir sırada arar. Bu sıraya LEGB kuralı denir:
Local — Fonksiyonun kendi içi
Enclosing — Dış fonksiyonun içi (iç içe fonksiyonlarda)
Global — Modül (dosya) seviyesi
Built-in — Python'un yerleşik isimleri (
print,len,rangevb.)
Python bu sırayla arar ve ilk bulduğu yerde durur.
Düşün ki bir şey arıyorsun. Önce kendi odan (L), sonra evin salonu (E), sonra apartman (G), sonra şehir (B). İlk nerede bulursan orada alırsın.
# Built-in scope
# print, len, range, int, str vb. burada
# Global scope
x = "global"
def dis():
# Enclosing scope
x = "enclosing"
def ic():
# Local scope
x = "local"
print(x) # Hangisi? → "local" (ilk L'de buldu)
ic()
dis() # localHer katmanı teker teker görelim:
x = "global"
def dis():
# x tanımlı DEĞİL burada → enclosing'de yok, global'e bak
def ic():
# x tanımlı DEĞİL burada → local'de yok, enclosing'e bak
# enclosing'de de yok → global'e bak
print(x) # → "global"
ic()
dis() # globalx = "global"
def dis():
x = "enclosing"
def ic():
# x tanımlı DEĞİL burada → local'de yok, enclosing'e bak
print(x) # → "enclosing" (enclosing'de buldu, global'e bakmadı)
ic()
dis() # enclosingLocal Scope: Fonksiyon İçi
Bir fonksiyon içinde tanımlanan değişkenler local (yerel) değişkenlerdir. Fonksiyon dışından erişilemezler.
def hesapla():
sonuc = 42 # Local değişken
print(f"Fonksiyon içi: {sonuc}")
hesapla() # Fonksiyon içi: 42
# print(sonuc) # NameError: name 'sonuc' is not definedsonuc değişkeni sadece hesapla() fonksiyonunun içinde yaşar. Fonksiyon bittiğinde değişken de yok olur.
Her Çağrıda Yeni Local Scope
Her fonksiyon çağrısı kendi local scope'unu oluşturur:
def sayac():
count = 0
count += 1
print(f"count = {count}")
sayac() # count = 1
sayac() # count = 1 ← Sıfırdan başlıyor, önceki çağrıyı hatırlamıyor
sayac() # count = 1Her çağrıda count sıfırdan oluşturulur. Önceki çağrının count'u çoktan yok olmuş. Bu aslında iyi bir şey — fonksiyonlar birbirlerinin verilerini bozamaz.
Parametreler de Local
Fonksiyon parametreleri de local değişkendir:
def kare(x):
# x burada bir local değişken
return x ** 2
print(kare(5)) # 25
# print(x) # NameError: x tanımlı değil (dışarıdan)Local, Global'i Gölgeler
Aynı isimde hem local hem global değişken varsa, fonksiyon içinde local olan kazanır:
mesaj = "Global mesaj"
def fonksiyon():
mesaj = "Local mesaj" # Global'i gölgeler (shadow)
print(mesaj)
fonksiyon() # Local mesaj
print(mesaj) # Global mesaj (değişmedi!)Fonksiyon içindeki mesaj yeni bir local değişken oluşturdu. Global olan mesaj'a dokunmadı bile.
Global Scope ve global Keyword
Global değişkenler modül (dosya) seviyesinde tanımlanır ve dosyanın her yerinden okunabilir:
PI = 3.14159
UYGULAMA_ADI = "Hesap Makinesi"
def daire_alani(r):
return PI * r ** 2 # Global PI'yi OKUYOR — sorun yok
print(daire_alani(5)) # 78.53975Global değişkeni okumak sorun değil. Sorun değiştirmeye çalışınca başlar:
sayac = 0
def artir():
sayac += 1 # ← HATA!
print(sayac)
# artir()
# UnboundLocalError: local variable 'sayac' referenced before assignmentNe oldu? Python, sayac += 1 (yani sayac = sayac + 1) gördüğünde "aha, sayac'a atama yapılıyor, demek bu local bir değişken" diye düşünür. Ama sayac + 1 hesaplanırken local sayac henüz tanımlı değil → hata.
global ile Çözüm
sayac = 0
def artir():
global sayac # "Bu fonksiyondaki sayac, global olandır" demek
sayac += 1
print(f"Sayac: {sayac}")
artir() # Sayac: 1
artir() # Sayac: 2
artir() # Sayac: 3
print(f"Global sayac: {sayac}") # Global sayac: 3global keyword'ü Python'a "bu ismi local olarak oluşturma, global olanı kullan" der.
⚠️ global Kullanmak Neden Kötü Practice?
global çalışır, ama neredeyse her zaman daha iyi bir alternatif vardır. İşte global'in sorunları:
1. Takip edilemezlik: Global değişken dosyanın herhangi bir yerinden değiştirilebilir. Büyük projelerde "bu değişkeni kim, ne zaman değiştirdi?" sorusunun cevabını bulmak kabus olur.
# ❌ Kötü: Global state
kullanici_sayisi = 0
def kayit_ol():
global kullanici_sayisi
kullanici_sayisi += 1
def sil():
global kullanici_sayisi
kullanici_sayisi -= 1
# 50 farklı fonksiyon bu değişkeni değiştirebilir
# Hata ayıklamak imkansız hale gelir2. Test edilemezlik: Global state'e bağlı fonksiyonları izole test edemezsin.
3. Thread safety: Çoklu thread'lerde global değişkenler race condition'lara yol açar.
4. Bağımlılık: Fonksiyon dış dünyaya bağımlı hale gelir. Aynı fonksiyonu başka bir yerde kullanamazsın.
Daha iyi alternatifler:
# ✅ İyi: Parametre olarak geç ve return et
def artir(sayac):
return sayac + 1
sayac = 0
sayac = artir(sayac) # 1
sayac = artir(sayac) # 2
# ✅ İyi: Class kullan
class Sayac:
def __init__(self):
self.deger = 0
def artir(self):
self.deger += 1
def oku(self):
return self.deger
s = Sayac()
s.artir()
s.artir()
print(s.oku()) # 2⚠️ Dikkat:
globalkeyword'ünü kullanman gereken durum neredeyse hiç yoktur. Eğer kendiniglobalyazarken bulursan, dur ve "bunu parametreler ve return ile yapabilir miyim?" diye sor. Cevap neredeyse her zaman "evet"tir.
İstisna: Modül seviyesinde sabitler (PI, MAX_RETRY, DATABASE_URL gibi) global tanımlamak normaldir. Bunlar zaten değiştirilmez. Sorun olan, global değişkenleri fonksiyonlardan değiştirmektir.
nonlocal Keyword: Enclosing Scope'a Erişim
global gibi ama bir dış fonksiyonun (enclosing scope) değişkenine erişmek için:
def dis():
sayac = 0
def ic():
nonlocal sayac # "Enclosing scope'taki sayac'ı kullan"
sayac += 1
print(f"İç: {sayac}")
ic() # İç: 1
ic() # İç: 2
print(f"Dış: {sayac}") # Dış: 2
dis()nonlocal olmadan ne olurdu?
def dis():
sayac = 0
def ic():
# nonlocal yok
sayac += 1 # UnboundLocalError!
ic()
# dis() # Hata!Aynı global sorunu — Python sayac += 1 gördüğünde local sanır ama tanımlı değil.
global vs nonlocal
x = "global"
def dis():
x = "enclosing"
def ic_global():
global x
x = "ic tarafından değiştirildi"
def ic_nonlocal():
nonlocal x
x = "ic tarafından değiştirildi"
ic_global()
print(f"Enclosing x: {x}") # enclosing (değişmedi!)
dis()
print(f"Global x: {x}") # ic tarafından değiştirildi
# Reset
x = "global"
def dis2():
x = "enclosing"
def ic_nonlocal():
nonlocal x
x = "ic tarafından değiştirildi"
ic_nonlocal()
print(f"Enclosing x: {x}") # ic tarafından değiştirildi
dis2()
print(f"Global x: {x}") # global (değişmedi!)global→ global scope'taki değişkeni hedeflernonlocal→ en yakın enclosing scope'taki değişkeni hedefler
Closure: Fonksiyon Fabrikası
Closure, Python'un en güçlü ve zarif kavramlarından biri. Ama ilk başta kafa karıştırıcı olabilir. Adım adım gidelim.
Fonksiyon Fabrikası Analojisi
Bir fabrika düşün. Bu fabrika ürün üretmiyor — başka fabrikalar üretiyor. Mesela bir "çarpma fabrikası" var. Bu fabrikaya "3" dersen, sana "3 ile çarpan bir makine" veriyor. "5" dersen, "5 ile çarpan bir makine" veriyor.
İşte closure tam da bu: fonksiyon üreten fonksiyon.
def carpici_fabrikasi(carpan):
"""Belirli bir sayıyla çarpan fonksiyon üretir."""
def carp(sayi):
return sayi * carpan # carpan enclosing scope'tan geliyor
return carp # Fonksiyonu döndürüyoruz, çağırmıyoruz!
# Fabrikadan "makineler" üret
ikiyle_carp = carpici_fabrikasi(2)
ucle_carp = carpici_fabrikasi(3)
onla_carp = carpici_fabrikasi(10)
# Makineleri kullan
print(ikiyle_carp(5)) # 10
print(ucle_carp(5)) # 15
print(onla_carp(5)) # 50
# Her biri bağımsız çalışır
print(ikiyle_carp(100)) # 200
print(onla_carp(100)) # 1000Burada olan şey:
carpici_fabrikasi(2)çağrıldığındacarpan=2olurİçerideki
carpfonksiyonucarpan'a erişircarpici_fabrikasibitse bilecarp,carpan=2'yi hatırlarBu "hatırlama" işi closure'ın ta kendisi
Closure Nedir (Teknik Tanım)?
Closure, bir iç fonksiyonun dış fonksiyonun değişkenlerine erişimini korumasıdır. Dış fonksiyon çoktan bitmiş olsa bile, iç fonksiyon o değişkenlere erişebilir.
Normal şartlarda carpici_fabrikasi(2) bittiğinde carpan=2 yok olmalıydı. Ama carp fonksiyonu ona referans tuttuğu için Python onu hayatta tutar.
# Closure'ın tuttuğu değişkenleri görebilirsin
print(ikiyle_carp.__closure__)
# (<cell at 0x...: int object at 0x...>,)
print(ikiyle_carp.__closure__[0].cell_contents)
# 2 ← closure'ın "hatırladığı" değerClosure Örnekleri
Örnek 1: Counter (Sayaç)
def sayac_olustur(baslangic=0):
"""Her çağrıda bir artıran sayaç fonksiyonu üretir."""
deger = [baslangic] # Liste kullanıyoruz (mutable olduğu için)
def artir():
deger[0] += 1
return deger[0]
return artir
sayac_a = sayac_olustur()
sayac_b = sayac_olustur(100)
print(sayac_a()) # 1
print(sayac_a()) # 2
print(sayac_a()) # 3
print(sayac_b()) # 101
print(sayac_b()) # 102
# Her sayaç bağımsız!
print(sayac_a()) # 4 (kendi state'ini tutuyor)Neden deger = [baslangic] (liste) kullandık? Çünkü deger += 1 yazsaydık Python bunu local atama sanacak ve nonlocal gerekecekti. Liste ile deger[0] += 1 yaparak listeyi mutate ediyoruz — yeni atama yok.
`nonlocal` ile daha temiz versiyon:
def sayac_olustur(baslangic=0):
"""nonlocal ile sayaç."""
deger = baslangic
def artir():
nonlocal deger
deger += 1
return deger
return artir
s = sayac_olustur()
print(s()) # 1
print(s()) # 2
print(s()) # 3Örnek 2: Logger Factory
def logger_olustur(modul_adi):
"""Modül adını hatırlayan bir logger fonksiyonu üretir."""
def log(mesaj, seviye="INFO"):
print(f"[{seviye}] [{modul_adi}] {mesaj}")
return log
# Farklı modüller için logger'lar
auth_log = logger_olustur("auth")
db_log = logger_olustur("database")
api_log = logger_olustur("api")
auth_log("Kullanıcı giriş yaptı")
# [INFO] [auth] Kullanıcı giriş yaptı
db_log("Bağlantı kuruldu")
# [INFO] [database] Bağlantı kuruldu
api_log("Rate limit aşıldı", "WARN")
# [WARN] [api] Rate limit aşıldıHer logger kendi modül adını closure ile hatırlıyor. Harika değil mi?
Örnek 3: Validator Factory
def aralik_kontrol(minimum, maksimum):
"""Belirli bir aralıkta olup olmadığını kontrol eden fonksiyon üretir."""
def kontrol(deger):
if minimum <= deger <= maksimum:
return True
print(f"HATA: {deger} [{minimum}-{maksimum}] aralığında değil!")
return False
return kontrol
# Validator'lar oluştur
yas_gecerli = aralik_kontrol(0, 150)
not_gecerli = aralik_kontrol(0, 100)
sicaklik_gecerli = aralik_kontrol(-50, 60)
print(yas_gecerli(25)) # True
print(yas_gecerli(200)) # HATA: 200 [0-150] aralığında değil! → False
print(not_gecerli(85)) # True
print(not_gecerli(-5)) # HATA: -5 [0-100] aralığında değil! → False
print(sicaklik_gecerli(36.5)) # TrueName Shadowing: Gölgeleme Tehlikesi
"Shadowing" bir ismin daha iç kapsamdaki aynı isim tarafından gizlenmesidir. En tehlikeli form built-in isimleri gölgelemek:
# ❌ TEHLİKE: Built-in'i gölgeleme
list = [1, 2, 3] # 'list' artık bir değişken, fonksiyon değil!
# Artık list() fonksiyonunu kullanamazsın
# list("merhaba") # TypeError: 'list' object is not callable
# Düzeltmek için:
del list # Shadow'u kaldır
print(list("merhaba")) # ['m', 'e', 'r', 'h', 'a', 'b', 'a']Sık gölgelenen built-in isimler:
# ❌ Bunları DEĞİŞKEN ADI olarak KULLANMA!
list = [] # list() fonksiyonunu öldürür
dict = {} # dict() fonksiyonunu öldürür
set = {1, 2} # set() fonksiyonunu öldürür
str = "merhaba" # str() fonksiyonunu öldürür
int = 42 # int() fonksiyonunu öldürür
type = "admin" # type() fonksiyonunu öldürür
input = "veri" # input() fonksiyonunu öldürür
print = "mesaj" # print() fonksiyonunu öldürür (FELAKET!)
id = 12345 # id() fonksiyonunu öldürür
max = 100 # max() fonksiyonunu öldürür
min = 0 # min() fonksiyonunu öldürür
sum = 500 # sum() fonksiyonunu öldürür
len = 10 # len() fonksiyonunu öldürürBu hataları yakalamanın yolu: IDE kullan! VS Code ve PyCharm bu tür shadowing'leri uyarı olarak gösterir.
# ✅ Güvenli alternatifler
my_list = []
user_dict = {}
item_set = {1, 2}
name_str = "merhaba"
count_int = 42
user_type = "admin"
user_input = "veri"
user_id = 12345
max_value = 100
total_sum = 500Fonksiyon İçi Shadowing
def yanlis_toplam(sayilar):
# ❌ sum'ı gölgeledik!
sum = 0
for s in sayilar:
sum += s
return sum
# Bu fonksiyon kendi içinde çalışır ama...
print(yanlis_toplam([1, 2, 3])) # 6
# sum fonksiyonu hâlâ global'de sağlam
print(sum([1, 2, 3])) # 6 (global sum etkilenmedi)Fonksiyon içindeki sum local scope'ta. Global sum'a dokunmuyor. Ama yine de kafa karıştırıcı, toplam gibi bir isim kullanmak çok daha iyi.
Scope ile İlgili İnce Noktalar
Mutable Nesneler ve Scope
Global bir mutable nesneyi (list, dict, set) fonksiyon içinden global keyword'ü olmadan değiştirebilirsin:
global_liste = [1, 2, 3]
def eleman_ekle():
# global keyword'ü YOK ama çalışıyor!
global_liste.append(4) # Listeyi mutate ediyoruz, yeniden atamıyoruz
eleman_ekle()
print(global_liste) # [1, 2, 3, 4] ← Değişti!Neden? Çünkü global_liste.append(4) listeye atama yapmıyor, listeyi değiştiriyor (mutate). Python bunu "okuma" olarak değerlendiriyor.
Ama global_liste = [1, 2, 3, 4] yazsaydın, bu bir atama olurdu ve global keyword'ü gerekirdi:
global_liste = [1, 2, 3]
def sifirla():
# global keyword'ü olmadan
global_liste = [] # Bu yeni bir LOCAL değişken oluşturur!
print(f"Fonksiyon içi: {global_liste}") # []
sifirla()
print(f"Global: {global_liste}") # [1, 2, 3] ← Değişmedi!Bu ince fark çok sayıda bug'ın kaynağıdır. Kural basit: atama (=) local scope oluşturur. Mutasyon (.append, .update vb.) oluşturmaz.
Döngüler ve Scope
Python'da döngü değişkenleri ayrı bir scope oluşturmaz (bazı dillerden farklı olarak):
for i in range(5):
pass
print(i) # 4 ← Döngü değişkeni hâlâ erişilebilir!
# Comprehension'lar farklı (Python 3'te)
sonuc = [x for x in range(5)]
# print(x) # NameError! Comprehension kendi scope'unu oluşturur (Python 3)Bu, döngü + closure kombinasyonunda klasik bir tuzağa yol açar:
# ❌ Klasik tuzak
fonksiyonlar = []
for i in range(5):
fonksiyonlar.append(lambda: i) # Hepsi aynı i'ye referans tutar!
print([f() for f in fonksiyonlar])
# [4, 4, 4, 4, 4] ← Hepsi 4! Çünkü i'nin son değeri 4
# ✅ Çözüm: default argument ile yakalama
fonksiyonlar = []
for i in range(5):
fonksiyonlar.append(lambda x=i: x) # i'nin o anki değerini yakala
print([f() for f in fonksiyonlar])
# [0, 1, 2, 3, 4] ✅Bu tuzak, callback fonksiyonları oluştururken çok sık karşılaşılan bir sorundur.
locals() ve globals()
Python, mevcut scope'taki tüm değişkenleri görmeni sağlayan fonksiyonlar sunar:
x = "global_x"
y = "global_y"
def ornek(a, b):
c = a + b
print("Local değişkenler:", locals())
print("c" in locals()) # True
print("x" in locals()) # False (x global, local değil)
ornek(1, 2)
# Local değişkenler: {'a': 1, 'b': 2, 'c': 3}# Global scope'u görmek
print("Global değişkenler:")
for isim, deger in globals().items():
if not isim.startswith("_"): # Dunder'ları filtrele
print(f" {isim} = {deger}")💡 İpucu:
locals()veglobals()debugging için faydalı. Ama kodunda bunlarla değişken oluşturmak veya değiştirmek kötü practice.globals()["x"] = 5gibi şeyler yapmak programı anlaşılmaz hale getirir.
Pratik: Closure ile Konfigürasyon
Closure'ların gerçek dünyada en yaygın kullanımlarından biri: konfigürasyona göre fonksiyon üretmek.
def formatter_olustur(para_birimi="₺", ondalik=2, binlik_ayirici="."):
"""Para formatlama fonksiyonu üretir."""
def formatla(miktar):
# Ondalık kısmı ayır
tam = int(miktar)
kesir = round(miktar - tam, ondalik)
# Binlik ayırıcı ekle
tam_str = f"{tam:,}".replace(",", binlik_ayiraci)
kesir_str = f"{kesir:.{ondalik}f}"[1:] # Başındaki 0'ı at
return f"{tam_str}{kesir_str} {para_birimi}"
# Yerel değişken adı düzeltmesi
binlik_ayiraci = binlik_ayirici
return formatla
# Farklı ülkeler için formatter'lar
tr_format = formatter_olustur("₺", 2, ".")
us_format = formatter_olustur("$", 2, ",")
eu_format = formatter_olustur("€", 2, ".")
print(tr_format(1234567.89)) # 1.234.567.89 ₺
print(us_format(1234567.89)) # 1,234,567.89 $Daha basit ve temiz bir closure örneği:
def greeting_factory(template):
"""Şablon bazlı selamlama fonksiyonu üretir."""
def greet(name):
return template.format(name=name)
return greet
# Farklı diller/durumlar için
turkce = greeting_factory("Merhaba {name}, nasılsın?")
ingilizce = greeting_factory("Hello {name}, how are you?")
resmi = greeting_factory("Sayın {name}, toplantıya davetlisiniz.")
print(turkce("Ahmet")) # Merhaba Ahmet, nasılsın?
print(ingilizce("John")) # Hello John, how are you?
print(resmi("Dr. Yılmaz")) # Sayın Dr. Yılmaz, toplantıya davetlisiniz.Scope Hataları ve Çözümleri Özet Tablosu
| Hata | Neden | Çözüm |
|---|---|---|
UnboundLocalError | Fonksiyonda atama yapınca local sanılıyor | global veya nonlocal kullan (ama önce tasarımı sorgula) |
NameError | Değişken hiçbir scope'ta tanımlı değil | İsmi kontrol et, doğru scope'ta tanımla |
| Built-in gölgeleme | list = [1,2] gibi | Built-in isimlerden kaçın |
| Döngü + closure tuzağı | Tüm lambda'lar aynı değişkene referans | Default argument: lambda x=i: x |
| Global mutasyon | Mutable obje global keyword'süz değişiyor | Fonksiyon parametresi + return kullan |
Özet
LEGB kuralı Python'un değişken arama sıralamasıdır: Local → Enclosing → Global → Built-in. İlk bulunan yerde durur.
Local scope her fonksiyon çağrısında yeniden oluşturulur. Fonksiyon içi değişkenler dışarıdan erişilemez.
`global` keyword fonksiyon içinden global değişkeni değiştirmeye izin verir ama neredeyse her zaman daha iyi bir alternatif vardır — parametre geç, return et, class kullan.
`nonlocal` keyword iç fonksiyondan enclosing scope'taki değişkeni değiştirmeye izin verir. Closure'larda kullanılır.
Closure, dış fonksiyonun değişkenlerini "hatırlayan" iç fonksiyondur. Fonksiyon fabrikaları, konfigürasyon bazlı fonksiyon üretimi ve decorator'ların temelidir.
Name shadowing (özellikle built-in isimleri gölgeleme) sinsi bugların kaynağıdır.
list,dict,str,typegibi isimleri değişken adı olarak kullanmaktan kaçın.
AI Asistan
Sorularını yanıtlamaya hazır