В останнє декоратори в 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
як функції (тобто, з ()
) – виконається функція 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()
виконує retrun значення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-об’єкт)- ця функція може приймати будь-які аргументи –
...,
- і може повертати будь-які дані –
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.