Ana içeriğe geç
Kesit Python
Orta · Pythonic kod

Fonksiyonel araçlar: lambda, map, sorted key ve kapanış (closure)

Fonksiyonları değer gibi kullanmayı öğrenir; lambda, map/filter/reduce, çok anahtarlı sorted, any/all, closure ve functools.partial ile veriyi kısa ve esnek biçimde işlersin.

Uygulamada aç, 23 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ç.

Neden önemli?

Anadolu Market'in rapor ekibinden şu istek gelsin: "Siparişleri önce şehre göre alfabetik, aynı şehirde tutara göre büyükten küçüğe sırala." Ya da Ilgaz Bank'tan: "Her şube için farklı komisyon oranıyla hesaplama yapan bir fonksiyon üret." Bu isteklerin ortak noktası, fonksiyonun kendisinin bir veri gibi ele alınmasıdır: sıralama ölçütünü fonksiyon olarak ver, komisyon oranını fonksiyona gömüp yeni fonksiyon üret. Python'da fonksiyonlar birinci sınıf (first-class) nesnelerdir: değişkene atanır, listeye konur, argüman olarak geçirilir, başka fonksiyondan döndürülür. Bu dersteki araçların hepsi bu tek fikrin üstüne kuruludur.

Fonksiyon bir değerdir

def kdv_ekle(tutar):
    return round(tutar * 1.20, 2)

def indirim_uygula(tutar):
    return round(tutar * 0.90, 2)

# Değişkene ata (parantez yok: çağırmıyoruz, işaret ediyoruz)
islem = kdv_ekle
print(islem(100))

# Listeye koy, sırayla uygula
boru = [indirim_uygula, kdv_ekle]
tutar = 1000
for f in boru:
    tutar = f(tutar)
    print(f.__name__, "->", tutar)

# Argüman olarak geçir
def uygula(fonk, degerler):
    return [fonk(d) for d in degerler]

print(uygula(kdv_ekle, [100, 250]))

kdv_ekle ile kdv_ekle() arasındaki fark kritik: ilki fonksiyon nesnesinin kendisi, ikincisi onun çağrılıp dönen sonucu. Argüman olarak her zaman parantezsiz halini verirsin.

lambda: tek satırlık isimsiz fonksiyon

lambda parametreler: ifade, tek bir ifade döndüren küçük bir fonksiyon üretir. Adı yoktur, return yazılmaz, gövdesi tek ifadedir. Yeri, başka fonksiyona kısa bir ölçüt verirken.

kare = lambda x: x * x
print(kare(7))

# Asıl kullanım: argüman olarak
siparisler = [("IST-1", 1200), ("ANK-2", 350), ("IZM-3", 4800)]
print(sorted(siparisler, key=lambda s: s[1]))
print(max(siparisler, key=lambda s: s[1]))
lambda'yı değişkene atamakare = lambda x: x * x çalışır ama PEP 8 buna karşıdır: hata mesajlarında adı "<lambda>" görünür ve docstring yazamazsın. Bir ad verecek kadar önemliyse def yaz. lambda yalnız "o anda, o satırda" gereken küçük ölçütler içindir.

sorted ve key: tek, çoklu ve karışık yönlü sıralama

sorted(kaynak, key=f), her öğe için f(öğe)'yi hesaplar ve bu değere göre sıralar; öğenin kendisi değişmez. Birden fazla ölçüt için key bir demet döndürür: Python demetleri soldan sağa karşılaştırır.

siparisler = [
    {"no": 101, "sehir": "İstanbul", "tutar": 1200},
    {"no": 102, "sehir": "Ankara", "tutar": 350},
    {"no": 103, "sehir": "İstanbul", "tutar": 4800},
    {"no": 104, "sehir": "Ankara", "tutar": 2100},
    {"no": 105, "sehir": "İzmir", "tutar": 90},
]

# Tek anahtar
for s in sorted(siparisler, key=lambda s: s["tutar"], reverse=True):
    print(s["no"], s["tutar"])
print("---")

# Çok anahtar: şehir artan, sonra tutar artan
for s in sorted(siparisler, key=lambda s: (s["sehir"], s["tutar"])):
    print(s["sehir"], s["tutar"])
print("---")

# Karışık yön: şehir artan, tutar AZALAN -> sayıyı eksiyle ters çevir
for s in sorted(siparisler, key=lambda s: (s["sehir"], -s["tutar"])):
    print(s["sehir"], s["tutar"])

Karışık yön hilesi sayılar için -değer. Metin alanını ters çevirmek gerekirse iki aşamalı sıralama yaparsın: önce ikincil anahtara göre sırala, sonra birincil anahtara göre reverse=True ile; Python'ın sıralaması kararlı (stable) olduğu için eşit öğelerin önceki sırası korunur. Sözlük alanlarına erişim için operator.itemgetter("sehir", "tutar") lambda'nın hazır ve biraz daha hızlı karşılığıdır.

Türkçe harf sırasıVarsayılan sıralama Unicode kod noktasına göredir; "İzmir" ile "Istanbul" beklediğin sırada gelmeyebilir ve "ç", "z"den sonra gelir. Basit işler için key=str.casefold yeter; tam Türkçe sıralama için locale ya da özel bir anahtar fonksiyonu gerekir.

map, filter, reduce

Üç klasik: map(f, kaynak) her öğeye f uygular, filter(f, kaynak) f'nin True döndürdüğü öğeleri tutar, functools.reduce(f, kaynak) öğeleri ikişer ikişer birleştirerek tek değere indirger. map ve filter tembel yineleyici (iterator) döndürür.

from functools import reduce

tutarlar = [1200, 350, 4800, 90]

print(list(map(lambda t: t * 1.2, tutarlar)))
print(list(filter(lambda t: t > 500, tutarlar)))
print(reduce(lambda a, b: a + b, tutarlar))
print(reduce(lambda a, b: a if a > b else b, tutarlar))

# map ile hazır fonksiyon: lambda gerekmez
metin_tutarlar = ["1200", "350"]
print(list(map(int, metin_tutarlar)))
print(sum(map(int, metin_tutarlar)))

# Aynı işler comprehension ile
print([t * 1.2 for t in tutarlar])
print([t for t in tutarlar if t > 500])
print(sum(tutarlar), max(tutarlar))

Son blok önemli bir dersi gösteriyor: map/filter'ın lambda ile kullanımı çoğu zaman comprehension'dan daha az okunur, ve reduce'un en yaygın iki kullanımı zaten sum ve max olarak hazır. Kural şu:

DurumTercihÖrnek
Hazır fonksiyon uygulanacakmapmap(int, satirlar), map(str.strip, satirlar)
Dönüşüm bir ifadecomprehension[t * 1.2 for t in tutarlar]
Süzme + dönüşüm birliktecomprehension[t * 1.2 for t in tutarlar if t > 500]
Toplam, en büyük, en küçüksum/max/minsum(tutarlar)
Özel ikili birleştirmereduceSözlükleri birleştirmek, çarpım
Fonksiyonu zincire argüman vermekmap/filterBoru hattında (pipeline) ölçüt dışarıdan geliyorsa

any ve all

İki küçük ama çok kullanılan araç. any "en az biri doğru mu", all "hepsi doğru mu" sorar; ikisi de üreteç (generator) ifadesiyle en güzel okunur ve sonucu bulur bulmaz durur (kısa devre).

basvurular = [
    {"ad": "Ayşe", "gelir": 25000, "kara_liste": False},
    {"ad": "Mehmet", "gelir": 9000, "kara_liste": False},
    {"ad": "Zeynep", "gelir": 40000, "kara_liste": True},
]
print(any(b["kara_liste"] for b in basvurular))
print(all(b["gelir"] >= 10000 for b in basvurular))
print(all(b["ad"] for b in basvurular))   # boş ad yok mu?
print(any([]), all([]))                     # boş kaynakta: False, True

Closure: ayarını içinde taşıyan fonksiyon

Bir fonksiyon, içinde tanımlanan başka bir fonksiyonu döndürürse, içteki fonksiyon dıştakinin değişkenlerini "hatırlar". Bu yapıya closure (kapanış) denir. Ilgaz Bank'ta her şubenin komisyon oranı farklı olsun: tek bir fabrika (factory) fonksiyonundan şubeye özel hesaplayıcılar üretirsin.

def komisyon_hesaplayici(oran, asgari):
    def hesapla(tutar):
        return max(round(tutar * oran, 2), asgari)
    return hesapla

