İçindekiler
Python dekoratörleri, bir fonksiyonun davranışını kaynak kodunu değiştirmeden genişletmenin zarif bir yoludur. Ancak dekoratörümüzü dikkatsizce yazarsak orijinal fonksiyonun adı, docstring’i ve diğer metadata bilgileri ortadan kaybolabilir. Fonksiyon çalışmaya devam eder ama sahneye başka birinin kimliğiyle çıkar. Neyse ki functools.wraps, bu küçük kimlik krizini çözmek için standart kütüphanede bizi bekler.
``
Dekoratör aslında ne yapar?
Bir dekoratör, bir fonksiyonu parametre olarak alır ve yerine genellikle başka bir fonksiyon döndürür. Matematiksel olarak dekoratör dönüşümünü şöyle düşünebiliriz:
\[D(f) = g\]Burada $f$ orijinal fonksiyon, $D$ dekoratör ve $g$ çağrıların yönlendirildiği yeni fonksiyondur. Python’daki @dekorator sözdizimi ise temelde şu eşitliğin kısa yazımıdır:
Basit bir zamanlama dekoratörü yazalım:
import time
def zaman_olc(func):
def wrapper(*args, **kwargs):
baslangic = time.perf_counter()
sonuc = func(*args, **kwargs)
sure = time.perf_counter() - baslangic
print(f"Çalışma süresi: {sure:.6f} saniye")
return sonuc
return wrapper
@zaman_olc
def topla(a, b):
"""İki sayıyı toplar."""
return a + b
wrapper, farklı imzalara sahip fonksiyonlarla çalışabilmek için *args ve **kwargs kullanır. Önce zamanı kaydeder, orijinal fonksiyonu çalıştırır ve geçen süreyi ekrana yazar.
Metadata neden kaybolur?
Dekoratör uygulandıktan sonra topla değişkeni artık orijinal fonksiyonu değil, döndürülen wrapper fonksiyonunu gösterir. Bu nedenle aşağıdaki sonuçlar şaşırtıcı olabilir:
print(topla.__name__) # wrapper
print(topla.__doc__) # None
Python açısından ortada gizem yoktur: Yeni nesnenin metadata’sı okunmaktadır. Sorun özellikle dokümantasyon araçlarında, hata ayıklamada, testlerde ve fonksiyon adını kullanan framework’lerde belirginleşir.
| Özellik | Dekoratörsüz | Sıradan dekoratör sonrası |
|---|---|---|
__name__ |
topla |
wrapper |
__doc__ |
İki sayıyı toplar. |
None |
__module__ |
Tanımlandığı modül | Dekoratörün modülü |
| İzleme kolaylığı | Yüksek | Daha düşük |
functools.wraps kurtarmaya geliyor
Çözüm, iç fonksiyonun üzerine @wraps(func) eklemektir:
import time
from functools import wraps
def zaman_olc(func):
@wraps(func)
def wrapper(*args, **kwargs):
baslangic = time.perf_counter()
sonuc = func(*args, **kwargs)
sure = time.perf_counter() - baslangic
print(f"Çalışma süresi: {sure:.6f} saniye")
return sonuc
return wrapper
@zaman_olc
def topla(a, b):
"""İki sayıyı toplar."""
return a + b
print(topla.__name__) # topla
print(topla.__doc__) # İki sayıyı toplar.
wraps, kendi başına sihirli bir kopyalama motoru değildir; functools.update_wrapper fonksiyonunun dekoratör biçiminde kullanılmasını sağlar. Varsayılan olarak __module__, __name__, __qualname__, __doc__, __annotations__ ve __type_params__ gibi nitelikleri wrapper’a aktarır. Ayrıca wrapper’ın sözlüğünü günceller.
| Yaklaşım | İsim korunur mu? | Docstring korunur mu? | Orijinale erişim |
|---|---|---|---|
Düz wrapper |
Hayır | Hayır | Closure üzerinden |
| Elle kopyalama | Kısmen | Kısmen | Genellikle hayır |
@wraps(func) |
Evet | Evet | __wrapped__ ile |
Bonus: __wrapped__ ve introspection
wraps, wrapper’a __wrapped__ niteliğini de ekler. Böylece orijinal fonksiyona doğrudan ulaşılabilir:
print(topla.__wrapped__(3, 4)) # 7; zaman ölçümü yapılmaz
inspect.signature gibi introspection araçları da bu zinciri takip ederek anlamlı sonuçlar üretir:
from inspect import signature
print(signature(topla)) # (a, b)
Bu özellik yalnızca kozmetik değildir. IDE desteği, otomatik API dokümantasyonu, bağımlılık enjeksiyonu ve test araçları doğru metadata’ya güvenir. Özetle dekoratör yazarken @wraps(func) kullanmak, emniyet kemeri takmak gibidir: Kod onsuz da ilerleyebilir, fakat ilk sert virajda neden gerekli olduğunu anlarsınız.
Yorumlar