İçeriğe geç
academia.sh

Ders 11 / 16

Lambda ve Yüksek Mertebeden Fonksiyonlar

İşlev bir nesnedir ve argüman olarak geçirilebilir: iki yüz öğede anahtar işlevi sorted, min ve max için öğe başına tam bir kez çağrılır — aynı işi elle yazan sıralama onu 39800 kez çağırır.

İçindekiler

Önceki derste süzme koşulu da öğe ifadesi de kavramanın içine gömülü kaldı. İkisi de bir kural anlatıyordu — “şunu geçir”, “şunu şuna dönüştür” — ama kural yazıldığı yerin dışında kullanılamıyordu. Aynı süzgeci ikinci bir kavramada tekrarlamak, onu tekrar yazmak demekti.

Kurallar adlandırılabilir. Bir işlev tam olarak budur: adlandırılmış bir kural. Bu dersin sorusu, o işlevin bir argüman olarak geçirilip geçirilemeyeceği ve geçirildiğinde kaç kez çağrıldığıdır. Sıralamada anahtar hesabı öğe başına bir kez mi yapılır, yoksa her karşılaştırmada yeniden mi? Aradaki fark iki yüz öğede iki sayı üretecek ve ikisi aynı büyüklükte olmayacak.

İşlev Bir Nesnedir

Python’da def bir bildirim değil, bir deyimdir: çalıştığında bir işlev nesnesi üretir ve onu bir ada bağlar. Bağlama başka her bağlama gibidir — nesne bir listeye konabilir, bir sözlükte değer olabilir, bir işleve argüman olarak geçirilebilir, bir işlevden döndürülebilir.

lambda aynı nesneyi ad bağlamadan üretir. Tek bir ifade taşır, return yazılmaz, sonucu ifadenin değeridir. Üretilen nesne def ile üretilenle aynı türdendir; ayrıldığı tek yer taşıdığı addır.

Bir işlevi argüman olarak alan ya da işlev döndüren işleve yüksek mertebeden fonksiyon (higher-order function) denir. Programlama Temelleri kursu bu kavramı fonksiyonel programlamaya girişte kurdu; burada kavram tekrarlanmaz. Ölçülen şey Python’un yerleşik yüksek mertebeden fonksiyonlarının verilen işlevi kaç kez çağırdığı ve kaç nesne var ettiğidir.

Anahtar İşlevinin Sözleşmesi

sorted, min ve max bir key argümanı kabul eder: her öğe için hesaplanacak karşılaştırma değerini veren işlev. Sözleşmenin ölçülebilir yanı şudur — bu işlev öğe başına kaç kez çağrılır?

İki yanıt akla gelir. Sıralama öğeleri birbiriyle karşılaştırır ve karşılaştırma sayısı öğe sayısından fazladır; anahtar her karşılaştırmada hesaplanıyorsa çağrı sayısı da öyle olur. Ya da anahtar bir kez hesaplanıp saklanır ve karşılaştırmalar saklanan değerler üzerinde yapılır. İkinci yol çağrı sayısını düşürür ama tutmayı artırır: her öğe için bir anahtar değeri canlı tutulmalıdır. Ölçüm hangisinin geçerli olduğunu söyler.

Ölçümün varsayımları:

  • YN36 — Ortak tanımın Oge sınıfı sayma düzeniyle aynen kullanılır; Oge yalnız üretim sayar, anahtar çağrısı ayrı bir sayaçta tutulur.
  • YN37 — Anahtar işlevi her çağrılışını kendi sayar; “kaç kez çağrıldı” sorusunun ölçüsü bu sayaçtır ve kâhin kurgunun kendisidir.
  • YN38 — Kaynak 200 öğelidir ve dört işlemin dördü de aynı kaynak üzerinde çalışır. Sayaç her işlemden önce sıfırlanır.
  • YN39 — Anahtar değeri -o.deger % 97 ile hesaplanır; sayı keyfîdir ve yalnız sıralamanın kaynak sırasından farklı bir sıra üretmesi için seçilmiştir.
  • YN40 — Elle yazılan seçmeli sıralama karşılaştırma başına anahtarı iki kez hesaplar; bu, anahtarı saklamayan bir yazımın davranışıdır ve ölçümün karşı ucudur. Sıralama bu derste bir algoritma olarak değil, çağrı sayısı üreten bir kurgu olarak kullanılır.
  • YN41sorted ile liste.sort ayrımı kimlikle ölçülür; kimlik is ile gösterilir, sayısı basılmaz.
  • YN42 — Eşleme ve süzme ölçümünde kurulum satırından hemen sonra sayaç okunur; öğe üreten işlev kasıtlı olarak yeni Oge üretir, böylece tembellik üretim üzerinden görünür.
  • YN43 — Üç işlevin ayrı nesneler olduğu ikişerli is karşılaştırmalarıyla gösterilir; kimlik sayısı basılmaz.
  • YN44 — Bağlama ölçümünde üç yazım da aynı üç işlevi kurar ve hepsi aynı öğeyle çağrılır; değişen tek şey çarpanın nereye yazıldığıdır.

