Ana içeriğe geç
Kesit Python
Temel · Fonksiyonlar ve modüller

Kapsam ve modüller

Bir adın nerede geçerli olduğunu (LEGB) öğrenir, global tuzağından kaçınır, import biçimlerini ve __name__ kalıbını kavrar, math/random/statistics ile standart kütüphaneye ilk adımı atarsın.

Uygulamada aç, 21 alıştırmayı çözÜcretsiz, hesap gerekmez, kurulum yok.

Bu sayfa dersin okuma sürümüdür. Metin bir kutuyu çalıştırmanı ya da adıma geçmeni söylüyorsa dersi uygulamada aç.

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.

KatmanNerede tanımlanırÖrnek
LocalFonksiyon gövdesinde atanan adlar ve parametrelerkesinti, tutar
Enclosingİç fonksiyonu saran dış fonksiyonun yerelleriiç fonksiyonun gördüğü oran
GlobalDosyanın en üst düzeyikomisyon_orani
Built-inPython'ın kendisiprint, 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ı
Yerleşik adı ezmesum = 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İç içe fonksiyonlarda dıştaki fonksiyonun değişkenini değiştirmek için 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 ve sys kısacaos.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.
Derinleş

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())
Güvenli ad seçimiSık gölgelenen yerleşikler için Türkçe ya da daha açıklayıcı adlar kullan: 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ımNe getirir?Ne zaman?
import mathmath adınıVarsayılan; kaynağı her kullanımda belli
import statistics as stKısa takma adıUzun adlı ya da gelenekli modüller (np, pd)
from math import ceil, floorYalnız seçilen adlarıBirkaç adı sık kullanıyorsan
from math import sqrt as kokSeçilen adı yeni adlaAd çakışmasını önlemek için
from math import *Her şeyiEtkileş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.

Gerçek işteProjelerde modül düzeni genellikle şöyledir: sabitler ve ayarlar bir modülde (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.
Uzmanlaş

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.

Alıştırmalara geç (21)