Bir isim nerede görünür?
Ilgaz Bank'ta günlük işlem raporu üreten bir betik yazdın. Fonksiyonun içinde toplam adında bir değişken var, dışarıda da başka bir toplam. Hangisi hangisi? Python bunu kapsam (scope) kurallarıyla çözer: her ad belirli bir bölgede geçerlidir. Fonksiyon içinde atanan ad o fonksiyona aittir, dışarıdan görünmez; fonksiyon bittiğinde silinir.
komisyon_orani = 0.002 # global (modül düzeyi)
def komisyon(tutar):
kesinti = tutar * komisyon_orani # global okunur
return round(kesinti, 2)
print(komisyon(15000))
print(komisyon_orani)
try:
print(kesinti) # yerel ad dışarıdan görünmez
except NameError as e:
print("Hata:", e)
kesinti fonksiyonun yerel (local) değişkenidir; dışarıda yoktur. komisyon_orani ise global: modülün en üst düzeyinde tanımlandı, her fonksiyon onu okuyabilir.
LEGB sırası
Python bir adı gördüğünde dört katmanı sırayla arar: Local (içinde bulunulan fonksiyon), Enclosing (iç içe fonksiyonlarda dıştaki fonksiyon), Global (modül), Built-in (print, len gibi yerleşikler). İlk bulduğunda durur.
| Katman | Nerede tanımlanır | Örnek |
|---|---|---|
| Local | Fonksiyon gövdesinde atanan adlar ve parametreler | kesinti, tutar |
| Enclosing | İç fonksiyonu saran dış fonksiyonun yerelleri | iç fonksiyonun gördüğü oran |
| Global | Dosyanın en üst düzeyi | komisyon_orani |
| Built-in | Python'ın kendisi | print, len, sum |
def rapor_hazirla(hesaplar):
baslik = "Ilgaz Bank Günlük Özet" # enclosing
def satir(ad, bakiye):
return baslik[:12] + " | " + ad + ": " + str(bakiye) # baslik dıştan okunur
for ad, bakiye in hesaplar:
print(satir(ad, bakiye))
rapor_hazirla([("Ayşe", 12500), ("Mehmet", 830)])
print(len("deneme")) # built-in katmanı
sum = 0 ya da list = [1, 2] yazarsan built-in sum/list gölgelenir ve sonraki satırlarda sum(fiyatlar) "int object is not callable" hatası verir. Değişken adlarını yerleşiklerden seçme: toplam, liste de.global tuzağı
Globali okumak serbesttir. Ama fonksiyon içinde ona atama yaparsan Python yeni bir yerel oluşturur; global değişmez. Hem okuyup hem değiştirmeye kalkarsan UnboundLocalError alırsın.
sayac = 0
def islem_kaydet_yanlis():
sayac_yerel = sayac + 1 # okumak sorun değil
return sayac_yerel
def islem_kaydet_global():
global sayac
sayac += 1
islem_kaydet_global()
islem_kaydet_global()
print("global sayaç:", sayac)
def islem_kaydet_temiz(sayac):
return sayac + 1 # saf: değer al, yeni değer döndür
yeni = islem_kaydet_temiz(sayac)
print("temiz yol:", yeni)
global sayac satırı "bu fonksiyonda sayac derken modüldekini kastediyorum" der. Çalışır, ama bir programda on tane fonksiyon aynı globali değiştiriyorsa hangi değeri kimin bozduğunu bulmak kabusa döner. Kural: değeri parametre olarak al, yenisini return et. global yalnızca küçük betiklerde ve bilinçli kullanılan bir istisnadır.
nonlocal kullanılır. Nadir gerekir; dekoratör (decorator) ve kapanış (closure) konularında karşına çıkacak.Modül nedir, nasıl içe aktarılır?
Bir .py dosyası aynı zamanda bir modüldür (module). fiyat.py içine kdv_hesapla yazdıysan başka dosyadan import fiyat deyip fiyat.kdv_hesapla(100) diye çağırırsın. Python'la gelen yüzlerce modüle standart kütüphane (standard library) denir; hiçbir şey kurmadan kullanılır. Dört import biçimi var:
import math # 1) modülü tümüyle al, math. öneki ile kullan print(math.ceil(1234.1)) from math import sqrt, floor # 2) yalnızca istenen adları al print(sqrt(144), floor(7.9)) import statistics as st # 3) kısa takma ad (alias) print(st.mean([120, 85, 230])) from random import randint as rastgele_tam # 4) ada takma ad print(type(rastgele_tam(1, 6)))
Kurumsal kodda en yaygın olanlar 1 ve 3: math.sqrt yazımı, sqrt'nin nereden geldiğini okuyana söyler. from x import * ise bütün adları dosyaya boca eder; hangi ad nereden geldi belli olmaz, kaçın.
if __name__ == "__main__"
Her modülün __name__ adlı bir değişkeni vardır. Dosya doğrudan çalıştırılıyorsa değeri "__main__", başka bir dosya tarafından import edildiyse dosyanın adıdır. Bu sayede aynı dosya hem kütüphane hem de çalıştırılabilir betik olabilir: fonksiyonlar her zaman tanımlanır, ama "demo" ya da "ana akış" kodu yalnızca doğrudan çalıştırıldığında koşar.
def faiz_hesapla(anapara, yillik_oran, ay):
"""Basit faiz: anapara * oran * (ay / 12)"""
return round(anapara * yillik_oran * ay / 12, 2)
def main():
print("Ilgaz Bank mevduat hesabı")
print("6 ay, 50.000 TL, %40:", faiz_hesapla(50000, 0.40, 6))
print("__name__ değeri:", __name__)
if __name__ == "__main__":
main()
Bu dosyayı başka bir yerden import faiz dersen yalnızca faiz_hesapla ve main tanımlanır; demo satırları basılmaz. Yazdığın her betiğe bu kalıbı koymak, ileride test yazarken ve kodu başkasıyla paylaşırken hayat kurtarır.
Standart kütüphaneye giriş: math, random, statistics
import math, random, statistics
# math: matematik
print(math.ceil(2.1), math.floor(2.9), round(2.5), round(3.5))
print(math.sqrt(2), math.pi)
# statistics: temel istatistik
bakiyeler = [1200, 4500, 980, 4500, 13000]
print("ortalama:", statistics.mean(bakiyeler))
print("medyan:", statistics.median(bakiyeler))
print("mod:", statistics.mode(bakiyeler))
# random: rastgelelik, seed ile tekrarlanabilir
random.seed(42)
print(random.randint(1, 100))
print(random.choice(["İstanbul", "Ankara", "İzmir"]))
print(random.sample(range(1000, 2000), 3))
random.seed(42) rastgele sayı üretecini (generator) sabit bir başlangıca getirir; aynı seed, aynı dizi. Test yazarken ve sahte veri üretirken bu şart: aksi hâlde testin her koşuda farklı veri görür. round'un 2.5'i 2'ye, 3.5'i 4'e yuvarladığına dikkat et; bu "bankacı yuvarlaması"dır (round half to even), math.floor ve math.ceil ise her zaman aşağı/yukarı gider.
os.getcwd() çalışma klasörünü, os.listdir() dosyaları, sys.argv komut satırı argümanlarını, sys.version Python sürümünü verir. Dosya işlemleri dersinde os ve pathlib'e döneceğiz. Tarayıcı ortamında dosya sistemi bellek içidir, o yüzden burada os.listdir çıktısı bilgisayarındakinden farklı görünür.UnboundLocalError'u gerçekten anlamak
Python bir fonksiyonu çalıştırmadan önce derler ve gövdede atama yapılan her adı o fonksiyonun yerel adı olarak işaretler. Bu karar satır sırasına bakmaz: atama gövdenin sonunda bile olsa, ad baştan itibaren yereldir. sayac += 1 hem okuma hem atama yaptığı için sayac yerel sayılır; okunmak istendiğinde yerel sayac henüz bir değere bağlanmamıştır.
sayac = 0
def oku():
print("okuma serbest:", sayac) # atama yok: global okunur
def artir():
try:
sayac += 1 # atama var: sayac yerel!
except UnboundLocalError as h:
print("Hata:", h)
def artir_dogru(deger):
return deger + 1 # en temizi: parametre al, sonuç döndür
oku()
artir()
sayac = artir_dogru(sayac)
print(sayac)
İç içe fonksiyonlarda dıştaki fonksiyonun değişkenini değiştirmek için global'in kardeşi nonlocal var. Onu ve fonksiyon döndüren fonksiyonları Kapanışlar ve kapsam dersinde ayrıntılı göreceksin.
Döngü ve comprehension değişkenlerinin kapsamı
Python'da if ve for blokları yeni bir kapsam açmaz: döngü değişkeni döngü bittikten sonra da yaşar ve son değerini korur. Comprehension'lar ([x * 2 for x in ...]) ise kendi küçük kapsamlarına sahiptir; içlerindeki değişken dışarı sızmaz.
for i in range(3):
pass
print("döngüden sonra i =", i)
kareler = [j * j for j in range(4)]
print(kareler, "j" in globals()) # j dışarı sızmadı
if True:
blok_ici = "if bloğu kapsam açmaz"
print(blok_ici)
Yerleşik adları gölgelemek
LEGB'nin son halkası B (built-in): print, len, sum, list, id, input, max gibi adlar yerleşik (builtins) modülündedir. Aynı adla bir değişken oluşturursan, o kapsamda yerleşik fonksiyon gölgelenir (shadowing) ve çağırmaya çalıştığında anlamsız hatalar alırsın. Hangi adların yerleşik olduğunu builtins modülüne, Python'un ayrılmış sözcüklerini (if, for, class...) keyword modülüne sorabilirsin.
import builtins, keyword
fiyatlar = [10, 20, 30]
sum = 0 # gölgeleme!
for f in fiyatlar:
sum += f
try:
print(sum(fiyatlar))
except TypeError as h:
print("Hata:", h)
del sum # yerel adı sil, yerleşik geri gelir
print(sum(fiyatlar))
for ad in ["list", "toplam", "class", "id", "2sehir"]:
print(ad, hasattr(builtins, ad), keyword.iskeyword(ad), ad.isidentifier())
list yerine urunler, sum yerine toplam, id yerine musteri_id, type yerine tur. Bir yerleşik adını gerçekten kullanman gerekiyorsa PEP 8 sona alt çizgi önerir: type_, id_.import'un incelikleri ve modül adı çakışması
| Yazım | Ne getirir? | Ne zaman? |
|---|---|---|
import math | math adını | Varsayılan; kaynağı her kullanımda belli |
import statistics as st | Kısa takma adı | Uzun adlı ya da gelenekli modüller (np, pd) |
from math import ceil, floor | Yalnız seçilen adları | Birkaç adı sık kullanıyorsan |
from math import sqrt as kok | Seçilen adı yeni adla | Ad çakışmasını önlemek için |
from math import * | Her şeyi | Etkileşimli denemeler dışında kullanma |
Python bir modülü ararken önce çalıştırdığın dosyanın klasörüne, sonra standart kütüphaneye ve yüklü paketlere bakar (bu liste sys.path'te). Bu yüzden kendi dosyana random.py, statistics.py ya da math.py adını verirsen, import random standart kütüphaneyi değil seninkini yükler ve AttributeError: module 'random' has no attribute 'randint' gibi şaşırtıcı hatalar alırsın. Dosyalarına standart modüllerin adlarını verme.
Bir modül ilk import'ta bir kez çalıştırılır ve sys.modules önbelleğine (cache) konur; sonraki import'lar aynı nesneyi döndürür. Aşağıdaki örnek bunu dosya bile oluşturmadan, modülü elle kurarak gösteriyor:
import sys, types
kaynak = 'print("ayarlar yükleniyor...")\nKDV = 0.20\ndef kdv_dahil(x):\n return round(x * (1 + KDV), 2)\n'
modul = types.ModuleType("ayarlar")
exec(kaynak, modul.__dict__) # modül kodunu kendi ad alanında çalıştır
sys.modules["ayarlar"] = modul # önbelleğe koy
import ayarlar # önbellekten gelir, kod tekrar çalışmaz
print(ayarlar.kdv_dahil(100), ayarlar is modul, ayarlar.__name__)
random ve statistics: ders dışında kalanlar
import random, statistics rng = random.Random(42) # kendi tohumlu üreteci: global random'u etkilemez urunler = ["çay", "kahve", "su", "kek"] print(rng.choice(urunler)) print(rng.sample(urunler, 2)) # tekrarsız 2 seçim print(rng.choices(urunler, weights=[5, 3, 1, 1], k=4)) # ağırlıklı, tekrarlı kopya = urunler[:] rng.shuffle(kopya) # yerinde karıştırır, None döner print(kopya) sureler = [12, 7, 9, 8, 10, 11, 95, 9, 10, 1] print(statistics.median(sureler), statistics.mode(sureler)) print(statistics.quantiles(sureler, n=4)) # çeyrekler: Q1, Q2, Q3 print(round(statistics.stdev(sureler), 2))
random.Random(tohum) nesnesi, testlerde ve simülasyonlarda tekrarlanabilir sonuç verirken programın geri kalanındaki random çağrılarını etkilemez; global random.seed ise herkesin sırasını değiştirir. Çeyrekler (quantiles) ise aykırı değer (outlier) bulmanın en yaygın yolu olan IQR kuralının temelidir.
ayarlar.py, BÜYÜK_HARF adlarla), iş kuralları saf fonksiyonlar olarak başka modüllerde, giriş noktası ise if __name__ == "__main__": ile korunan küçük bir main(). Global değişkenle durum taşımak yerine fonksiyonlar parametre alıp sonuç döndürür. Paket yapısı, __init__.py ve göreli import'ları Modüller ve paketler dersinde, üçüncü parti paket kurmayı Sanal ortam (virtual environment) ve pip dersinde göreceksin.Modül bir kez yüklenir
Aynı modülü on dosyada import etsen de Python onu bir kez çalıştırır ve sys.modules sözlüğünde saklar; sonraki importlar oradan döner. Bu yüzden modülün en üst düzeyine yazdığın kod (örneğin bir veritabanı bağlantısı) yalnızca ilk importta koşar. Modülün üst düzeyinde "iş" yapma; fonksiyon tanımla, işi main() içinde yap.
Paketler ve kendi modüllerin
Klasörün içine __init__.py koyarsan o klasör bir paket (package) olur: from anadolu.fiyat import kdv_hesapla. Python modülü bulmak için sys.path listesindeki klasörlere sırayla bakar; ilk sırada çalıştırılan dosyanın klasörü vardır. "ModuleNotFoundError" aldığında ilk kontrol edeceğin şey bu listedir. Git ve proje yapısı dersinde paket düzenine ayrıntılı gireceğiz.
Kapsam neden önemli?
Globale bağımlı fonksiyon, dışarıdaki durumu bilmeden test edilemez. Bir üretim sisteminde "fonksiyon dün çalışıyordu, bugün bozuldu" hatalarının büyük kısmı bir yerde değiştirilen global bir değerden çıkar. Fonksiyona ihtiyacı olan her şeyi parametre olarak vermek (dependency injection fikrinin en sade hâli) kodu hem test edilebilir hem taşınabilir yapar.