Ölçüm

"""Islev bir nesnedir: anahtar islevi kac kez cagrilir, akis ne zaman var eder."""

import functools

SAYAC = {"uretilen": 0, "anahtar": 0}


class Oge:
    """Her uretimi sayan oge."""

    def __init__(self, deger):
        SAYAC["uretilen"] += 1
        self.deger = deger

    def __repr__(self):
        return f"Oge({self.deger})"


def sifirla():
    SAYAC["uretilen"] = 0
    SAYAC["anahtar"] = 0


def uretilen():
    return SAYAC["uretilen"]


def anahtar(o):
    """Ogenin siralama anahtari; her cagrilisini sayar."""
    SAYAC["anahtar"] += 1
    return -o.deger % 97


N = 200
KAYNAK = [Oge(i) for i in range(N)]


def elle_sec(ogeler, islev):
    """Her karsilastirmada anahtari yeniden hesaplayan secmeli siralama."""
    kalan, sirali = list(ogeler), []
    while kalan:
        enk = 0
        for i in range(1, len(kalan)):
            if islev(kalan[i]) < islev(kalan[enk]):
                enk = i
        sirali.append(kalan.pop(enk))
    return sirali


ISLEMLER = (("sorted(n, key=f)", lambda n: len(sorted(n, key=anahtar))),
            ("min(n, key=f)", lambda n: min(n, key=anahtar)),
            ("max(n, key=f)", lambda n: max(n, key=anahtar)),
            ("elle seçmeli sıralama", lambda n: len(elle_sec(n, anahtar))))

print(f"{'işlem':<24s} {'anahtar çağrısı':>16s} {'öğe başına':>11s}")
for ad, islem in ISLEMLER:
    sifirla()
    islem(KAYNAK)
    print(f"  {ad:<22s} {SAYAC['anahtar']:16d} {SAYAC['anahtar'] / N:11.1f}")

sifirla()
sirali = sorted(KAYNAK, key=anahtar)
print()
print(f"sorted(n) is n -> {sirali is KAYNAK}, var edilen {uretilen()},"
      f" paylaşılan {sum(1 for o in sirali if any(o is k for k in KAYNAK))}")
kopya = list(KAYNAK)
once = kopya
sonuc = kopya.sort(key=anahtar)
print(f"liste.sort dönen değer {sonuc!r}, kap aynı kaldı -> {once is kopya}")


def adli(o):
    return o.deger


adsiz = lambda o: o.deger
print()
print(f"{'işlev':<12s} {'çağrılabilir':>13s} {'__name__':>12s} {'tür aynı mı':>12s}")
for ad, f in (("def ile", adli), ("lambda ile", adsiz)):
    print(f"  {ad:<10s} {str(callable(f)):>13s} {f.__name__:>12s}"
          f" {str(type(f) is type(adli)):>12s}")

DEFTER = {"artir": lambda o: o.deger + 1, "iki_kat": lambda o: o.deger * 2}
print(f"sözlükte tutulan işlevler: {sorted(DEFTER)},"
      f" DEFTER['iki_kat'](Oge(21)) -> {DEFTER['iki_kat'](Oge(21))}")

sifirla()
esleme = map(lambda o: Oge(o.deger * 2), KAYNAK)
suzme = filter(lambda o: o.deger % 2 == 0, KAYNAK)
kurulumda = uretilen()
ilk = next(esleme)
print()
print(f"map ve filter kurulumda {kurulumda} nesne var etti,"
      f" ilk öğe istendiğinde {uretilen()}")
print(f"tümü gezildiğinde: map {len(list(esleme)) + 1} öğe,"
      f" filter {len(list(suzme))} öğe, üretilen {uretilen()}")

sifirla()
toplam = functools.reduce(lambda a, o: a + o.deger, KAYNAK, 0)
print(f"reduce ile toplam {toplam}, var edilen {uretilen()}")


def carpan_ureten(k):
    def carp(o):
        return o.deger * k
    return carp


uc = [carpan_ureten(k) for k in (1, 2, 3)]
ayri = all(a is not b for i, a in enumerate(uc) for b in uc[i + 1:])
print()
print(f"üç işlev birbirinden ayrı nesne -> {ayri},"
      f" sonuçlar {[f(Oge(10)) for f in uc]}")
print(f"carpan_ureten(2) is carpan_ureten(2) -> "
      f"{carpan_ureten(2) is carpan_ureten(2)}")

gec = [lambda o: o.deger * k for k in (1, 2, 3)]
erken = [lambda o, k=k: o.deger * k for k in (1, 2, 3)]
kismi = [functools.partial(lambda o, k: o.deger * k, k=k) for k in (1, 2, 3)]
print()
print(f"{'bağlama biçimi':<26s} {'nesne':>6s} {'sonuçlar':>16s}")
for ad, kap in (("gövdede serbest ad", gec), ("varsayılan argüman", erken),
                ("partial ile", kismi)):
    print(f"  {ad:<24s} {len(kap):6d} {str([f(Oge(10)) for f in kap]):>16s}")
işlem                     anahtar çağrısı  öğe başına
  sorted(n, key=f)                    200         1.0
  min(n, key=f)                       200         1.0
  max(n, key=f)                       200         1.0
  elle seçmeli sıralama             39800       199.0

sorted(n) is n -> False, var edilen 0, paylaşılan 200
liste.sort dönen değer None, kap aynı kaldı -> True

işlev         çağrılabilir     __name__  tür aynı mı
  def ile             True         adli         True
  lambda ile          True     <lambda>         True
sözlükte tutulan işlevler: ['artir', 'iki_kat'], DEFTER['iki_kat'](Oge(21)) -> 42

map ve filter kurulumda 0 nesne var etti, ilk öğe istendiğinde 1
tümü gezildiğinde: map 200 öğe, filter 100 öğe, üretilen 200
reduce ile toplam 19900, var edilen 0

üç işlev birbirinden ayrı nesne -> True, sonuçlar [10, 20, 30]
carpan_ureten(2) is carpan_ureten(2) -> False

bağlama biçimi              nesne         sonuçlar
  gövdede serbest ad            3     [30, 30, 30]
  varsayılan argüman            3     [10, 20, 30]
  partial ile                   3     [10, 20, 30]

Öğe Başına Bir Çağrı

Üst tablonun ilk üç satırı aynı sayıyı veriyor: 200 çağrı, öğe başına tam 1.0. Sıralama, en küçüğü bulma ve en büyüğü bulma — üçünde de anahtar işlevi her öğe için bir kez çağrılıyor, daha fazla değil.

min ve max için bu beklenendir; her öğeye bir kez bakmak yeter. sorted için değildir. Sıralama öğeleri birbiriyle karşılaştırmak zorundadır ve karşılaştırma sayısı öğe sayısından fazladır. Yine de anahtar çağrısı 200’de kalıyor: demek ki anahtarlar bir kez hesaplanıp saklanıyor ve bütün karşılaştırmalar saklanan değerler üzerinde yapılıyor.

Dördüncü satır bu kararın karşı ucunu gösteriyor. Aynı işi yapan, anahtarı hiç saklamayan ve her karşılaştırmada yeniden hesaplayan bir sıralama, aynı iki yüz öğede anahtarı 39800 kez çağırıyor — öğe başına 199.0. İki sayı arasındaki oran iki yüzdür ve bu oran öğe sayısı büyüdükçe büyür.

Kursun ekseninden okunuşu şudur: yerleşik sorted bir takas yapıyor. Anahtar değerlerini saklamak öğe sayısı kadar başvuru tutmak demektir; karşılığında çağrı sayısı öğe sayısına iner. Tutmadan kaçınan yazım — dördüncü satır — hiçbir ara değer saklamıyor ve bedelini çağrı sayısıyla ödüyor. Bu, ikinci dersin tersine çevrilmiş hâlidir: orada tutmayan biçim kazanıyordu, burada tutan biçim kazanıyor. Neyin tutulduğu kadar, tutmanın neyi ortadan kaldırdığı da sayılmalıdır.

Bu ölçüm bir sıralama algoritması karşılaştırması değildir; algoritmalar ve karmaşıklık çözümlemesi Algoritmalar kursunun konusudur. Buradaki dördüncü satır yalnız anahtarı saklamayan bir yazımın kaç çağrı ürettiğini göstermek için elle kuruldu.

Yeni Kap mı, Yerinde mi

Sonraki iki satır aynı işin iki yazımını ayırıyor. sorted yeni bir liste üretiyor: sorted(n) is n yanlış, var edilen Oge 0 ve paylaşılan 200. Sıralama öğeleri çoğaltmıyor, yalnız yeni bir kaba başka sırayla yazıyor — birinci dersin kap kurma ölçümünün aynısı.

liste.sort yeni kap kurmuyor: kap aynı kalıyor ve dönen değer None. Bu, Python’da yerinde çalışan yöntemlerin ortak imzasıdır ve bir sonucu vardır — n = n.sort() yazımı listeyi None ile değiştirir. Ayrım tek satırda görünür: sonuç isteniyorsa sorted, kabı korumak isteniyorsa sort.

İşlevin Nesne Olduğunun Kanıtı

Orta tablo iki işlevi karşılaştırıyor ve üç sütunun ikisinde aynı yanıtı veriyor. def ile tanımlanan ve lambda ile üretilen nesnelerin ikisi de çağrılabilir, ikisi de aynı türden. Ayrıldıkları tek sütun __name__: biri kendi adını taşıyor, öbürü <lambda>.

Bu tek fark önemsiz görünür ama bir sonraki dersin ölçtüğü şeyin habercisidir. Bir işlev nesnesi kendi adını bir öznitelikte taşır ve o öznitelik hata izlerinde, günlük kayıtlarında ve belge üretiminde kullanılır. Adı olmayan bir nesne bu bilgiyi veremez.

Sonraki satır nesne olmanın pratikteki karşılığını gösteriyor: iki işlev bir sözlükte değer olarak duruyor ve anahtarla çağrılıyor. Kural artık veriyle aynı yerde yaşıyor — adlandırılabilir, saklanabilir, geçirilebilir. Önceki dersin kavramaya gömülü kalan süzgeci burada bir sözlük değeri olabilirdi.

Tembel Yüksek Mertebeden Yerleşikler

Son üç satır map ve filter yerleşiklerini ölçüyor ve tanıdık bir sayı çıkıyor: kurulumda 0 nesne. İkisi de birer akış döndürüyor; verilen işlev kurulum anında hiç çağrılmıyor. İlk öğe istendiğinde üretilen 1 oluyor, tümü gezildiğinde 200.

Bu, üçüncü dersin iddiasının yerleşikler üzerindeki karşılığıdır: map üretimden kaçınmaz, onu geciktirir. İki yüz öğe istendiğinde iki yüz nesne doğar. Süzme de aynı düzendedir ve iki yüz öğeden 100’ünü geçirir; geçmeyenler için yeni nesne kurulmadığı için üretim sayısı yine iki yüzde kalır.

map ile bir kavrama arasındaki seçim bu yüzden bir maliyet seçimi değil, bir yazım seçimidir — biri akış verir, öbürü kap. İkisinin sayıları önceki iki derste ölçüldü ve burada değişmiyor.