kadikoy = komisyon_hesaplayici(0.015, 5)
merkez = komisyon_hesaplayici(0.010, 10)

print(kadikoy(1000), kadikoy(100))
print(merkez(1000), merkez(100))

# İçteki fonksiyon neyi hatırlıyor?
print(kadikoy.__closure__[0].cell_contents, kadikoy.__closure__[1].cell_contents)

# Sayaç: durum tutan closure (nonlocal gerekir)
def sayac():
    adet = 0
    def arttir():
        nonlocal adet
        adet += 1
        return adet
    return arttir

say = sayac()
print(say(), say(), say())

nonlocal, içteki fonksiyonun dıştaki değişkene yazmasına izin verir; yalnız okuyorsan gerekmez. Closure, "ayar + davranış" paketlemenin hafif yoludur; sınıf yazmaya değmeyecek kadar küçük durumlar için idealdir. Bir sonraki derste göreceğin dekoratörler (decorator) tümüyle closure'dur.

functools.partial: argümanı önceden doldur

Varolan bir fonksiyonun bazı argümanlarını sabitleyip yeni fonksiyon üretmek istiyorsan closure yazmana gerek yok; partial bunu tek satırda yapar.

from functools import partial

def fiyatla(tutar, kdv, indirim):
    return round(tutar * (1 - indirim) * (1 + kdv), 2)

standart = partial(fiyatla, kdv=0.20, indirim=0.0)
kampanya = partial(fiyatla, kdv=0.20, indirim=0.15)

print(standart(1000))
print(kampanya(1000))
print(list(map(kampanya, [100, 200])))

# Hazır fonksiyonlarla da
ikili = partial(int, base=2)
print(ikili("1010"))

partial ürünü sıradan bir çağrılabilirdir; map'e, sorted'ın key'ine, bir dekoratöre argüman olarak verilebilir. Doldurduğun argümanları çağrı anında yeniden geçersen senin verdiğin kazanır.

Hepsi bir arada: Anadolu Market sipariş raporu

from functools import partial

siparisler = [
    {"no": 101, "sehir": "İstanbul", "tutar": 1200, "durum": "teslim"},
    {"no": 102, "sehir": "Ankara", "tutar": 350, "durum": "iptal"},
    {"no": 103, "sehir": "İstanbul", "tutar": 4800, "durum": "kargoda"},
    {"no": 104, "sehir": "Ankara", "tutar": 2100, "durum": "teslim"},
]

def esik_ustu(esik, s):
    return s["durum"] != "iptal" and s["tutar"] >= esik

buyukler = list(filter(partial(esik_ustu, 1000), siparisler))
sirali = sorted(buyukler, key=lambda s: (s["sehir"], -s["tutar"]))
for s in sirali:
    print(f'{s["sehir"]:<10}{s["no"]:>5}{s["tutar"]:>8}')
print("Hepsi 1000 üstü mü:", all(s["tutar"] >= 1000 for s in sirali))
Derinleş

Derinleş: fonksiyonlarla çalışan araç kutusu

lambda, sorted/key, map/filter/reduce, any/all, closure ve partial'ı gördün. Bu bölüm, iş kodunda bunları daha temiz ve doğru kullanmanı sağlayan araçları ekliyor: operator modülü, kararlı sıralamayla karışık yönlü sıralama, Türkçe alfabetik sıralama, min/max'ın default parametresi, fonksiyon tabloları (dispatch) ve fonksiyon birleştirme (composition). Closure'ların kapsam ayrıntıları için Kapsam ve kapanışlar dersine bakabilirsin.

operator modülü: lambda yazmadan anahtar

key=lambda k: k["tutar"] o kadar sık yazılır ki standart kütüphane bunun hazırını sunar. itemgetter sözlük anahtarı ya da indeksle, attrgetter nesne özniteliğiyle, methodcaller metot çağrısıyla değer çeken fonksiyonlar üretir. Birden çok alan verirsen demet döndürür; çoklu sıralama anahtarı hazır olur. Ayrıca biraz daha hızlıdır ve adı ne yaptığını söyler.

lambda ileoperator ile
lambda k: k["tutar"]itemgetter("tutar")
lambda k: (k["sehir"], k["tutar"])itemgetter("sehir", "tutar")
lambda d: d[1]itemgetter(1)
lambda m: m.bakiyeattrgetter("bakiye")
lambda s: s.strip()methodcaller("strip")
lambda a, b: a * boperator.mul
from operator import itemgetter, mul
from functools import reduce

siparisler = [
    {"id": 3, "sehir": "İzmir", "tutar": 820.0},
    {"id": 1, "sehir": "Ankara", "tutar": 1830.0},
    {"id": 2, "sehir": "Ankara", "tutar": 450.0},
]
for s in sorted(siparisler, key=itemgetter("sehir", "tutar")):
    print(s["id"], s["sehir"], s["tutar"])

kac = itemgetter("id", "tutar")
print([kac(s) for s in siparisler])
print("Faktöriyel 5:", reduce(mul, range(1, 6), 1))

Kararlı sıralama: karışık yönler, metinlerde bile

Sayısal alanda azalan sıralama için eksi işareti hilesini (-k["tutar"]) gördün. Ama metni "eksi" yapamazsın: "şehre göre artan, aynı şehirde ada göre azalan" isteğinde ne yaparsın? Python'un sıralaması kararlıdır (stable): eşit anahtarlı elemanların önceki sırası korunur. Bu yüzden önce ikincil ölçüte göre, sonra birincil ölçüte göre sıralarsın; her geçiş kendi yönünü alır.

from operator import itemgetter

calisanlar = [("Satış", "Ayşe"), ("BT", "Can"), ("Satış", "Mehmet"), ("BT", "Ali"), ("Satış", "Zeynep")]

adim1 = sorted(calisanlar, key=itemgetter(1), reverse=True)   # ikincil: ad, azalan
adim2 = sorted(adim1, key=itemgetter(0))                      # birincil: departman, artan
for dep, ad in adim2:
    print(dep, ad)
Sıra ters!Çok geçişli sıralamada en önemsiz ölçütten başlarsın, en önemli ölçütü en son uygularsın. Son geçiş "kazanır"; önceki geçişler yalnızca eşitlikleri çözer.

Türkçe alfabetik sıralama

sorted(["Çorum", "Bursa", "Ağrı", "Iğdır", "İzmir"]) Unicode kod sırasına göre sıralar: Ç, I, İ, Ş, Ğ gibi harfler Latin harflerinin sonuna düşer. Türkçe sıralama için kendi anahtarını yazarsın: her harfi Türk alfabesindeki sıra numarasına çeviren bir fonksiyon. (Kendi bilgisayarında locale.strxfrm ile işletim sisteminin Türkçe ayarı da kullanılabilir ama sunucuda o ayar kurulu olmayabilir.)

ALFABE = "abcçdefgğhıijklmnoöprsştuüvyz"
SIRA = {h: i for i, h in enumerate(ALFABE)}

def tr_kucuk(metin):
    return metin.replace("I", "ı").replace("İ", "i").lower()

def tr_anahtar(metin):
    return [SIRA.get(h, 100 + ord(h)) for h in tr_kucuk(metin)]

iller = ["Çorum", "Bursa", "Şanlıurfa", "Iğdır", "İzmir", "Ağrı", "Uşak", "Ordu", "Ödemiş"]
print("Varsayılan:", sorted(iller))
print("Türkçe    :", sorted(iller, key=tr_anahtar))

min, max: key ve default

max(siparisler, key=itemgetter("tutar")) en büyük tutarlı siparişi (sözlüğü) döndürür, tutarın kendisini değil. Boş veride ValueError fırlatır; default= parametresi bunu önler. Eşitlikte ilk karşılaşılan eleman döner.

from operator import itemgetter

depolar = [{"ad": "Gebze", "km": 42, "stok": 0}, {"ad": "Tuzla", "km": 18, "stok": 5}, {"ad": "Esenyurt", "km": 18, "stok": 9}]
uygun = [d for d in depolar if d["stok"] > 0]
print(min(uygun, key=itemgetter("km"))["ad"])                     # eşitlikte ilki
print(min((d for d in depolar if d["stok"] > 100), key=itemgetter("km"), default=None))

Fonksiyon tablosu ve fonksiyon birleştirme

Fonksiyonlar değer olduğu için sözlüğe konabilir. Uzun bir if/elif zinciri yerine "işlem adı → fonksiyon" tablosu (dispatch table) kurarsın; yeni işlem eklemek tek satırdır. Fonksiyonları sıralı bir boru hattına bağlamak (composition) ise reduce ile birkaç satırdır.

from functools import reduce

KOMISYON = {
    "havale": lambda t: 0.0,
    "eft": lambda t: 4.5,
    "swift": lambda t: max(25.0, round(t * 0.002, 2)),
}

def komisyon(tur, tutar):
    try:
        return KOMISYON[tur](tutar)
    except KeyError:
        raise ValueError(f"bilinmeyen işlem türü: {tur}") from None

print(komisyon("eft", 1000), komisyon("swift", 50000))

def boru(*fonksiyonlar):
    return lambda x: reduce(lambda deger, f: f(deger), fonksiyonlar, x)

temizle = boru(str.strip, str.lower, lambda s: s.replace(" ", "_"))
print(temizle("  Anadolu Market Rapor "))
reduce'un başlangıç değerireduce(f, liste) boş listede TypeError verir. Üçüncü argüman (initializer) hem boş durumu çözer hem de türü belirler: reduce(mul, sayilar, 1). Toplama için ise reduce yerine sum kullan.

map ve filter'ın iki küçük sırrı

  • map(f, a, b) birden çok yinelenebilir alır ve f(a[i], b[i]) çağırır; en kısa olan bitince durur: list(map(mul, fiyatlar, adetler)).
  • filter(None, veri) yalnız doğru (truthy) değerleri bırakır: boş metinleri, 0'ları ve None'ları atmanın kısa yolu. Dikkat: 0 da gider; 0 geçerli bir değerse kullanma.
Gerçek işteKurumsal kodda fonksiyonel araçlar en çok üç yerde görülür: (1) raporlarda çok ölçütlü sıralama (itemgetter ve iki geçişli sıralama), (2) iş kurallarının tablo hâlinde tutulması (komisyon türleri, KDV oranları, durum geçişleri: if/elif yerine sözlük), (3) veri temizleme adımlarının sıralı bir fonksiyon listesi olarak yazılması. Üçünde de amaç aynı: kural değişince kodun yapısı değil, yalnızca tablo ya da liste değişsin. Türkçe sıralama ise müşteri listesi, il listesi gibi kullanıcıya gösterilen her ekranda istenir; "Çorum neden en sonda?" sorusu mutlaka gelir.
Uzmanlaş

Saf fonksiyon ve test edilebilirlik

Fonksiyonel programlamanın kalbi saf fonksiyondur (pure function): aynı girdiye her zaman aynı çıktıyı verir, dışarıdaki hiçbir şeyi değiştirmez. kdv_ekle(100) saftır; "global listeye ekleyen" bir fonksiyon değildir. Saf fonksiyonlar test etmesi en kolay kod parçalarıdır: girdi ver, çıktıyı assert et, bitti. İş kodunda veri dönüşümlerini saf fonksiyonlara, yan etkileri (dosya yazma, veritabanı, e-posta) ise sınırlı birkaç noktaya toplarsan hem test hem hata ayıklama (debugging) kolaylaşır.

Closure'ın geç bağlanma tuzağı

Döngü içinde lambda ya da iç fonksiyon üretirken dikkat: [lambda: i for i in range(3)] listesindeki üç fonksiyon da çağrıldığında 2 döndürür; çünkü i çağrı anında okunur, döngü bitmiştir. Çözüm değeri o anda bağlamak: lambda i=i: i ya da partial. Bu tuzak kod incelemelerinde (code review) ve mülakatlarda sık çıkar.

İş hayatında nasıl görünür?

pandas'ta df["tutar"].map(f) ve df.apply(f, axis=1), bu dersteki map'in sütun üzerinde çalışan hali. Web çerçevelerinde yol (route) işleyicileri, olay dinleyicileri ve geri çağırma (callback) fonksiyonları hep "fonksiyonu argüman olarak geçir" kalıbıdır. Konfigürasyonla davranış üretme (şube bazlı komisyon, müşteri segmentine göre indirim) ise closure ve partial'ın doğal alanıdır; bu desenle bir "if şube == ... elif şube == ..." zincirinden kurtulursun.

Alıştırmalara geç (23)