Məzmuna keç
Educora
İrəli22 dəq16 / 42

Dekoratorlar və qapanmalar

Funksiyalar obyekt kimi, LEGB qaydası, qapanmalar və `nonlocal`, öz dekoratorların, arqumentli dekoratorlar, `functools.wraps` və `lru_cache` ilə keşləmə.

Özünü yoxla
Bu dərsdə öyrənəcəksən
  • Funksiyaları obyekt kimi ötürmək, qaytarmaq və susmaya görə qiymətlərin nə vaxt hesablandığını izah etmək
  • Qapanma yaratmaq və nonlocal-dan düzgün istifadə etmək
  • @wraps ilə dekorator və arqumentli dekorator yazmaq
  • lru_cache ilə nəticələri keşləmək və onun məhdudiyyətlərini bilmək

İyirmi funksiyan var və istəyirsən ki, hər biri çağırışlarını jurnala yazsın, şəbəkə xətasından sonra yenidən cəhd etsin və ya artıq hesabladığı nəticələri yadda saxlasın. Eyni sətirləri iyirmi yerə köçürmək pis fikirdir. Python-un cavabı dekoratordur: funksiyanın üstündə yazılmış @lru_cache kimi bir sətir onun koduna toxunmadan ona yeni davranış əlavə edir. Flask, FastAPI və pytest kimi freymvorklar dekoratorlar üzərində qurulub. Öz dekoratorunu yazmaq üçün iki ideya lazımdır: funksiyalar obyektdir, daxili funksiyalar isə ətraflarını yadda saxlaya bilir — bunlar qapanmalardır.

Funksiyalar obyektdir

Python-da funksiya ədəd və ya siyahı kimi adi obyektdir. Onu başqa ada mənimsətmək, lüğətdə saxlamaq, başqa funksiyaya ötürmək və funksiyadan qaytarmaq olar. map(square, ...) məhz buna görə işləyir: funksiyanın özünü ötürürük, onu çağırmırıq (mötərizə yoxdur).

Python
def square(x):
    return x * x

f = square
print(f(5), f.__name__)

operations = {'double': lambda x: x * 2, 'square': square}
for name, op in operations.items():
    print(name, op(7))

print(list(map(square, [1, 2, 3])))
▸ Gözlənilən nəticə
25 square
double 14
square 49
[1, 4, 9]

Funksiya obyekt olduğu üçün onun atributları da var: __name__, __doc__ və parametrlərin susmaya görə qiymətlərinin saxlandığı __defaults__. Bu qiymətlər hər çağırışda yox, def icra olunanda bir dəfə hesablanır. Python-un ən məşhur tələlərindən biri məhz bununla izah olunur:

Python
def add_item(item, basket=[]):
    basket.append(item)
    return basket

print(add_item('apple'))
print(add_item('pear'))
print(add_item.__defaults__)

def add_item_fixed(item, basket=None):
    if basket is None:
        basket = []
    basket.append(item)
    return basket

print(add_item_fixed('apple'), add_item_fixed('pear'))
▸ Gözlənilən nəticə
['apple']
['apple', 'pear']
(['apple', 'pear'],)
['apple'] ['pear']

Qapanmalar və nonlocal

Python adı dörd sahədə, LEGB ardıcıllığı ilə axtarır: Local (cari funksiya), Enclosing (əhatə edən funksiyalar), Global (modul) və Built-in (len, print kimi daxili adlar). Deməli, daxili funksiya onu əhatə edən funksiyanın dəyişənlərini oxuya bilər. Ən maraqlısı odur ki, xarici funksiya işini bitirəndən sonra da bu dəyişənlər daxili funksiyada qalır:

Python
def make_multiplier(factor):
    def multiply(x):
        return x * factor
    return multiply

double = make_multiplier(2)
triple = make_multiplier(3)
print(double(10), triple(10))
print(double.__closure__[0].cell_contents)
▸ Gözlənilən nəticə
20 30
2
Tərif
Qapanma (closure)

