Dekoratörleri kısaca parametre olarak fonksiyon alan, geriye fonksiyon döndüren fonksiyonlar olarak tanımlayabiliriz. İlk bakışta karmaşık gibi görünüyor, evet. Ancak Python'da fonksiyonların birinci sınıf nesne olduğunu düşünüp bu tanımlamayı tekrar okuduğumuzda bir nebze daha açık hale gelecektir.

Dekoratörleri açıklamaya başlamadan önce, birinci sınıf nesnelerin ve iç içe fonksiyonların ne olduğundan bahsetmenin, dekoratörleri anlamaya yardımcı olacağını (ya da ön koşulu olduğunu) düşünüyorum.

Birinci Sınıf Nesne Nedir?

Dinamik olarak oluşturulabilen, yok edilebilen, bir fonksiyona parametre olarak verilebilen ya da bir fonksiyondan sonuç değeri olarak döndürülebilen ve değişkenlerle aynı haklara sahip olan varlıklara birinci sınıf nesne ya da birinci sınıf vatandaş denir.

  • Anonim olarak ifade edilebilir.
  • Değişkenlerde ya da veri yapılarında saklanabilir.
  • Kendi isminden bağımsız benzersiz bir ismi vardır.
  • Diğer varlıklarla karşılaştırma operatörlerine tabi tutulabilir.
  • Fonksiyonlara parametre olarak gönderilebilir.
  • Fonksiyonlardan sonuç değeri olarak döndürülebilir.
  • Okunabilir ve yazılabilir.
  • Çalışma zamanında oluşturulabilir.
  • Dağıtık işlemler arasında iletilebilir.
  • Çalışan işlemler dışında saklanabilir.

Aşağıdaki örnekte, önce iki sayıyı toplayan add fonksiyonunu oluşturuyoruz. Ardından fonksiyonun kendisini ve ismini (__name__ özelliğinde tutulur), parametre verip çalıştırarak sonucunu ve veri tipini yazdırıyoruz. Sonra parametre olarak fonksiyon alan bir fonksiyon alıp, verilen diğer parametreleri bu fonksiyona göndererek çalıştırıp, sonucunu döndürüyoruz. Son olarak bu işlemin de sonucunu yazdırarak fonksiyonu hafızadan siliyoruz.


def add(x, y):
    return x + y

print('add: {}'.format(add))
print('add.__name__: {}'.format(add.__name__))
print('add(2, 3): {}'.format(add(2, 3)))
print('type(add): {}'.format(type(add)))


def call_function(func, *args):
    return func(*args)

print('call_function(add, 2, 3): {}'.format(
    call_function(add, 2, 3)
))

del add

İç İçe Fonksiyonlar

Dekoratörler, temelde alınan parametreyi iç fonksiyona(wrapper) gönderen ve geriye de bu fonksiyonu döndüren fonksiyonlardır. İç fonksiyonları dekoratör yazmadığınız durumlarda da kullanabilirsiniz.

  • Kapsülleme (Encapsulation)

    Yazdığınız fonksiyonları, dışarıdan gelecek bir müdahaleden korumak için kullanabilirsiniz. Herhangi bir iç fonksiyonglobal alandan erişilemeyecektir.

    
    def factorial(n: int):
        assert type(n) == int
        assert n >= 0
    
        def fact(n):
            if n <= 1:
                return 1
            return n * fact(n - 1)
    
        return fact(n)
    
    print(factorial(4))
    

    Yukarıda parametre olarak verilen sayının faktoriyelini alan bir fonksiyon var. Verilen parametre iç fonksiyon ile 1 olana kadar azaltılarak, her adımda kendisi (n) ve kendisinin bir eksiği (n - 1) ile çarpılıyor. Hemen üzerinde ise parametrenin tam sayı olduğu ve sıfırdan yüksek olduğundan emin olunuyor. Tabii normalde bu kontrolleri assert kullanmak yerine exception fırlatarak yapmanızı öneririm.

  • Kendini Tekrar Etme (Don't Repeat Yourself)

    Fonksiyonun içinde tekrar eden kod blokları varsa onları bir iç fonksiyon haline getirerek hem okunabilirliği artırmış hem de DRY prensibine uymuş olursunuz.

    Örnekte kullanmak üzere bir User sınıfı oluşturalım. Bu sınıfta kullanıcı adı için name, aktif kullanıcı olup olmadığını tutmak üzere is_active ve izinlerin listesini tutmak üzere permissions özellikleri olsun.

    
    class User(object):
        is_active = True
    
        def __init__(self, name, permissions):
            """
            :param name: string
            :param permissions: list
            """
            self.name = name
            self.permissions = permissions
    

    Şimdi, bir User nesnesinin özelliklerinin doğru veri tiplerine sahip olup olmadığını test eden bir fonksiyon yazalım.

    
    def validate_user(user):
        def check(value, necessary_type):
            if type(value) == necessary_type:
                print("- {value}'s type is OK".format(value=value))
            else:
                raise TypeError("{value}'s type should be {type}.".format(
                    value=value,
                    type=necessary_type
                ))
    
        check(user.name, str)
        check(user.is_active, bool)
        check(user.permissions, list)
        for permission in user.permissions:
            check(permission, str)
    

    Parametre olarak verilen değerin, yine parametre olarak verilen veri tipine sahip olup olmadığını kontrol eden bir check iç fonksiyonu var. Bu fonksiyonda eğer veri tipi geçerliyse ekrana bir bilgi satırı yazdırıyor, eğer geçerli değilse bir TypeError fırlatarak durumu bildiriyoruz.

    
    user = User('umutcoskun', ['can_view_dashboard'])
    validate_user(user)
    

    Eğer iç içe fonksiyon kullanmasaydık, check fonksiyonunu dışarı yazıp kalabalık oluşturacaktık. Ya da sınıfın kontrol etmek istediğimiz her özelliği için bilgi satırı yazdırma ve hata fırlatma işlemlerini tekrarlayacaktık.

    Fonksiyonu artık silebilirsiniz, ancak User sınıfını saklayın aşağıda tekrar lazım olacak.

Dekoratörler

Evet, sonunda asıl konuya gelebildik. Dekoratörler, kapsamasını istediğimiz fonksiyonların üzerine, önünde @ karakteri konularak yazılır. Buna pie syntax denir. Aslında bu yazım stili sadece bir kısayoldan ibarettir.


# Yöntem 1:
@decorator
def func():
    ...

# Yöntem 2:
def func():
    ...

func = decorator(func)

Yukarıdaki örnekte; üstteki pie syntax ile yazılmış dekoratörün yaptığı iş aslında hemen altındaki bölümün yaptığı iş ile aynıdır. Kullanım kolaylığı sağlamak ve okunabilirliği artırmak açısından böyle bir söz dizimi tercih edilmiş.

Aşağıda en basit kullanımıyla bir dekoratör görüyorsunuz. Fonksiyon çalışmadan önce bir mesaj yazdırıyor. Asıl fonksiyonda ise fonksiyonun çalıştığını yazdırıyoruz.


def decorator(func):
    def wrapper(*args, **kwargs):
        print('Fonksiyon çalışacak...')
        func()
    return wrapper


@decorator
def func():
    print('Fonksiyon çalıştı.')


func()

Bir dekoratör kullanıldığında, kullanılan fonksiyon dekoratöre parametre olarak düşer. Dekoratör içinde bir kapsayıcı (wrapper) iç fonksiyon oluşturur ve asıl fonksiyona gelen parametreleri bununla yakalarız. Dilediğimiz işlemleri yaptıktan sonra iç fonksiyondan, parametre olarak gelen asıl fonksiyondan dönen değeri, dekoratörden ise iç fonksiyonun kendisini geriye döndürürüz.

Şimdi, bir işe yarayabilecek gerçek bir dekoratör yazalım.


def benchmark(func):
    def wrapper(*args, **kwargs):
        from time import time

        time_start = time()
        result = func(*args, **kwargs)
        time_finish = time()

        time_delta = time_finish - time_start
        print('- [{func_name}] fonksiyonu {seconds} saniye sürdü.'.format(
            func_name=func.__name__,
            seconds=round(time_delta, 2),
        ))

        return result

    return wrapper

Yukarıda benchmark isimli bir dekoratör görüyorsunuz. Bu dekoratör parametre olarak gelen fonksiyonu çalıştırmadan önce başlangıç zamanını(time_start), çalıştırdıktan sonra da bitiş zamanını(time_finish) alıyor. Ardından aradan geçen süreyi(time_delta) hesaplayıp ekrana yazdırıyor. Asıl fonksiyondan (func) dönen değeri (result) iç fonksiyondan döndürdükten sonra son olarak da iç fonksiyonu(wrapper) geriye döndürüyor. Böylece, çalışma süresini hesaplamak istediğimiz fonksiyona @benchmark eklediğimizde başka müdahaleye gerek kalmadan konsolda çalışma sürelerini görebiliyoruz.


@benchmark
def test_response_status_is_200():
    from urllib import request

    response = request.urlopen(
        request.Request('https://httpbin.org/status/200',
                        method='HEAD')
    )

    assert response.status == 200


test_response_status_is_200()

Yukarıda, yazdığımız dekoratörün bir kullanımını görüyorsunuz. Fonksiyonda önce httpbin sitesine HEAD isteği atıp ardından dönen cevabın HTTP durum kodunun 200 (başarılı) olup olmadığını kontrol ediyoruz. Son olarak da yazdığımız bu fonksiyonu çalıştırıyoruz. Eğer denediyseniz fonksiyon çalıştığında aşağıdakine benzer bir çıktı alacaksınız:

- [test_response_status_is_200] fonksiyonu 0.87 saniye sürdü.

Parametreli Dekoratörler

Bildiğiniz üzere dekoratörlere parametre de verebiliyoruz. Örneğin aşağıda Flask frameworkten bir view görüyorsunuz.


from flask import Flask, render_template

app = Flask(__name__)


@app.route('/', methods=['GET'])
def home():
	return render_template('home.html')

Anasayfayı görüntülemek için home isimli bir view yazılmış ve route dekoratörü kullanılmış. Bu dekoratörün birinci parametresi bu viewın hangi URL'e (bu örnekte kök adres) karşılık vereceği, ikinci opsiyonel parametre ise hangi HTTP methodlarının kullanılabilir olacağı.

Parametreli dekoratörlerde, iç fonksiyon (wrapper) parametreyi alacak ek bir iç fonksiyon (decorator) içine alınır.

Yukarıda oluşturduğumuz User sınıfını hatırlıyorsunuz değil mi? Şimdi, asıl fonksiyona parametre olarak verilen kullanıcının, dekoratöre parametre olarak verilen izne sahip olup olmadığını kontrol eden bir dekoratör yazacağız.


def has_permission(permission):
    def decorator(func):
        def wrapper(*args, **kwargs):
            user = kwargs.get('user')
            assert permission in user.permissions

            result = func(*args, **kwargs)
            return result

        return wrapper

    return decorator

Önce verilen parametre has_permission fonksiyonuna düşüyor. Üzerinde dekoratör kullanılan asıl fonksiyon decorator iç fonksiyonuna düşüyor. Asıl fonksiyona verilen parametreler ise wrapper iç fonksiyonuna düşüyor.

Ardından, verilen anahtar kelimeli parametreler (keyword arguments) içinden user isimli olanı alıyoruz. Bu parametre bir User nesnesi olduğu için, dekoratöre parametre olarak verilen izin, kullanıcının izinlerinde (user.permissions) var mı diye kontrol ediyoruz.

Sonra asıl fonksiyondan dönen değeri (result) alıp geriye döndürüyoruz. Sonra decorator fonksiyonundan wrapper fonksiyonunu döndürüyoruz. Son olarak da has_permission fonksiyonundan decorator fonksiyonunu döndürüyoruz.


@has_permission('can_view_dashboard')
def view_dashboard(**kwargs):
    user = kwargs.pop('user')

    print('\n- DASHBOARD')
    print('- Merhaba {username}, giriş başarılı.'.format(
        username=user.name
    ))

Yönetim panelini gösteren sahte bir fonksiyonumuz var. Hemen üzerinde de yazdığımız dekoratör ile kullanıcının can_view_dashboard iznine sahip olup olmadığını kontrol ediyoruz. Böylece normal kullanıcılar yönetim paneline girş yapamayacaklar.

Yazdığımız kodu aşağıdaki gibi test edebiliriz. Eğer kullanıcının izni yoksa dekoratör AssertionError fırlatacak.


user = User('umutcoskun', ['can_view_dashboard'])
view_dashboard(user=user)

Zincirleme Dekoratörler

Dekotörler uç uca eklenebilirler. Diğer bir deyişle, bir fonksiyon üzerinde birden fazla dekoratör kullanabilirsiniz. Örneğin, aşağıdaki gibi yazdığımız @benchmark ve @has_permission dekoratörlerinin ikisi birden kullanabiliriz.


@benchmark
@has_permission('can_view_dashboard')
def view_dashboard(**kwargs):
    user = kwargs.pop('user')

    print('\n- DASHBOARD')
    print('- Merhaba {username}, giriş başarılı.'.format(
        username=user.name
    ))


view_dashboard(user=user)

Ancak bir problem var. Fonksiyonu çalıştırdığınızda göreceksiniz ki, @benchmark dekoratörü asıl fonksiyon yerine alttan gelen wrapper fonksiyonunun ölçümünü yapıyor. Bu problemi aşmak için functools modülündeki wraps dekoratörünü kullanacağız. Bu dekoratör, bizim dekoratörümüzün parametre olarak aldığı fonksiyonu ismi, parametre listesi, docstring vs. gibi tüm üst bilgisi ile beraber döndürmesini sağlayacak.

Şimdi, @has_permission dekoratörünü aşağıdaki gibi düzenleyelim.


def has_permission(permission):
    def decorator(func):
        from functools import wraps

        @wraps(func)
        def wrapper(*args, **kwargs):
            user = kwargs.get('user')
            assert permission in user.permissions

            result = func(*args, **kwargs)
            return result

        return wrapper

    return decorator

Eğer bir problem çıkmadıysa aşağıdakine benzer bir çıktı alacaksınız. Çalışma süresinin sıfır saniye olması normal, hiçbir şey yapmadık fonksiyonda.

- DASHBOARD
- Merhaba umutcoskun, giriş başarılı.
- [view_dashboard] fonksiyonu 0.0 saniye sürdü.

Sınıfları Dekoratör Olarak Kullanmak

Önceki örneklerde sadece fonksiyonları dekoratör oluşturmak için kullandık. Ancak sınıfları ve onların __call__ sihirli fonksiyonlarını kullanarak da dekoratör oluşturabiliriz. Bu fonksiyon, bir sınıfa callable gibi davranıldığında yani fonksiyon gibi çalıştırıldığında çalışır. Dekoratör parametresini almak için __init__ sihirli fonksiyonunu kullanacağız. Bu fonksiyon bildiğiniz üzere sınıf ilk defa oluşturulduğunda çalışır.


class sleep:
    def __init__(self, secs):
        self.secs = secs

    def __call__(self, func):
        from functools import wraps

        @wraps(func)
        def wrapper(*args, **kwargs):
            from time import sleep

            sleep(self.secs)
            return func(*args, **kwargs)

        return wrapper

Yukarıdaki örnekte dekoratör kullanılan fonksiyonu, parametre olarak verilen saniye değeri kadar bekletip sonra çalıştıran bir dekoratör görüyorsunuz. İlk olarak __init__ içinde, verilen saniyeyi sınıf içine kaydediyoruz, __call__ methodu tetiklendiğinde asıl fonksiyonu sınıf içindeki secs kadar bekletip çalıştırıyoruz. Kullanımı ise aşağıdaki gibi:


@sleep(5)
def hello(name):
    print('Hello {}'.format(name))


hello('World')
hello('Umut')

Dekoratörleri Sınıflar Üzerinde Kullanmak

Önceki örneklerde dekoratörleri hep fonksiyonlar üzerinde kullandık. Ancak sınıflar da birer çağrılabilir(callable) olduğu için elbette onlar üzerinde de kullanabiliriz. Örneğin, oluşturduğumuz @benchmark ve @sleep dekoratörlerini yine oluşturduğumuz User sınıfı üzerinde kullanalım.


@benchmark
@sleep(3)
class User(object):
    is_active = True

    def __init__(self, name, permissions):
        self.name = name
        self.permissions = permissions

user = User('umutcoskun', ['can_view_dashboard'])

Böylece her User nesnesi oluşturulduğunda, oluşturulmadan önce 3 saniye beklenecek ve toplamda oluşturulma işleminin kaç saniye sürdüğü ekrana yazılacak.

Kolay gelsin. :)