- 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 @wrapskullanan bir dekoratör ve argüman alan bir dekoratör yazmaklru_cacheile 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).
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:
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:
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
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:
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
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:
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:
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:
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}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:
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:
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.
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.
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
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.
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;
nonlocalonlara yeniden değer atamayı sağlar. def f'in üstündeki@deco,f = deco(f)demektir; sarmalayıcı*args, **kwargsve@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.
def load(): satırının üstüne yazılan @timer ne anlama gelir?