Qapanma — əhatə edən sahədən yadda saxladığı dəyişənlərlə birlikdə funksiyadır. Burada double funksiyası factor = 2 qiymətini yadda saxlayır: qiymət xanada (cell) durur və onu __closure__ vasitəsilə görmək olar.

Əhatə edən dəyişəni oxumaq sərbəstdir, amma ona qiymət mənimsətsən, Python adı yeni lokal dəyişən sayar və count += 1 sətri UnboundLocalError xətası verər. **nonlocal** sözü bildirir ki, bu ad əhatə edən funksiyaya aiddir. make_counter()-in hər çağırışı ayrıca, gizli count yaradır:

Python
def make_counter():
    count = 0
    def increment():
        nonlocal count
        count += 1
        return count
    return increment

counter = make_counter()
print(counter(), counter(), counter())
other = make_counter()
print(other())
▸ Gözlənilən nəticə
1 2 3
1
Python
funcs = [lambda: i for i in range(3)]
print([f() for f in funcs])

funcs = [lambda i=i: i for i in range(3)]
print([f() for f in funcs])
▸ Gözlənilən nəticə
[2, 2, 2]
[0, 1, 2]

Dekoratorlar

Dekorator funksiya qəbul edib yeni funksiya qaytaran funksiyadır. Adətən bu, orijinalı çağırmazdan əvvəl və sonra nəsə edən sarğı funksiyadır (wrapper). def add-in üstündəki @log_calls sətri sadəcə add = log_calls(add) yazılışının sintaktik şəkəridir. Sarğı funksiya *args, **kwargs qəbul edir, ona görə istənilən imzalı funksiya ilə işləyir:

Python
def log_calls(func):
    def wrapper(*args, **kwargs):
        print(f'calling {func.__name__}{args}')
        result = func(*args, **kwargs)
        print(f'{func.__name__} returned {result}')
        return result
    return wrapper

@log_calls
def add(a, b):
    return a + b

total = add(2, 3)
print(total, add.__name__)
▸ Gözlənilən nəticə
calling add(2, 3)
add returned 5
5 wrapper

Son sətrə bax: funksiyanın adı indi wrapper-dir. Dekorator add-i əvəz edib, onun adını və sənədləşdirmə sətrini itirib — bu, sazlamanı, sənədləri və pytest kimi alətləri korlayır. Həll **functools.wraps** dekoratorudur: o, orijinal funksiyanın __name__, __doc__ və digər atributlarını sarğı funksiyaya köçürür. Sarğı funksiya öz vəziyyətini atributda da saxlaya bilər:

Python
from functools import wraps

def count_calls(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        wrapper.calls += 1
        return func(*args, **kwargs)
    wrapper.calls = 0
    return wrapper

@count_calls
def greet(name):
    """Return a greeting."""
    return f'Salam, {name}!'

print(greet('Leyla'))
print(greet('Elvin'))
print(greet.calls, greet.__name__, greet.__doc__)
▸ Gözlənilən nəticə
Salam, Leyla!
Salam, Elvin!
2 greet Return a greeting.

Arqumentli dekoratorlar

Bəs @retry(times=3) necə işləyir? Burada əvvəlcə retry(times=3) çağırılır və onun nəticəsi əsl dekoratordur. Deməli, arqumentli dekoratorun üç səviyyəsi var: arqumentləri qəbul edən fabrik, funksiyanı qəbul edən dekorator və hər çağırışda işləyən sarğı funksiya. Qapanmalar sayəsində sarğı funksiya həm times-ı, həm də func-ı görür:

Python
from functools import wraps

def retry(times):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(1, times + 1):
                try:
                    return func(*args, **kwargs)
                except ConnectionError as e:
                    print(f'attempt {attempt} failed: {e}')
            raise ConnectionError(f'{func.__name__} failed {times} times')
        return wrapper
    return decorator

calls = 0
@retry(times=3)
def fetch_rates():
    global calls
    calls += 1
    if calls < 3:
        raise ConnectionError('server busy')
    return {'USD': 1.7}

print(fetch_rates())
▸ Gözlənilən nəticə
attempt 1 failed: server busy
attempt 2 failed: server busy
{'USD': 1.7}
İlk iki çağırış uğursuz olur, üçüncüsü alınır — çağıran tərəf xətaları heç görmür. Üç uğursuzluqdan sonra isə sarğı funksiya xətanı özü qaldırardı.

Dekoratorları üst-üstə də yazmaq olar. Onlar aşağıdan yuxarıya tətbiq olunur — def-ə ən yaxın olan dekorator funksiyanı birinci bükür:

Python
def bold(func):
    return lambda: '<b>' + func() + '</b>'

def italic(func):
    return lambda: '<i>' + func() + '</i>'

@bold
@italic
def title():
    return 'Educora'

print(title())
▸ Gözlənilən nəticə
<b><i>Educora</i></b>

Hazır dekoratorlar: lru_cache

Standart kitabxanada çoxlu faydalı dekorator var. **functools.lru_cache funksiyanın nəticələrini arqumentlərinə görə lüğətdə saxlayır — buna memoizasiya** deyilir. Sadə rekursiv fib(80) təxminən 7,6 · 10¹⁶ çağırış edərdi; keşlə isə hər qiymət yalnız bir dəfə hesablanır:

Python
from functools import lru_cache

@lru_cache(maxsize=None)
def fib(n):
    return n if n < 2 else fib(n - 1) + fib(n - 2)

print(fib(80))
print(fib.cache_info())
▸ Gözlənilən nəticə
23416728348467685
CacheInfo(hits=78, misses=81, maxsize=None, currsize=81)

cache_info() göstərir ki, cəmi 81 real hesablama olub (misses), qalan 78 sorğuya isə keşdən cavab verilib (hits). maxsize=None limitsiz keş deməkdir; ədəd versən, məsələn, maxsize=128, ən çoxdan istifadə olunmayan (LRU) qeydlər birinci silinir. Python 3.9-dan bəri @functools.cache yazılışı lru_cache(maxsize=None)-un qısa formasıdır. Növbəti dərslərdə başqa daxili dekoratorlarla da tanış olacaqsan: @property, @classmethod, @staticmethod, @dataclass və @contextmanager.

Tapşırıq

Funksiyanın qaytardığı sətri böyük hərflərə çevirən uppercase dekoratorunu yaz. Funksiya öz adını saxlasın deyə @wraps işlət.

Tapşırıq · Python
from functools import wraps

def uppercase(func):
    # return a wrapper (with @wraps) that returns func's result in upper case
    ...

@uppercase
def greet(name):
    return f'salam, {name}'

print(greet('Aysel'))
print(greet.__name__)
▸ Gözlənilən nəticə
SALAM, AYSEL
greet
Tapşırıq

make_accumulator(start) funksiyasını yaz: o, add(x) funksiyası qaytarsın; add hər çağırışda x-i ümumi cəmə əlavə edib yeni cəmi qaytarsın. Hər akkumulyatorun öz cəmi olmalıdır.

Tapşırıq · Python
def make_accumulator(start=0):
    # return a function add(x) that adds x to a running total and returns it
    ...

acc = make_accumulator(100)
print(acc(10))
print(acc(5))
other = make_accumulator()
print(other(1))
▸ Gözlənilən nəticə
110
115
1

Əsas fikirlər

  • Funksiyalar obyektdir: onları ötürmək, qaytarmaq və saxlamaq olar; susmaya görə qiymətlər def icra olunanda bir dəfə hesablanır.
  • Qapanma əhatə edən funksiyanın dəyişənlərini yadda saxlayır; nonlocal onlara yeni qiymət verməyə imkan yaradır.
  • def f-in üstündəki @deco yazılışı f = deco(f) deməkdir; sarğı funksiya *args, **kwargs və @wraps(func) işlədir.
  • Arqumentli dekorator fabrikdir: @retry(times=3) yazılışı retry(times=3)(f) deməkdir.
  • lru_cache heşlənən arqumentli təmiz funksiyaların nəticələrini yadda saxlayır.

Özünü yoxla

10 sual. Hər düzgün cavab XP qazandırır.

1 / 10
def load(): sətrinin üstündə yazılmış @timer nə deməkdir?