Python: знайомство з декораторами на прикладі FastAPI
В останнє декоратори в Python трогав ще років 10 тому, в Python 2, хочеться трохи оновити пам’ять, бо зараз почав доволі активно ними користуватись, ну і ще раз подивитись як жеж воно працює під капотом, і що воно таке взагалі.
Пост вийшов трохи... дивний? Бо перша половина — в стилі «у нас є одне яблуко, і ми до нього додаємо ще одне», а друга половина — якісь інтеграли. Але anyway — особисто в мене в голові картинка склалась, розуміння з’явилось, тому най буде так.
Отже, якщо коротко — Python decorator являє собою просто функцію, яка в аргументах приймає іншу функцію, і «додає» до неї якийсь новий функціонал.
Спочатку зробимо власний декоратор, подивимось як все це діло виглядає в пам’яті системи, а потім розберемо FaspAPI та його додавання роутів через app.get("/path").
В кінці будуть кілька корисних посилань, де більше детально розглядається теорія про функції і декоратори в Python, а тут буде суто практична частина.
Простий приклад Python decorator
Описуємо функцію, яка буде нашим декоратором, і нашу «робочу» функцію:
#!/usr/bin/env python
# a decorator function, which accetps another function as an argument
def decorator(func):
# extend the gotten function with a new feature
def do_domething():
print("I'm sothing")
# execute the function, passed in the argument
func()
# return the "featured" functionality
return do_domething
# just a common function
def just_a_func():
print("Just a text")
# run it
just_a_func()
Тут функція decorator() приймає аргументом будь-яку іншу функцію, а just_a_func() — наша «основна» функція, яка робить для нас якісь дії:
$ ./example_decorators.py Just a text
Тепер ми можемо зробити такий фінт — створимо змінну $decorated, яка буде посиланням на decorator(), аргументом до decorator() передамо нашу just_a_func(), і викличемо $decorated як функцію:
... # run it #just_a_func() # create a variable pointed to the decorator(), and pass the just_a_func() in the argument decorated = decorator(just_a_func) # call the function from the 'decorated' object decorated()
Результат — у нас виконається і «внутрішня» функція do_domething(), бо вона є в return функції decorator(), і функція just_a_func(), яку ми передали в аргументах — бо в decorator.do_domething() є її виклик:
$ ./example_decorators.py I'm sothing Just a text
А тепер замість того аби створювати змінну і їй призначати функцію decorator() з аргументом — ми можемо зробити те саме, але через виклик декоратора як @decorator перед нашою робочою функцією:
#!/usr/bin/env python
# a decorator function, which accetps another function as an argument
def decorator(func):
# extend the gotten function with a new feature
def do_domething():
print("I'm sothing")
# execute the function, passed in the argument
func()
# return the "featured" functionality
return do_domething
# just a common function
#def just_a_func():
# print("Just a text")
# run it
#just_a_func()
# create a variable pointed to the decorator(), and pass the just_a_func() in the argument
#decorated = decorator(just_a_func)
# call the function from the 'decorated' object
#decorated()
@decorator
def just_a_func():
print("Just a text")
just_a_func()
І отримаємо той самий результат:
$ ./example_decorators.py I'm sothing Just a text
Як працюють декоратори
Знаєте, чому з інфраструктурою простіше, ніж з програмуванням? Бо при роботі з серверами-мережами-кубернетесом у нас є якісь умовно-фізичні об’єкти, які ми можемо помацати руками і побачити очима. А в програмуванні — це все треба тримати в голові. Але є дуже дієвий лайф-хак: просто дивись на карту пам’яті процесу.
Давайте розберемо, що відбувається «під капотом», коли ми використовуємо декоратори:
def decorator(func): в пам’яті створюється об’єкт функціїdecorator()def just_a_func(): аналогічно, створюється об’єкт для функціїjust_a_func()decorated = decorator(just_a_func): створюється третій об’єкт — зміннаdecorated:- decorated в собі містить посилання на функцію
decorator() - аргументом до
decorator()передається посилання на адресу, де знаходитьсяjust_a_func() - функція
decorator()створює новий об’єкт —do_domething(), бо вона є в return уdecorator()do_domething()виконує якісь додаткові дії, і викликає функцію, яка передана вfunc
- decorated в собі містить посилання на функцію
В результаті, при виклику decorated як функції (тобто, з ()) — виконається функція do_domething(), а потім функція, яку передали аргументом, бо в аргументі func є посилання на функцію just_a_func().
Все це можна побачити в консолі:
>>> from example_decorators import * >>> decorator # check the decorator() address <function decorator at 0x7668b8eef2e0> >>> just_a_func # check the just_a_func() address <function just_a_func at 0x7668b8eef380> >>> decorated # check the decorated variable address <function decorator.<locals>.do_domething at 0x7668b8eef420>
Так як в decorated = decorator() ми створили посилання на функцію decorator() яка повертає свою внутрішню функцію do_domething(), то тепер decorated — це функція decorator.do_domething().
А у func ми будемо мати адресу just_a_func.
Для кращого розуміння — давайте просто глянемо на адреси пам’яті з функцією id():
#!/usr/bin/env python
# a decorator function, which accetps another function as an argument
def decorator(func):
# extend the gotten function with a new feature
def do_domething():
#print("I'm sothing")
print(f"Address of the do_domething() function: {id(do_domething)}")
# execute the function, passed in the argument
func()
print(f"Address of the 'func' argument: {id(func)}")
# return the "featured" functionality
return do_domething
# just a common function
def just_a_func():
return None
#print("Just a text")
print(f"Address of the decorator() function object: {id(decorator)}")
print(f"Address of the just_a_func() function object (before decoration): {id(just_a_func)}")
# run it
#just_a_func()
# create a variable pointed to the decorator(), and pass the just_a_func() in the argument
decorated = decorator(just_a_func)
decorated()
print(f"Address of the just_a_func() function object (after decoration): {id(just_a_func)}")
print(f"Address of the 'decorated' variable: {id(decorated)}")
Виконуємо скрипт, і маємо такий результат:
$ ./example_decorators.py Address of the decorator() function object: 130166777561632 Address of the just_a_func() function object (before decoration): 130166777574272 Address of the do_domething() function: 130166777574432 Address of the 'func' argument: 130166777574272 Address of the just_a_func() function object (after decoration): 130166777574272 Address of the 'decorated' variable: 130166777574432
Тут:
decorator(): об’єкт функції за адресою 130166777561632 (створюється під час запуску програми)just_a_func(): другий об’єкт функції за адресою 130166777574272 (створюється під час запуску програми)- виклик
decorator()вdecorated()створює об’єкт функційdo_domething(), який знаходиться за адресою 130166777574432 (створюється під час виконанняdecorator()) - в аргументі
funcпередається адреса об’єктуjust_a_func()— 130166777574272 - сама функція
just_a_func()не змінюється, і знаходиться там же ж — 130166777574272 - і змінна decorated тепер «відправляє» нас до функції
do_domething()за адресою 130166777574432, боdecorator()виконує return значенняdo_domething()

Реальний приклад з FastAPI
Ну і давайте глянемо як це використовується в реальному житті.
Наприклад, я до цього посту прийшов, бо робив нові роути для FastAPI, і мені стало цікаво — як же ж FastAPI app.get("/path") додає роути?
Створимо файл fastapi_routes.py з двома роутами:
#!/usr/bin/env python
from fastapi import FastAPI
app = FastAPI()
# main route
@app.get("/")
def home():
return {"message": "default route"}
# new route
@app.get("/ping")
def new_route():
return {"message": "pong"}
Що тут відбувається:
- створюємо інстанс класу FastAPI()
- через декоратор
@app.get("/")додаємо запуск функціїhome()при виклику path «/» - аналогічно робимо для запиту при виклику app з path «
/ping»
Встановлюємо fastapi та uvicorn:
$ python3 -m venv .venv $ . ./.venv/bin/activate $ pip install fastapi uvicorn
Запускаємо нашу програму з uvicorn:
$ uvicorn fastapi_routes:app --reload --port 8082 INFO: Will watch for changes in these directories: ['/home/setevoy/Scripts/Python/decorators'] INFO: Uvicorn running on http://127.0.0.1:8082 (Press CTRL+C to quit) INFO: Started reloader process [2700158] using StatReload INFO: Started server process [2700161] INFO: Waiting for application startup. INFO: Application startup complete.
...
Перевіряємо:
$ curl localhost:8082/
{"message":"default route"}
$ curl localhost:8082/ping
{"message":"pong"}
Як працює FastAPI get()
Як це працює?
Сама функція get() не є декоратором, але вона повертає декоратор — див. applications.py#L1460:
.. -> Callable[[DecoratedCallable], DecoratedCallable]: ... return self.router.get(...)
Тут:
->: return type annotation(анотація типу поверненого значення), тобтоget()повертає якийсь тип данихCallable[...]: повертається типCallable(функція)Callable[[DecoratedCallable], DecoratedCallable]: функція, яка повертається, приймає аргументом типDecoratedCallable, і повертає теж типDecoratedCallable- тип
DecoratedCallableописаний в types.py:DecoratedCallable = TypeVar("DecoratedCallable", bound=Callable[..., Any]):bound=Callableвказує, що типом даних може бути тільки функція (callable-об’єкт)- ця функція може приймати будь-які аргументи i може повертати будь-які дані — Any
- тип
- виклик
app.get()повертає методself.router.get()- а
self.router.get()— це метод APIRouter, який описаний в routing.py#:1366, і який повертає методself.api_route():- а функція
api_route(), яка описана в тому ж routing.py#L963 повертає функцію-декораторdecorator(func: DecoratedCallable)- а функція
decorator()викликає методadd_api_route()— в тому ж routing.py#L994:- а
add_api_routeпершим аргументом приймає path, а другим — функцію func, яку треба зв’язати з цим роутом - потім
add_api_route()повертає func
- а
- а
api_route()повертаєdecorator()
- а функція
router.get()повертаєapi_route()
- а функція
app.get()повертаєrouter.get()
- а
... def api_route( self, path: str, ... ), ) -> Callable[[DecoratedCallable], DecoratedCallable]: def decorator(func: DecoratedCallable) -> DecoratedCallable: self.add_api_route( path, func, ... ) return func return decorator ...
Ми могли б переписати цей код так — залишимо додавання «/» через app.get(), а для «/ping» зробимо аналогічно тому, як робили в нашому першому прикладі — через створення змінної.
Тільки тут треба робити два об’єкти — спершу для app.get(), а потім вже викликати decorator() і передавати нашу функцію:
#!/usr/bin/env python
from fastapi import FastAPI
app = FastAPI()
# main route
@app.get("/")
def home():
return {"message": "default route"}
# new route
def new_route():
return {"message": "pong"}
# create 'decorator' variable pointed to the app.get() function
# the 'decorator' then will return another function, the decorator() itself
decorator = app.get("/ping")
# create another variable using the decorator() returned by the get() above, and pass our function
decorated = decorator(new_route)
Результат буде аналогічним в обох випадках — і для «/„, і для “/ping».
Для більшої ясності — давайте це знову зробимо в консолі:
>>> from fastapi import FastAPI
>>> app = FastAPI()
>>> def new_route():
... return {"message": "pong"}
...
>>> decorator = app.get("/ping")
>>> decorated = decorator(new_route)
І перевіримо типи об’єктів та адреси пам’яті:
>>> app
<fastapi.applications.FastAPI object at 0x7381bb521940>
>>> app.get("/ping")
<function APIRouter.api_route.<locals>.decorator at 0x7381bb5a2480>
>>> decorator
<function APIRouter.api_route.<locals>.decorator at 0x7381bb5a2200>
>>> new_route
<function new_route at 0x7381bb5a22a0>
>>> decorated
<function new_route at 0x7381bb5a22a0>
Або навіть можемо просто використати метод add_api_route() напряму, прибравши виклик @app.get:
#!/usr/bin/env python
from fastapi import FastAPI
app = FastAPI()
# main route
#@app.get("/")
def home():
return {"message": "default route"}
app.add_api_route("/", home)
# new route
#@app.get("/ping")
def new_route():
return {"message": "pong"}
app.add_api_route("/ping", new_route)
Власне, це все, що треба знати про використання get() як декоратор в FastAPI.
18 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарівяка то просто классіка — в коменти прийшов тім лід, який про декоратори не шарить і про них додати йому нема шо, но хоче шоб нікто нікого відкрито не критикув і взагалі була дісципліна і повага один до одного.
Людина яка настільки неграмотна шо пише в профілі «обожнюю ліса, гори та навчатися» розказує усім про софт скіли )
Дякую за підсвітлення помилок, виправлю, давно не оновлював
Крихітна частина мови програмування не є настільки критично важливою в роботі, в порівнянні з тим як збирати команду, вести людей та з оптимальними зусиллями отримувати файний результат
Тим паче коли за свій досвід писав не на одній мові програмування
Давай я знову трохи підсвічу — паттерни або шаблони проектування не я частниною якойсь мови програмування. Вони зовсім незалажні від мов. Я дамаю рівень програміста без знань4-5 базових паттернів це трейні після першого року в універі. Нічого лічного, але тобі розумніше трохи здать назад — вибачиться там чи ше шось, бо я можу копнуть трохи далі в сторону тестування а твій персональний бренд може не витримати тиску. Мені воно не цікаво, а тобі воно не додає користі, хіба шо ти будуєш план навчання на цей рік і хочеш зрозуміть де в тебе пробєли в знаннях.
Патерни, в основі своїй, не є частиною мови, але в певних мовах це вже є мовною конструкцією. Я як раз і кажу про важливість розуміння патернів та гарних практик, щоб коли це потрібно та доцільно — використовувати
А надання фідбеку знайомим чи не знайомим людям — це скіл який не просто прокачується, на своїй шкурі дуже добре відчув
Вас понесло кудись в сторону деструктиву. Нема сенсу продовжувати дискусію
почекай, тема патернів як мовних конструкцій осталась на розкрита. Шо це ти мав на увазі ?
Під кожною статею, знайдеться такий анонім який має не прокачані Power skills(в минулому soft skills) та нагадить в душу автору
Такє враження, що треба заборонити людям бути анонімами, бо так дискусія скочуються до рівня «прекрасного.айті»
Я категорично проти цього — для новачків це гарний спосіб на ДОУ качати скіли роботи з «до##ями», яких буде ще багато на їх життєвому шляху
Автор пиши исче... то что ты тут написал — это тяжело читаемая академическая фигня, ни разу не видел что бы так сложно объясняли декораторы. Внизу Владислав одним примером показал почему декораторы так популярны — меньше кода, лучше читаемость, нет дублирования. Но все равно, спасибо за статью и пиши еще .
Хтось може мені пояснити які переваги використання декораторів порівняно з класичним ООП?
Які завдання вирішують декоратори краще ніж було до їх появи?
Тепер уяви векторний кеш:
— функція оброблює батчі поелементно
— треба знайти які ключі є у кешу, а всі інші обчислити
— ті шо обчислили — доплати в кеш, агрегувати і повернути результат батчу
Це вже десь 20 строк на ООП, і все ще одна строка із декоратором
ну код
такоє треба показати щоб можна було порівняти кількість коду
Переважно декоратор є зміст робити якщо він буде викликатися з багатьох місць. так само в ООП робиш метод який буде викликатися з багатьох інших методів. тобто нічого нового
Ну звісно ж з декількох. Сенс у тому що ви не зможете написати такий ООП метод щоб його використання коротше за приклад вище. Ну тобто зможете, але лише якщо зробите expensive computation віртуальним методом (але ж це ускладнення). Це все через те що декоратор дозволяє виконати щось ДО І ПІСЛЯ виклику декорованого методу. Це принципово для таких речей як той же кеш.
В ооп теж є декоратори, на поліморфізмі працюють
точно так же можна добавить максимальное возможное время выполнения функции, или логирование времени исполнения функции, или тригернуть какую то другую таску и добавить ее в очередь или сделать ресет кеша. Во всех этих случаях функции могут иметь разное количество параметров и принадлежать разным классам, но декоратор добавит им всем одинаковый сайдеффект. Если сидеть в чистом ооп то нада будет везде дублировать код, по крайней мере в питоне, в других языках декораторы редко пользуются.
Спробуйте wrapt — wrapt.readthedocs.io/en/master — «людький» інтерфейс для декораторів, який (на відміну від базового декорування) — веде себе очікувано в усіх edge cases (наприклад, не ламає рантайм тайп інференс після декорування)
Чи буде продовження про class decorators, і якісь більше in-depth приклади використання декораторів як наприклад contextlib.contextmanager або pytest fixtures