Python’da Dekoratörler: @ İşaretinin Arkasında Ne Var?

Python'da @ işareti aslında ne yapıyor? Closure'dan functools.wraps'e, parametre alan retry dekoratöründen functools.cache ile 2,7 milyon çağrının 31'e inmesine kadar dekoratörleri çalışan örneklerle anlatıyoruz.

Bir önceki Python yazımızda sınıfın üstüne tek bir @dataclass satırı ekleyip 13 satırlık kodu 5 satıra indirmiştik. Peki o @ işareti gerçekte ne yapıyor? Cevap şaşırtıcı derecede basit: dekoratör, bir fonksiyonu alıp yerine başka bir fonksiyon koyan sıradan bir Python fonksiyonu. Bu yazıda kendi dekoratörlerimizi yazıyoruz. Süre ölçen, başarısız işlemi yeniden deneyen, sonuçları önbelleğe alan ve komutları kaydeden dekoratörleri çalışan örneklerle inceliyoruz.

Dekoratör Nedir?

Şu iki yazım biçimi birebir aynı işi yapar:

@timer
def slow_sum(n):
    ...

# yukarıdaki, aşağıdakinin kısaltmasıdır:
def slow_sum(n):
    ...
slow_sum = timer(slow_sum)

Yani @timer, fonksiyon tanımlandıktan hemen sonra timer fonksiyonunu çağırır ve dönen değeri aynı isme atar. Bunun mümkün olması için Python’un iki özelliğine ihtiyaç var: fonksiyonların başka fonksiyonlara parametre olarak verilebilmesi ve fonksiyonların içinde yeni fonksiyonlar tanımlanabilmesi. Önce bu ikisine bakalım.

Bu yazıdaki örneklerin tamamı tek bir main.py dosyasında. Her başlık dosyadaki bir demo_ fonksiyonuna karşılık geliyor.

Kodun Anatomisi

Proje bilgisi Yazar: Ahmet Aksoy · Tarih: 08.10.2026 Ortam: Python 3.12.3, Ubuntu 24.04 · Gereken en düşük sürüm: Python 3.9

1. Fonksiyonlar da nesnedir: closure

Python’da bir fonksiyon, bir sayı ya da liste gibi bir nesnedir. Bir değişkene atanabilir, başka bir fonksiyondan döndürülebilir. İçteki fonksiyon, dıştaki fonksiyonun değişkenlerini dış fonksiyon bittikten sonra da hatırlar. Buna closure (kapanış) denir:

def make_multiplier(factor):
    def multiply(x):
        return x * factor          # factor, dış fonksiyondan "hatırlanıyor"
    return multiply

double, triple = make_multiplier(2), make_multiplier(3)
print(double(21))   # 42
print(triple(21))   # 63

make_multiplier(2) çalışıp bittiği halde, döndürdüğü multiply fonksiyonu factor değerinin 2 olduğunu unutmuyor. Dekoratörlerin hepsi bu düzene dayanıyor: dış fonksiyon süslenecek fonksiyonu alır, içteki fonksiyon onu hatırlayıp çağırır. Lambda yazımızda fonksiyonları parametre olarak kullanmanın başka örneklerini bulabilirsiniz.

2. İlk dekoratör: Süre ölçmek

Bir fonksiyonun ne kadar sürdüğünü ölçmek için her fonksiyonun başına ve sonuna zaman ölçen satırlar eklemek yerine, bu işi bir kez yazıp istediğimiz fonksiyona takabiliriz:

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) * 1000
        print(f"[timer] {func.__name__} {elapsed:.1f} ms sürdü")
        return result
    return wrapper

@timer
def slow_sum(n):
    return sum(i * i for i in range(n))

print(slow_sum(1_000_000))

# Çıktı:
# [timer] slow_sum 246.8 ms sürdü
# 333332833333500000

wrapper, *args ve **kwargs sayesinde her türlü parametreyi olduğu gibi asıl fonksiyona iletiyor. Ardından sonucu geri döndürüyor. Bu iki ayrıntıyı unutmak en sık yapılan dekoratör hatasıdır: return result yazılmazsa süslenen fonksiyon her zaman None döndürür.

3. functools.wraps neden gerekli?

Yukarıdaki örnekte @functools.wraps(func) satırı dikkatinizi çekmiştir. Bu satır olmadan ne olduğuna bakalım:

def without_wraps(func):
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@without_wraps
def greet_a(name):
    """Kullanıcıyı selamlar."""
    return f"Merhaba {name}"

# Çıktı:
# wraps yok    : 'wrapper', doc=None
# wraps var    : 'greet_b', doc='Kullanıcıyı selamlar.'

Dekoratör, fonksiyonun yerine wrapper fonksiyonunu koyduğu için fonksiyonun adı ve açıklaması (docstring) kayboluyor. Bu durum hata mesajlarında, günlük (log) kayıtlarında ve help() çıktısında kafa karıştırır. functools.wraps, asıl fonksiyonun adını, açıklamasını ve diğer bilgilerini wrapper fonksiyonuna kopyalar. Kendi yazdığınız her dekoratörde bu satırı kullanın.

4. Parametre alan dekoratör: retry

Ağ istekleri bazen geçici olarak başarısız olur. Böyle bir işlemi birkaç kez yeniden denemek isteriz, ama kaç kez deneneceği ve hangi hatalarda deneneceği işten işe değişir. Dekoratöre parametre vermek için bir katman daha ekliyoruz:

def retry(times=3, exceptions=(Exception,), delay=0.0):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, times + 1):
                try:
                    return func(*args, **kwargs)
                except exceptions as exc:
                    print(f"[retry] {func.__name__} deneme {attempt}/{times} başarısız: {exc}")
                    if attempt == times:
                        raise
                    time.sleep(delay)
        return wrapper
    return decorator

@retry(times=3, exceptions=(ConnectionError,))
def fetch_rates():
    ...  # ilk iki çağrıda ConnectionError fırlatıyor

print(fetch_rates())

# Çıktı:
# [retry] fetch_rates deneme 1/3 başarısız: sunucu yanıt vermedi
# [retry] fetch_rates deneme 2/3 başarısız: sunucu yanıt vermedi
# {'USD': 41.6, 'EUR': 48.4}

Kurlar örnekteki sahte sunucunun döndürdüğü değerler. Asıl önemli olan, üç katmanın sırası: retry(times=3) önce çalışıp bir dekoratör döndürüyor, o dekoratör de fonksiyonu sarıyor.

exceptions parametresi de önemli bir iş görüyor. Yalnızca geçici olabilecek hatalar yeniden deneniyor. Örnekteki parse_price("kırk iki") çağrısının fırlattığı ValueError hemen yukarı iletiliyor, çünkü bir metni sayıya çevirememek ikinci denemede düzelmez. Gerçek projelerde delay parametresine birkaç saniyelik bir bekleme vermek sunucuya nefes aldırır. requests yazımızda bu işi kütüphanenin kendi araçlarıyla nasıl yapabileceğinizi anlatmıştık.

5. Hazır dekoratör: functools.cache

Her dekoratörü kendiniz yazmanız gerekmiyor. Standart kütüphanedeki functools.cache, bir fonksiyonun aynı parametrelerle döndürdüğü sonucu hatırlar ve hesabı bir daha yapmaz. Fibonacci yazımızdaki özyinelemeli fonksiyon bu dekoratörün etkisini görmek için birebir:

@functools.cache
def fib_cached(n):
    return n if n < 2 else fib_cached(n - 1) + fib_cached(n - 2)

# Çıktı:
# fib(30) önbelleksiz: 832040, 2,692,537 çağrı,    776.3 ms
# fib(30) önbellekli : 832040,        31 çağrı,    0.029 ms
# cache_info   : CacheInfo(hits=28, misses=31, maxsize=None, currsize=31)

Önbelleksiz sürüm aynı değerleri tekrar tekrar hesapladığı için 2,7 milyon kez çağrılıyor. Önbellekli sürümde her n değeri yalnızca bir kez hesaplanıyor: 0’dan 30’a kadar toplam 31 çağrı. Süre yaklaşık 800 milisaniyeden 0,03 milisaniyeye iniyor. Bu ölçümde çağrıları saymak için eklediğimiz sayaç da biraz süre ekliyor, ama aradaki fark yine de binlerce kat.

Bir uyarı: cache sonuçları sınırsız biriktirir. Çok farklı parametrelerle çağrılan fonksiyonlarda belleği şişirmemek için @functools.lru_cache(maxsize=1000) gibi sınırlı bir önbellek kullanın. Ayrıca önbellek, yalnızca aynı girdiye her zaman aynı sonucu veren fonksiyonlar için güvenlidir. Saate ya da dosya içeriğine bağlı fonksiyonlarda yanlış sonuç döndürür.

6. Kayıt dekoratörü: Komut tablosu

Dekoratörün fonksiyonu sarması da gerekmiyor. Fonksiyonu bir yere kaydedip olduğu gibi geri döndürebilir. Flask’taki @app.route dekoratörü de bu düzeni kullanır: adresi ve fonksiyonu bir tabloya kaydeder, fonksiyonu ise olduğu gibi bırakır.

COMMANDS = {}

def command(name):
    def decorator(func):
        COMMANDS[name] = func       # fonksiyonu değiştirmeden kaydeder
        return func
    return decorator

@command("selam")
def cmd_hello(arg):
    return f"Selam, {arg}!"

@command("ters")
def cmd_reverse(arg):
    return arg[::-1]

# Çıktı:
# komutlar     : ['selam', 'ters']
# selam Ayşe    -> Selam, Ayşe!
# ters gurmezin -> nizemrug
# sil dosya     -> bilinmeyen komut: sil

Yeni bir komut eklemek için yalnızca yeni bir fonksiyon yazıp üstüne @command("ad") koymak yetiyor. Komutları dağıtan koda dokunmak gerekmiyor. argparse yazımızdaki alt komutları bu yöntemle düzenlemek, büyüyen bir komut satırı aracını epey toparlar.

7. Üst üste dekoratörler: Sıra önemli

Bir fonksiyona birden fazla dekoratör takılabilir. Bu durumda fonksiyona en yakın olan, yani en alttaki dekoratör önce uygulanır:

@bold
@italic
def title_a():
    return "gurmezin"

@italic
@bold
def title_b():
    return "gurmezin"

# Çıktı:
# @bold @italic: <b><i>gurmezin</i></b>
# @italic @bold: <i><b>gurmezin</b></i>

@bold @italic yazımı bold(italic(title_a)) anlamına gelir. Bu sıra, gerçek projelerde fark yaratır. Örneğin @timer ile @retry birlikte kullanılırsa, @timer üstteyse tüm denemelerin toplam süresini, alttaysa her denemenin süresini ayrı ayrı ölçer.

Nasıl Çalıştırılır?

Üçüncü taraf hiçbir paket gerekmiyor. functools.cache Python 3.9 ile geldi, bu yüzden 3.9 veya daha yeni bir sürüm yeterli:

python3 main.py

Süreler makineden makineye değişir, ama çağrı sayıları her makinede aynı çıkar.

Ne Öğrendik?

  • Dekoratör, bir fonksiyonu alıp başka bir fonksiyon döndüren sıradan bir fonksiyondur. @timer yazmak, f = timer(f) yazmanın kısaltmasıdır.
  • Dekoratörler closure sayesinde çalışır. İçteki fonksiyon, sardığı fonksiyonu hatırlar.
  • functools.wraps, süslenen fonksiyonun adını ve açıklamasını korur. Her dekoratörde kullanılmalıdır.
  • Parametre alan dekoratörler üç katmanlıdır: parametreler, dekoratör ve sarmalayıcı.
  • functools.cache, Fibonacci örneğinde 2,7 milyon çağrıyı 31’e indirdi. Önbellek yalnızca aynı girdiye aynı sonucu veren fonksiyonlarda kullanılmalıdır.
  • Kayıt dekoratörleri fonksiyonu değiştirmeden bir tabloya ekler. Flask’taki @app.route bu düzenin bilinen bir örneğidir.
  • Üst üste dekoratörlerde en alttaki önce uygulanır.

Kaynaklar ve Sonraki Adımlar

Sonraki adım olarak kendi projelerinizde birkaç fonksiyonun başında ve sonunda tekrar eden satırları arayın. Zaman ölçme, günlük kaydı, yetki kontrolü gibi işler genellikle bir dekoratöre taşınmayı bekliyordur.

Ahmet Aksoy

Not: Bu yazıda incelediğimiz kodu ve benzer projelerin kaynak kodlarını https://github.com/ahmetax/practical-python-examples adresinde bulabilirsiniz.

Bir Yanıt Bırak

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir