Всім, хто хоч трохи працює з 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 — декоратори. Це доволі потужний інструмент, але він не підходить для багатьох задач. Тож треба бути обережним використовуючи його.
Ще немає коментарів