Знайомство з декораторами в Python та способи їх використання

7 хв. читання

Всім, хто хоч трохи працює з Python, знайомі декоратори. Декоратори — це обгортка для функції. Найкраще показати все на прикладі:

>>> def dec(fn):
...     def func_wrapper():
...         print("Before")
...         fn()
...         print("After")
...     return func_wrapper
... 
>>> @dec
... def hello():
...     print("Hello World!")
... 
>>> hello()
Before
Hello World!
After

Тут dec — і є наш декоратор. Він оголошується так само як функція, аргументом приймає функцію для обробки. Всередині декоратора оголошується нова функція (в яку ви можете додати потрібні вам дії) і повертається. Для декорування функції перед її об'явою потрібно написати @decorator_name. Замість декорованої функції буде викликатися та, що створена всередині декоратора. Тепер, після невеликого вступу, давайте подивимось для чого ж їх можна використовувати.

Замірювання часу виконання

Декоратори можна використовувати для невеличких бенчмарків. Наприклад, так:

>>> import time
>>> from random import randint
>>> 
>>> def ex_time(fn):
...     def wrap(*args, **kwargs):
...         start = time.time()
...         res = fn(*args, **kwargs)
...         end = time.time()
...         print("Time: {}s.".format(round(end-start, 5)))
...         return res
...     return wrap
... 
>>> @ex_time
... def slow_fn(x):
...     # імітуємо вичислення
...     time.sleep(randint(1000, 3000)/1000)
...     return x*x*x
... 
>>> slow_fn(3)
Time: 2.71892s.
27

А можна цю версію трохи поліпшити. Декоратори також можуть приймати аргументи. Знаючи це, давайте напишемо покращену версію. Для цього сам декоратор потрібно обгорнути іншим декоратором (безумство, так):

>>> def ex_time(count):
...     def wrapper(fn):
...         def wrap(*args, **kwargs):
...             times = []
...             for i in range(count):
...                 start = time.time()
...                 res = fn(*args, **kwargs)
...                 end = time.time()
...                 times.append(round(end-start, 5))
...             print("Avg. time {avg}s. \
Max time {max}s. \
Min time {min}s."
...                     .format(avg=sum(times)/count, max=max(times), min=min(times)))
...             return res
...         return wrap
...     return wrapper
... 
>>> @ex_time(3)
... def slow_fn(x):
...     # імітуємо вичислення
...     time.sleep(randint(1000, 3000)/1000)
...     return x*x*x
... 
>>> slow_fn(3)
Avg. time 2.1003s. 
Max time 2.53272s. 
Min time 1.42262s.
27

Перевірка типів

Якщо ви сумуєте за Java або іншою статично типізованою мовою, то в мене для вас гарні новини: статичну типізацію можна частково прикрутити й до Python. Так, звісно, в нових версіях є вбудовані нотації типів, але вони містять лише рекомендаційний характер і ні на що не впливають.

>>> def typed(*types, **kwtypes):
...     def wrapper(fn):
...         def wrap(*f_args, **f_kwargs):
...             for i in range(len(types)):
...                 if type(f_args[i]) != types[i]:
...                     raise TypeError()
...             for k in kwtypes:
...                 if type(f_kwargs[k]) != kwtypes[k]:
...                     raise TypeError()
...             return fn(*f_args, **f_kwargs)
...         return wrap
...     return wrapper
... 
>>> @typed(int, int, int)
... def typed_fn(x, y, z):
...     return x*y*z
... 
>>> @typed(str, str, str)
... def typed_fn2(x, y, z):
...     return x+y+z
... 
>>> @typed(str, str, z=int)
... def typed_fn3(x, y, z="Hello! "):
...     return (x+y)*z
... 
>>> typed_fn(3, 4, 5)
60
>>> # Error:
... typed_fn(3, 4, "Doge")
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
  File "<stdin>", line 6, in wrap
TypeError
>>> 
>>> typed_fn2("Hello, ", "Codeguida", "!")
'Hello, Codeguida!'
>>> typed_fn3("Hello, ", "Codeguida! ", z=5)
'Hello, Codeguida! Hello, Codeguida! Hello, Codeguida! Hello, Codeguida! Hello, Codeguida! '

Транзакції

Транзакція в контексті баз даних — сеанс роботи з БД, в якому всі зміни можна відмінити одним лише викликом. Уявіть, ваш скрипт інтенсивно працює з БД, наповнюючи її, і тут вилітає якийсь Exception. І тепер у вас повна база не валідних записів, адже скрипт не завершив свою роботу. А якби ви використовували транзакції всі ці зміни можна було б відмінити викликавши щось типу db.rollback(). Давайте напишемо декоратор, що буде обгортати наші дії с БД в транзакцію:

def transaction(db):
    def wrapper(fn):
        def wrap(*args, **kwargs):
            res = None
            db.start_transaction()
            try:
                res = fn(*args, **kwargs)
            except Exception as e:
                db.rollback()
                raise e
            else:
                db.end_transaction()
            return res
        return wrap
    return wrapper

@transaction(db)
def action(db):
    db.ids.insert(1/0)

Висновок

Ось так можна використовувати ще один ninja-hack в Python — декоратори. Це доволі потужний інструмент, але він не підходить для багатьох задач. Тож треба бути обережним використовуючи його.

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 4.7K
Приєднався: 10 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація