Python'da dekoratörler: nedir, nasıl çalışır ve nasıl yazılır
Python'da dekoratör, bir fonksiyon alıp yeni bir fonksiyon (genellikle bir sarmalayıcı) döndüren fonksiyondur; çağrıdan önce ya da sonra loglama, süre ölçme, önbellekleme veya yetki kontrolü gibi davranış ekler. @dekoratör yazımı func = dekoratör(func) kısayoludur; sarmalayıcının orijinal fonksiyonun adını ve docstring'ini koruması için functools.wraps kullanılır.
Python'da dekoratör, bir fonksiyon alıp başka bir fonksiyon döndüren bir fonksiyondur; döndürülen genellikle orijinali saran bir sarmalayıcıdır ve çağrıya ek davranış kazandırır: loglama, süre ölçme, önbellekleme, yetki kontrolü. Dekoratörler, bir fonksiyonun üstüne @ad satırı yazılarak uygulanır ve Flask, FastAPI, pytest gibi kütüphanelerin ve standart kütüphanenin her yerinde görülür.
Dekoratör sade bir dille nedir
Bir hediye ve onu saran kâğıdı düşünün. Hediye (fonksiyon) aynı kalır, ama dışına ilk önce devreye giren bir katman eklenir. Dekoratör böyle bir ambalajdır: çağrı yapıldığında önce sarmalayıcının kodu çalışır, o orijinal fonksiyonu çağırır ve sonucu üzerinde bir şey yapabilir.
Bu, Python'da fonksiyonların sıradan nesneler olmasıyla mümkündür. Fonksiyonlar başka fonksiyonlara verilebilir, fonksiyonlardan döndürülebilir ve fonksiyonların içinde tanımlanabilir. Dekoratörler bu üç özelliğin üzerine kuruludur.
Dekoratör nasıl çalışır: @ bir sözdizimi kısayoludur
En basit dekoratörü ele alalım; çağrıdan önce ve sonra bir mesaj yazdırır:
def log_calls(func):
def wrapper():
print(f"{func.__name__} çağrılıyor")
result = func()
print(f"{func.__name__} tamamlandı")
return result
return wrapper
@log_calls
def hello():
print("Merhaba!")
hello()
hello çağrılıyor
Merhaba!
hello tamamlandı
def üstündeki @log_calls satırı, fonksiyon tanımlandıktan sonra yazılan şu satıra tamamen eşdeğerdir:
def hello():
print("Merhaba!")
hello = log_calls(hello)
Artık hello adı wrapper'ı gösterir; orijinal fonksiyon ise bir kapanımın içinde yaşar, çünkü wrapper dış fonksiyondaki func değişkenini hatırlar. Önemli bir ayrıntı: dekoratör yalnızca bir kez, def çalıştığında, yani modül içe aktarıldığında uygulanır; sarmalayıcı ise her çağrıda devreye girer.
Kapanımlar ve dekoratörler
Kapanım (closure), dış fonksiyon işini bitirdikten sonra da onun değişkenlerini hatırlayan iç fonksiyondur. Kapanımlar olmasaydı dekoratörler de olmazdı: wrapper, func'a bu sayede erişir. Kapanım içinde durum da tutulabilir; örneğin çağrı sayacı:
def count_calls(func):
calls = 0
def wrapper(*args, **kwargs):
nonlocal calls
calls += 1
print(f"{func.__name__}: {calls}. çağrı")
return func(*args, **kwargs)
return wrapper
Python'da dekoratör yazmak: genel şablon
İlk örneğin iki eksiği var: sarmalayıcı argüman kabul etmiyor ve orijinal fonksiyonun adını "yutuyor". Her dekoratörün başlangıç noktası olması gereken çalışan şablon şöyledir:
import functools
def my_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
# çağrıdan önce yapılacaklar
result = func(*args, **kwargs)
# çağrıdan sonra yapılacaklar
return result
return wrapper
Şablonun üç kuralı var:
*args, **kwargs— sarmalayıcı her türlü argümanı kabul eder ve değiştirmeden iletir.return result— sonucu döndürmeyi unutmayın; aksi halde dekore edilmiş fonksiyon her zamanNoneverir.@functools.wraps(func)— orijinal fonksiyonun meta verilerini sarmalayıcıya taşır.
Çalışma süresini ölçen bir dekoratör örneği:
import functools
import time
def timer(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
elapsed = time.perf_counter() - start
print(f"{func.__name__} {elapsed:.3f} sn sürdü")
return result
return wrapper
@timer
def slow_sum(n):
return sum(range(n))
slow_sum(10_000_000) # slow_sum 0.142 sn sürdü
functools.wraps neden gerekli
wraps olmadan, dekore edilmiş fonksiyonun adı ve docstring'i değişir; dışarıdan wrapper görünür:
def plain(func):
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
@plain
def add(a, b):
"""İki sayıyı toplar."""
return a + b
print(add.__name__, add.__doc__) # wrapper None
@functools.wraps(func) ile sarmalayıcı orijinalin __name__, __doc__, __module__, __qualname__ ve __annotations__ değerlerini alır; ayrıca __wrapped__ özniteliğinde orijinale bir referans tutulur. Bu, hata ayıklama ve hata izleri, help(), adıyla fonksiyon kaydeden framework'ler ve gerçek imzayı gösteren inspect.signature için önemlidir.
Parametreli dekoratör (argümanlı)
Bazen dekoratörün ayarlara ihtiyacı olur: @retry(times=3), @app.route("/"). Bunun için bir iç içe katman daha eklenir. Dış fonksiyon parametreleri alır ve gerçek dekoratörü döndürür; bu yapıya dekoratör fabrikası denir.
import functools
def repeat(times):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
result = None
for _ in range(times):
result = func(*args, **kwargs)
return result
return wrapper
return decorator
@repeat(times=3)
def ping():
print("ping")
ping() # ping, ping, ping — üç satır
Çağrı sırası şöyledir: repeat(times=3) decorator'ı döndürür, o da ping'e uygulanıp wrapper'ı verir. Yani @repeat(times=3), ping = repeat(times=3)(ping) anlamına gelir.
Pratik bir örnek olarak hata durumunda yeniden deneme:
import functools
import time
def retry(times=3, delay=1.0, exceptions=(Exception,)):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, times + 1):
try:
return func(*args, **kwargs)
except exceptions:
if attempt == times:
raise
time.sleep(delay)
return wrapper
return decorator
@retry(times=5, delay=0.5, exceptions=(ConnectionError,))
def fetch_rate():
...
Bir fonksiyonda birden fazla dekoratör
Dekoratörler üst üste yazılabilir. Uygulanma sırası alttan yukarıdır: def'e en yakın olan ilk uygulanır.
@timer
@log_calls
def report():
...
# aynı anlama gelir:
report = timer(log_calls(report))
Çağrı sırasında dış katman (timer) ilk devreye girer ve log_calls'un çalışma süresini de ölçer. Sıra önemlidir: örneğin biri yetki kontrolü, diğeri sonuç önbellekleme yapıyorsa hangisinin dışta olacağı davranışı değiştirir.
Standart ve yerleşik Python dekoratörleri
Birçok faydalı dekoratör zaten dilde ve standart kütüphanede bulunur:
| Dekoratör | Ne yapar |
|---|---|
@property | metodu yalnızca okunabilir bir özniteliğe çevirir; @x.setter ile yönetilen bir özniteliğe |
@staticmethod | self almayan sınıf metodu; sınıfın ad alanında duran sıradan bir fonksiyon |
@classmethod | metot örnek yerine sınıfı (cls) alır; alternatif kurucular bu şekilde yazılır |
@functools.cache, @functools.lru_cache | sonuçları argümanlarına göre önbelleğe alır |
@functools.wraps | fonksiyonun meta verilerini sarmalayıcıya kopyalar |
@dataclasses.dataclass | sınıf için __init__, __repr__, __eq__ üretir |
@contextlib.contextmanager | bir üreteci with için bağlam yöneticisine çevirir |
Önbellekleme: functools.cache ve lru_cache
Önbellekleyen dekoratör, her argüman kümesi için sonucu saklar ve tekrar çağrıldığında hesaplamadan döndürür. Klasik örnek, önbellek olmadan üssel sürede çalışan özyinelemeli Fibonacci sayılarıdır:
import functools
@functools.cache
def fib(n):
return n if n < 2 else fib(n - 1) + fib(n - 2)
print(fib(100)) # 354224848179261915075
functools.cache (Python 3.9'da geldi) tüm sonuçları sınırsız saklar. @functools.lru_cache(maxsize=128) ise yalnızca son kayıtları tutar ve eskileri atar; uzun süre çalışan servisler için daha güvenlidir. İstatistiklere fib.cache_info() ile bakılır, fib.cache_clear() ile temizlenir. Bu tür fonksiyonların argümanları özetlenebilir olmalıdır: argüman olarak liste verirseniz TypeError: unhashable type: 'list' alırsınız. Ayrıca yalnızca saf fonksiyonları önbelleğe alın: aynı argümanlarla her zaman aynı değeri döndüren ve yan etkisi olmayanları.
property dekoratörü ne yapar
@property, bir metoda parantez olmadan, öznitelik gibi erişmenizi sağlar; gerekirse atamayı da denetleyebilirsiniz:
class Account:
def __init__(self, balance):
self._balance = balance
@property
def balance(self):
return self._balance
@balance.setter
def balance(self, value):
if value < 0:
raise ValueError("Bakiye negatif olamaz")
self._balance = value
acc = Account(100)
print(acc.balance) # 100
acc.balance = -5 # ValueError
staticmethod ve classmethod
class Money:
def __init__(self, kurus):
self.kurus = kurus
@classmethod
def from_lira(cls, lira):
return cls(round(lira * 100))
@staticmethod
def is_valid(value):
return value >= 0
m = Money.from_lira(12.5)
print(m.kurus, Money.is_valid(-1)) # 1250 False
Yalnızca @classmethod, metodun örneklerle değil doğrudan sınıfla çalışmasını sağlar: from_lira bir alt sınıfın nesnesini de doğru biçimde oluşturur.
Sınıf dekoratörleri ve dekoratör olarak sınıflar
Dekoratör bir sınıfa da uygulanabilir: sınıfı alır ve aynı veya değiştirilmiş bir sınıf döndürür. @dataclass böyle çalışır. Basit bir örnek olarak sınıf kaydı (registry):
registry = {}
def register(cls):
registry[cls.__name__] = cls
return cls
@register
class CsvExporter:
pass
print(registry) # {'CsvExporter': <class '__main__.CsvExporter'>}
Tersine, bir sınıf da dekoratör olabilir. __call__ metodu olan her nesne bu işi görür; durum örneğin özniteliklerinde tutulur:
import functools
class CountCalls:
def __init__(self, func):
functools.update_wrapper(self, func)
self.func = func
self.calls = 0
def __call__(self, *args, **kwargs):
self.calls += 1
return self.func(*args, **kwargs)
@CountCalls
def tick():
pass
tick(); tick()
print(tick.calls) # 2
Asenkron dekoratör
async def fonksiyonları için sarmalayıcının da bir coroutine olması ve sonucu await ile beklemesi gerekir; aksi halde bekleyen bir coroutine nesnesi döner:
import functools
def log_async(func):
@functools.wraps(func)
async def wrapper(*args, **kwargs):
print(f"{func.__name__} başladı")
result = await func(*args, **kwargs)
print(f"{func.__name__} bitti")
return result
return wrapper
Dekoratörler neden kullanılır
- Ortak mantık — loglama, metrikler, süre ölçümü; her fonksiyona kod kopyalamadan.
- Önbellekleme — pahalı ve saf hesaplamalar için
@functools.cache. - Kontroller — yetki, argüman doğrulama, web işleyicilerinde kimlik doğrulama.
- Kayıt — Flask ve FastAPI'de rotalar (
@app.get("/")), pytest fikstürleri. - Dayanıklılık — yeniden deneme, zaman aşımı, hata yönetimi.
Asıl kazanç sorumlulukların ayrılmasıdır: fonksiyon kendi işine odaklanır, yardımcı davranış tek satırla eklenir ve aynı kolaylıkla kaldırılır.
Tipik hatalar
- Sarmalayıcı sonucu döndürmüyor — dekore edilmiş fonksiyon
Noneverir. *args, **kwargsyok — argümanlı çağrıdaTypeError.wrapsunutulmuş — yanlış__name__ve docstring, loglarda karışıklık.- Fabrikada parantez eksik —
@repeatyerine@repeat(3)yazılmalı; aksi halde fonksiyontimesparametresine geçer. - Dekoratörün gövdesine ağır kod — bu kod çağrıda değil, modül içe aktarılırken çalışır.
Özet
Python dekoratörleri, fonksiyon alıp fonksiyon döndüren yüksek dereceli fonksiyonlardır; @ ise atamanın kısa yazımıdır. Yeni bir dekoratöre *args, **kwargs, return result ve functools.wraps içeren şablonla başlayın; ayar gerektiğinde bir fabrika katmanı ekleyin. Dekoratörlerin dayandığı fonksiyonlar, kapanımlar ve tür ipuçları kursta uygulamalı olarak işlenir.
Sık sorulan sorular
Bir dekoratörü bir fonksiyondan nasıl kaldırabilirim?
Dekoratör functools.wraps kullanıyorsa orijinal fonksiyon __wrapped__ özniteliğinde durur: func.__wrapped__(...) çağrısı sarmalayıcısız çalışır. Kullanmıyorsa kodda @ ile başlayan satırı silmek gerekir.
Dekoratörün kodu ne zaman çalışır?
Dekoratörün kendi gövdesi bir kez, fonksiyon tanımlanırken, genellikle modül içe aktarılırken çalışır. wrapper içindeki kod ise dekore edilmiş fonksiyon her çağrıldığında çalışır.
Dekoratörleri sınıf metotlarına uygulayabilir miyim?
Evet; *args, **kwargs alan sıradan bir dekoratör metotlarla da çalışır. self, args'ın ilk elemanı olarak gelir.
Dekoratör ile kalıtım arasındaki fark nedir?
Kalıtım bir sınıfın davranışını hiyerarşi üzerinden değiştirir; dekoratör ise bir fonksiyonu ya da sınıfı dışarıdan sarar ve kodunu değiştirmeden diğer dekoratörlerle kolayca birleştirilir.
Bu konu “Python 3.15: от новичка до профи” kursunda uygulamalı olarak ayrıntılı işleniyor.
Kursa git