← Kursa Dön
📄 Text · 15 min

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:

  1. Local — Fonksiyonun kendi içi

  2. Enclosing — Dış fonksiyonun içi (iç içe fonksiyonlarda)

  3. Global — Modül (dosya) seviyesi

  4. Built-in — Python'un yerleşik isimleri (print, len, range vb.)

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()  # local

Her 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()  # global
x = "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()  # enclosing

Local 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 defined

sonuc 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 = 1

Her ç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.53975

Global 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 assignment

Ne 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: 3

global 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 gelir

2. 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: global keyword'ünü kullanman gereken durum neredeyse hiç yoktur. Eğer kendini global yazarken 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 hedefler

  • nonlocal → 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))    # 1000

Burada olan şey:

  1. carpici_fabrikasi(2) çağrıldığında carpan=2 olur

  2. İçerideki carp fonksiyonu carpan'a erişir

  3. carpici_fabrikasi bitse bile carp, carpan=2'yi hatırlar

  4. Bu "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ğer

Closure Ö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))  # True

Name 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ür

Bu 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 = 500

Fonksiyon İç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() ve globals() debugging için faydalı. Ama kodunda bunlarla değişken oluşturmak veya değiştirmek kötü practice. globals()["x"] = 5 gibi ş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

HataNedenÇözüm
UnboundLocalErrorFonksiyonda atama yapınca local sanılıyorglobal veya nonlocal kullan (ama önce tasarımı sorgula)
NameErrorDeğişken hiçbir scope'ta tanımlı değilİsmi kontrol et, doğru scope'ta tanımla
Built-in gölgelemelist = [1,2] gibiBuilt-in isimlerden kaçın
Döngü + closure tuzağıTüm lambda'lar aynı değişkene referansDefault argument: lambda x=i: x
Global mutasyonMutable obje global keyword'süz değişiyorFonksiyon 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, type gibi isimleri değişken adı olarak kullanmaktan kaçın.