İçeriğe geç
Educora
İleri22 dk16 / 42

Dekoratörler ve kapanışlar

Nesne olarak fonksiyonlar, LEGB kuralı, kapanışlar ve `nonlocal`, kendi dekoratörlerin, argümanlı dekoratörler, `functools.wraps` ve `lru_cache` ile önbellekleme.

Kendini test et
Bu derste öğreneceklerin
  • Fonksiyonları nesne olarak aktarmak ve döndürmek, varsayılan değerlerin ne zaman hesaplandığını açıklamak
  • Kapanış oluşturmak ve nonlocal'ı doğru kullanmak
  • @wraps kullanan bir dekoratör ve argüman alan bir dekoratör yazmak
  • lru_cache ile sonuçları önbelleğe almak ve sınırlarını bilmek

Yirmi fonksiyonun var ve her birinin çağrılarını günlüğe yazmasını, bir ağ hatasından sonra yeniden denemesini ya da önceden hesapladığı sonuçları hatırlamasını istiyorsun. Aynı satırları yirmi yere kopyalamak kötü bir fikirdir. Python'un cevabı dekoratördür: bir fonksiyonun üstüne yazılan @lru_cache gibi tek bir satır, koduna dokunmadan ona yeni bir davranış ekler. Flask, FastAPI ve pytest gibi çatılar dekoratörler üzerine kuruludur. Kendi dekoratörünü yazmak için iki fikir gerekir: fonksiyonlar nesnedir ve iç fonksiyonlar çevrelerini hatırlayabilir; bunlara kapanış (closure) denir.

Fonksiyonlar birer nesnedir

Python'da fonksiyon, bir sayı ya da liste gibi sıradan bir nesnedir. Onu başka bir ada atayabilir, bir sözlükte saklayabilir, başka bir fonksiyona verebilir ve bir fonksiyondan döndürebilirsin. map(square, ...) bu yüzden çalışır: fonksiyonun kendisini veririz, onu çağırmayız (parantez yok).

Python
def square(x):
    return x * x

f = square
print(f(5), f.__name__)

operations = {'double': lambda x: x * 2, 'square': square}
for name, op in operations.items():
    print(name, op(7))

print(list(map(square, [1, 2, 3])))
▸ Beklenen çıktı
25 square
double 14
square 49
[1, 4, 9]

Fonksiyon bir nesne olduğu için nitelikleri de vardır: __name__, __doc__ ve parametrelerin varsayılan değerlerinin saklandığı __defaults__. Bu değerler her çağrıda değil, def çalıştığında bir kez hesaplanır. Python'un en ünlü tuzaklarından biri tam olarak bununla açıklanır:

Python
def add_item(item, basket=[]):
    basket.append(item)
    return basket

print(add_item('apple'))
print(add_item('pear'))
print(add_item.__defaults__)

def add_item_fixed(item, basket=None):
    if basket is None:
        basket = []
    basket.append(item)
    return basket

print(add_item_fixed('apple'), add_item_fixed('pear'))
▸ Beklenen çıktı
['apple']
['apple', 'pear']
(['apple', 'pear'],)
['apple'] ['pear']

Kapanışlar ve nonlocal

Python bir adı dört kapsamda, LEGB sırasıyla arar: Local (geçerli fonksiyon), Enclosing (çevreleyen fonksiyonlar), Global (modül) ve Built-in (len, print gibi yerleşik adlar). Yani bir iç fonksiyon, onu çevreleyen fonksiyonun değişkenlerini okuyabilir. İşin ilginci, dış fonksiyon bittikten sonra da bu değişkenleri saklar:

Python
def make_multiplier(factor):
    def multiply(x):
        return x * factor
    return multiply

double = make_multiplier(2)
triple = make_multiplier(3)
print(double(10), triple(10))
print(double.__closure__[0].cell_contents)
▸ Beklenen çıktı
20 30
2
Tanım
Kapanış (closure)

Kapanış, çevreleyen kapsamdan hatırladığı değişkenlerle birlikte bir fonksiyondur. Burada double, factor = 2 değerini hatırlar: değer bir hücrede (cell) tutulur ve __closure__ üzerinden görülebilir.

Çevreleyen değişkeni okumak serbesttir, ama ona değer atarsan Python adı yeni bir yerel değişken sayar ve count += 1 satırı UnboundLocalError ile çöker. **nonlocal** anahtar sözcüğü, bu adın çevreleyen fonksiyona ait olduğunu söyler. make_counter()'ın her çağrısı ayrı ve dışarıdan erişilemeyen bir count oluşturur:

Python
def make_counter():
    count = 0
    def increment():
        nonlocal count
        count += 1
        return count
    return increment

counter = make_counter()
print(counter(), counter(), counter())
other = make_counter()
print(other())
▸ Beklenen çıktı
1 2 3
1
Python
funcs = [lambda: i for i in range(3)]
print([f() for f in funcs])

funcs = [lambda i=i: i for i in range(3)]
print([f() for f in funcs])
▸ Beklenen çıktı
[2, 2, 2]
[0, 1, 2]

Dekoratörler

Dekoratör, bir fonksiyon alıp yeni bir fonksiyon döndüren fonksiyondur. Bu genellikle, asıl fonksiyonu çağırmadan önce ve sonra bir şey yapan bir sarmalayıcıdır (wrapper). def add'in üstündeki @log_calls satırı, add = log_calls(add) yazımının sözdizimsel şekeridir. Sarmalayıcı *args, **kwargs alır; bu yüzden her imzaya sahip fonksiyonla çalışır:

Python
def log_calls(func):
    def wrapper(*args, **kwargs):
        print(f'calling {func.__name__}{args}')
        result = func(*args, **kwargs)
        print(f'{func.__name__} returned {result}')
        return result
    return wrapper

@log_calls
def add(a, b):
    return a + b

total = add(2, 3)
print(total, add.__name__)
▸ Beklenen çıktı
calling add(2, 3)
add returned 5
5 wrapper

Son satıra bak: fonksiyonun adı artık wrapper. Dekoratör add'in yerini aldı ve onun adını ve belge dizisini kaybetti; bu da hata ayıklamayı, belgelendirmeyi ve pytest gibi araçları bozar. Çözüm **functools.wraps** dekoratörüdür: asıl fonksiyonun __name__, __doc__ ve diğer niteliklerini sarmalayıcıya kopyalar. Sarmalayıcı kendi durumunu bir nitelikte de saklayabilir:

Python
from functools import wraps

def count_calls(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        wrapper.calls += 1
        return func(*args, **kwargs)
    wrapper.calls = 0
    return wrapper

@count_calls
def greet(name):
    """Return a greeting."""
    return f'Salam, {name}!'

print(greet('Leyla'))
print(greet('Elvin'))
print(greet.calls, greet.__name__, greet.__doc__)
▸ Beklenen çıktı
Salam, Leyla!
Salam, Elvin!
2 greet Return a greeting.

Argümanlı dekoratörler

Peki @retry(times=3) nasıl çalışır? Burada önce retry(times=3) çağrılır ve onun sonucu asıl dekoratördür. Yani argümanlı bir dekoratörün üç katmanı vardır: argümanları alan bir fabrika, fonksiyonu alan dekoratör ve her çağrıda çalışan sarmalayıcı. Kapanışlar sayesinde sarmalayıcı hem times'ı hem func'ı görür:

Python
from functools import wraps

def retry(times):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, times + 1):
                try:
                    return func(*args, **kwargs)
                except ConnectionError as e:
                    print(f'attempt {attempt} failed: {e}')
            raise ConnectionError(f'{func.__name__} failed {times} times')
        return wrapper
    return decorator

calls = 0
@retry(times=3)
def fetch_rates():
    global calls
    calls += 1
    if calls < 3:
        raise ConnectionError('server busy')
    return {'USD': 1.7}

print(fetch_rates())
▸ Beklenen çıktı
attempt 1 failed: server busy
attempt 2 failed: server busy
{'USD': 1.7}
İlk iki çağrı başarısız olur, üçüncüsü başarılı olur; çağıran taraf hataları hiç görmez. Üç başarısızlıktan sonra ise sarmalayıcı hatayı kendisi fırlatırdı.

Dekoratörler üst üste de yazılabilir. Aşağıdan yukarıya uygulanırlar; def'e en yakın olan dekoratör fonksiyonu ilk sarar:

Python
def bold(func):
    return lambda: '<b>' + func() + '</b>'

def italic(func):
    return lambda: '<i>' + func() + '</i>'

@bold
@italic
def title():
    return 'Educora'

print(title())
▸ Beklenen çıktı
<b><i>Educora</i></b>

Hazır dekoratörler: lru_cache

Standart kütüphanede birçok yararlı dekoratör vardır. **functools.lru_cache, bir fonksiyonun sonuçlarını argümanlarına göre bir sözlükte saklar; buna memoizasyon** denir. Saf özyinelemeli bir fib(80) yaklaşık 7,6 · 10¹⁶ çağrı yapardı; önbellekle ise her değer yalnızca bir kez hesaplanır:

Python
from functools import lru_cache

@lru_cache(maxsize=None)
def fib(n):
    return n if n < 2 else fib(n - 1) + fib(n - 2)

print(fib(80))
print(fib.cache_info())
▸ Beklenen çıktı
23416728348467685
CacheInfo(hits=78, misses=81, maxsize=None, currsize=81)

cache_info(), yalnızca 81 gerçek hesaplama yapıldığını (misses), kalan 78 isteğin önbellekten yanıtlandığını (hits) gösterir. maxsize=None sınırsız önbellek demektir; maxsize=128 gibi bir sayı verirsen en uzun süredir kullanılmayan (LRU) kayıtlar önce silinir. Python 3.9'dan beri @functools.cache, lru_cache(maxsize=None)'un kısa biçimidir. Sonraki derslerde başka yerleşik dekoratörlerle de tanışacaksın: @property, @classmethod, @staticmethod, @dataclass ve @contextmanager.

Alıştırma

Bir fonksiyonun döndürdüğü karakter dizisini büyük harfe çeviren bir uppercase dekoratörü yaz. Fonksiyonun adını koruması için @wraps kullan.

Alıştırma · Python
from functools import wraps

def uppercase(func):
    # return a wrapper (with @wraps) that returns func's result in upper case
    ...

@uppercase
def greet(name):
    return f'salam, {name}'

print(greet('Aysel'))
print(greet.__name__)
▸ Beklenen çıktı
SALAM, AYSEL
greet
Alıştırma

make_accumulator(start) fonksiyonunu yaz: her çağrıda x'i biriken toplama ekleyip yeni toplamı döndüren bir add(x) fonksiyonu döndürsün. Her biriktiricinin kendi toplamı olmalıdır.

Alıştırma · Python
def make_accumulator(start=0):
    # return a function add(x) that adds x to a running total and returns it
    ...

acc = make_accumulator(100)
print(acc(10))
print(acc(5))
other = make_accumulator()
print(other(1))
▸ Beklenen çıktı
110
115
1

Önemli noktalar

  • Fonksiyonlar nesnedir: aktarılabilir, döndürülebilir ve saklanabilir; varsayılan değerler def çalıştığında bir kez hesaplanır.
  • Kapanış, çevreleyen fonksiyonun değişkenlerini hatırlar; nonlocal onlara yeniden değer atamayı sağlar.
  • def f'in üstündeki @deco, f = deco(f) demektir; sarmalayıcı *args, **kwargs ve @wraps(func) kullanır.
  • Argümanlı dekoratör bir fabrikadır: @retry(times=3), retry(times=3)(f) demektir.
  • lru_cache, hash'lenebilir argümanlı saf fonksiyonların sonuçlarını hatırlar.

Kendini test et

10 soru. Her doğru cevap XP kazandırır.

1 / 10
def load(): satırının üstüne yazılan @timer ne anlama gelir?