Son satır üçüncü bir yüksek mertebeden aracı gösteriyor: standart kitaplığın reduce işlevi, iki argümanlı bir işlevi akış boyunca uygulayarak tek bir değere iner. Toplam 19900 ve var edilen 0 — hiçbir ara kap kurulmuyor, her adımda yalnız biriken değer taşınıyor. Kavrama bir kap, map bir akış, reduce bir değer üretir; üçü de aynı işlevi argüman olarak alır.

İşlev Döndüren İşlev

Son iki blok yönü tersine çeviriyor: işlev artık argüman değil, dönen değer.

carpan_ureten çağrıldığında gövdesindeki def çalışır ve yeni bir işlev nesnesi üretir. Üç çağrı üç ayrı nesne veriyor ve üçü ayrı sonuçlar üretiyor — her biri kendi çarpanını taşıyor. Aynı argümanla yapılan iki çağrı bile ayrı nesne veriyor: carpan_ureten(2) is carpan_ureten(2) yanlış. İşlev üretmek, her seferinde bir nesne var etmektir; on ayrı çarpan on ayrı işlev nesnesi demektir.

Programlama Temelleri kursu kapanışı — iç işlevin dış kapsamdaki adı taşıması — bir kavram olarak kurmuştu; burada eklenen tek şey o taşımanın nesne sayısıyla ölçülmesi ve taşınanın bir değer değil, bir ad olmasıdır. Son tablo bu ayrımın sonucunu veriyor.

Üç yazım da üç işlev üretiyor ve üçünde de nesne sayısı 3. Ama sonuçlar aynı değil. Gövdede serbest ad kullanan biçim [30, 30, 30] veriyor: üç işlevin üçü de aynı k adına bakıyor ve çağrıldıkları anda o adın son değeri 3. Kapanış k’nin değerini kopyalamadı, adına bağlandı.

Varsayılan argüman biçimi [10, 20, 30] veriyor. Python Temelleri kursunun fonksiyon tanımı dersi varsayılan değerin bir kez, tanım anında değerlendirildiğini ölçmüştü; orada bu bir tuzaktı, burada çözümdür. Değer tanım anında hesaplandığı için her işlev kendi çarpanını yanında taşır. Standart kitaplığın partial aracı aynı sonucu argümanı önceden bağlayarak verir.

Üç satır aynı yazımın üç okunuşudur ve aralarındaki fark ad ile değer arasındaki farktır. Kavramada döngü adı kavrama bitince kaybolur; ona bağlanan işlev kaybolmaz ve son değeri görür. Bir ada bağlanmak, bir değeri almak değildir.

Özet

  • def bir deyimdir ve bir işlev nesnesi üretir; lambda aynı türden nesneyi ad bağlamadan verir. İkisi de çağrılabilir, ikisi de aynı türdendir ve ayrıldıkları tek yer __name__’dir.
  • İşlev nesnesi bir kapta değer olarak tutulabilir ve argüman olarak geçirilebilir; bir işlevi argüman alan ya da döndüren işlev yüksek mertebeden fonksiyondur.
  • İki yüz öğede anahtar işlevi sorted, min ve max için 200 kez — öğe başına tam bir kez — çağrılır. Anahtarı saklamayan elle yazılmış sıralama aynı işlevi 39800 kez çağırır.
  • sorted yeni kap kurar ve 200 öğeyi paylaşır, hiçbirini çoğaltmaz; liste.sort yerinde çalışır ve None döndürür.
  • map ve filter kurulumda 0 nesne var eder, tümü gezildiğinde 200; süzme iki yüz öğeden 100’ünü geçirir. reduce ise hiç kap kurmadan tek değere iner.

Sonraki Adım

Bu derste işlevler hep argüman olarak geçti; hiçbiri değiştirilmedi. Oysa bir işlev döndürülebiliyorsa, bir işlevi alıp yerine geçecek başka bir işlev döndürmek de mümkündür — özgün davranışı koruyup çevresine bir şeyler ekleyen bir sarmalayıcı. Sonraki ders bu kalıbı ölçer: bir işlevi sarmalamak kaç nesne var eder, özgün nesneye ne olur, ve sarmalanmış işlevin __name__ özniteliği ne söyler? Bu dersin önemsiz görünen tek sütunu orada bir sonuç üretecek.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat