Decorator-lar (örtük və ya bəzəyici kimi tərcümə oluna bilər) bir çox Python kitabxanası və çərçivələrində (framework) istifadə olunan özəllikdir.
Hər hansı metodu istifadə etməzdən qabaq görüləcək işləri özündə ehtiva edir. Məsələn, Django çərçivəsində hansısa spesifik bir keçiddə istifadəçinin öz hesabına daxil olub-olmadığını yoxlayırıq.
@login_required
def account_view(request):
return HttpResponse('Gizli səhifəyə xoş gəldiniz!')
account_view
funksiyasının üzərinə yazılan örtük (decorator) ora sorğu göndərməzdən qabaq, hesaba daxil olub olmadığını yoxayır. Əgər daxil olubsa cavab qaytarır, yoxsa giriş (login) səhifəsinə yönləndirir.
Bəs bunun işləmə prinsipi nədir? Necə olur ki, həmən funksiya çağırılmamışdan qabaq login_required
işə düşür?
Əslində işləmə prinsipi çox sadədir. İşin sehri sadəcə qısayol sintaksisindədir (syntactic sugar).
İlk addımlar
Fərz edək ki, bir ədədi digərinə bölən funksiyamız var:
def divider(num1, num2):
return num1/num2
divider(5, 2)
divider(5, 0)
İkinci çağırılmada 5 və 0 ötürdük və 0-a bölmə olmadığından Python bizə ZeroDivisionError qaytardı.
Örtük (decorator) vasitəsi ilə biz çıxa biləcək xətaları əvvəlcədən idarə edə bilərik. İlk öncə sadə bir misala baxaq:
def my_decorator(func):
def wrapper():
print("Funksiyadan qabaq işləyəcək.")
# parametr kimi ötürdüyümüz funksiya
func()
print("Funksiyadan sonra işləyəcək.")
return wrapper
def divide():
print("divide funksiyası")
foo = my_decorator(divide)
foo()
Bu kod bizə belə bir nəticə qaytaracaq:
Funksiyadan qabaq işləyəcək.
divide funksiyası
Funksiyadan sonra işləyəcək.
Burada biz my_decorator
adında bir funksiya yartdıq. Hansı ki, içində wrapper
adında daxili (inner) funksiya var. Əlavə olaraq, bir də divide
funksiyası yaratdıq. Daha sonra foo adında bir dəyişən yaradıb, divide()
funksiyasını parametr kimi ötürdüyümüz my_decorator
funksiyasına mənimsətdik. foo()
funksiyasını çağıranda ilk öncə my_decorator
işə düşür, ardınca içindəki wrapper()
. Ən sonda isə divide()
.
Göründüyü kimi çox sadə məntiqlə – bir funksiyanı digərinə ötürməklə, istənilən funksiyadan qabaq hər hansısa bir əməliyyat etmək mümkündür
Örtüklər və arqumentlər
Əgər bizim örtük ilə istifadə edəcəyimiz funksiyamız (divide) parametr qəbul edəcəksə, o zaman belə bir xəta ilə qarşılaşa bilərik:
def my_decorator(func):
def wrapper():
print("Funksiyadan qabaq işləyəcək.")
func()
print("Funksiyadan sonra işləyəcək.")
return wrapper
def divide(num1, num2):
print(num1/num2)
foo = my_decorator(divide)
foo(5, 2)
Traceback (most recent call last):
File “main.py”, line 12, in
foo(5, 2)
TypeError: wrapper() takes 0 positional arguments but 2 were given
Bu xətanı biz ona görə aldıq ki, 4-cü sətirdə çağrılan func()
metoduna heç bir arqument verməmişik. Halbuki, divide()
bizdən num1 və num2 dəyərlərini gözləyir.
Örtükdəki daxili funksiyamız olan wrapper()
əsas funksiyamızın (divider()) parametrlərini qəbul edə bilir. Biz də bunu func()
-a ötürə bilərik:
def my_decorator(func):
def wrapper(num1, num2):
print("Funksiyadan qabaq işləyəcək.")
func(num1, num2)
print("Funksiyadan sonra işləyəcək.")
return wrapper
def divide(num1, num2):
print(num1/num2)
foo = my_decorator(divide)
foo(5, 2)
Bununla da xəta aradan qalxmış olur.
Əgər funksiya bizdəki kimi 2 yox daha çox parametr qəbul etsə hər dəfə bunları yazmaq o qədər də ağlabatan deyil. Üstəlik hər funksiya üçün eyni işi görən ayrı-ayrı örtüklər yazmalı olacağıq. Bunun həll yolu *args və **kwargs ikilisidir.
*args funksiyaya ötürülən istənilən mövqeli (positional) arqumentləri, **kwargs isə açar sözlü (keyword) arqumentləri qəbul edir.
def my_decorator(func):
def wrapper(*args, **kwargs):
print("Funksiyadan qabaq işləyəcək.")
func(*args, **kwargs)
print("Funksiyadan sonra işləyəcək.")
return wrapper
def divide(num1, num2):
print(num1/num2)
foo = my_decorator(divide)
foo(5, 2)
Qısayol sintaksisi (syntactic sugar)
Örtüyü foo = my_decorator(divide)
kimi yazmaq əvəzinə
def my_decorator(func):
def wrapper(*args, **kwargs):
print("Funksiyadan qabaq işləyəcək.")
func(*args, **kwargs)
print("Funksiyadan sonra işləyəcək.")
return wrapper
@my_decorator
def divide(num1, num2):
print(num1/num2)
divide(5, 2)
kimi yaza bilərik.
Bir neçə örtük funksiyasını eyni bir funksiyanın üzərində istifadə etmək mümkündür:
@my_decorator1
@my_decorator2
@my_decorator3
def divide(num1, num2):
print(num1/num2)
Nəzərə almaq lazımdır ki, Python örtükləri aşağıdan yuxarıya doğru oxuyub, icra edəcək. Yəni birinci @my_decorator3
icra olunacaq, ardınca @my_decorator2
və sonda @my_decorator3
.
@wraps decorator
Python-da __name__, __doc__ və s. kimi hazır gələn (built-in) sehrli (magic) dəyişənlər var. Məsələn, __name__ bizə funksiyanın adını verir:
print(print.__name__)
“print” funksiyasının adını qaytarır.
Bu məntiqlə biz print(divide.__name__)
kodunu icra etsək divide qaytarmalı idi, amma “wrapper” qaytardı. Çünki, biz divide
-ı my_docator
-a mənimsətmişik. O da bizə wrapper()
metodunu qaytarır. Problemi həll etmək üçün Python-un @wraps
decorator-undan istifadə edəcəyik.
from functools import wraps
def my_decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
print("Funksiyadan qabaq işləyəcək.")
func(*args, **kwargs)
print("Funksiyadan sonra işləyəcək.")
return wrapper
@my_decorator
def divide(num1, num2):
print(num1/num2)
print(divide.__name__)
Daxili funksiya olan wrapper()
funksiyasının üstünə @wraps
örtüyünü qoyub, parametr kimi də func
dəyərini verdikdən sonra divide.__name__
bizə konsolda “divide” qaytaracaq.
Və, sonda məsələnin həllinə gələk:
from functools import wraps
def my_decorator(func):
@wraps(func)
def wrapper(num1, num2):
if num2 == 0:
print("You can not divide by zero.")
else:
func(num1, num2)
return wrapper
@my_decorator
def divide(num1, num2):
print(num1/num2)
divide(5, 0)
divide(5, 2)
Nəticə:
"You can not divide by zero."
2.5