Поздравляем! Вы уже изучили все темы образовательного блока, и впереди — заключительное задание.
Итак: эта задача похожа на те, что решают Python-разработчики на практике. Она поможет вам повторить пройденный материал и применить полученные знания.
Задача состоит из нескольких блоков:
Контекст. В реальной жизни задача не существует сама по себе, а возникает из определённого бизнес-контекста. Поэтому мы добавили небольшое описание трансформации задачи, от первого запроса до её финального вида.
Подсказки. Помощь в решении, если зайдёте в тупик. Комментарии к ключевым моментам решения задачи в формате FAQ.
Разбор. Подробный разбор задачи от автора, специалиста из Яндекса: от составления алгоритма решения до реализации и пояснения каждого блока кода.
Важно: эта задача «со звёздочкой». Она посложнее предыдущих. Если не справитесь — ничего страшного: в программировании редко что-то получается с первого раза. Сделайте паузу, перечитайте теорию и возвращайтесь с новыми силами и хорошим настроением.
Повторите:
Функции
Лямбда-функция
Генераторы, декораторы
Работа с JSON
И ещё один совет напоследок: рекомендуем работать с задачей последовательно.
Изучите контекст.
Ознакомьтесь с условием задачи и пожеланиями.
Попробуйте решить её самостоятельно в Яндекс.Контесте.
Если понадобится помощь — обратитесь к подсказкам. Если совсем тяжко — изучите разбор и попробуйте после самостоятельно решить.
Когда решите задачу, сравните свой вариант с решением от специалиста.
Желаем удачи!
Чтобы добавить в заметки выделенный текст, нажмите Ctrl + E
Я Алексей, автор задачи. Решая её, вы погрузитесь в устройство современных веб-приложений и напишете имитацию сервера, который получает запросы от пользователей и перенаправляет их дальше.
Не переживайте — работать будем на уровне знаний из тем, которые вы недавно прошли. А когда решите задачу, покажу, как решал её я. Приглашаю вас пройти этот путь вместе со мной!
О подходе к современным приложениям
В разработке ПО важны скорость доставки новой функциональности до пользователя и качество кода.
Это влияет как на устройство рабочих команд, так и на организацию процесса разработки. Мы работаем в небольших командах по спринтам. Спринт — фиксированный отрезок времени от одной до четырёх недель, за который команда должна выкатить небольшую, но готовую функциональность.
Как правило, над одним продуктом параллельно работает несколько команд разработчиков. Чтобы они не «толкались локтями» и не мешали друг другу, умные люди придумали микросервисную архитектуру.
Архитектура — это способ организовать код в проекте. Таких способов может быть много: например, в предыдущей задаче наше приложение было спроектировано по трёхзвенной архитектуре (интерфейс/клиент, веб-сервер, база данных).
Конкретно микросервисная архитектура предполагает, что наша кодовая база состоит из множества небольших, изолированных сервисов, каждый из которых можно модернизировать независимо, не нарушая работу всего приложения.
Обычно такие сервисы проектируют так, чтобы их устройство и логика полностью умещались в голове одного разработчика.
Прежде чем двинуться дальше — аналогия
Представьте — вы с друзьями поехали на озеро с ночёвкой: пожарить шашлыки, половить рыбу, вот это вот всё. Приехали — и дальше есть много способов разбить лагерь.
Можно всем одновременно делать всё и сразу: и лодку надувать, и палатки ставить, и угли разжигать. Но каждый привык и умеет делать по-своему: в процессе все гарантированно переругаются и потратят больше времени на выяснение, «как надо».
Гораздо эффективнее назначить ответственного на каждый «сервис»: Андрей надувает лодку, Никита ставит палатки, Альберт занимается шашлыками, Костя накрывает на стол, Вадим готовит снасти.
Закончили, похвалили друг друга — дело сделано, можно отдыхать.
Продолжаем. Ниже на картинке пример устройства микросервисной архитектуры для маркетплейса. Маркетплейс состоит из нескольких сервисов, каждый из которых отвечает за свой кусок работы.
Вот как это всё работает:
Сначала поступает запрос от клиента (это веб- или мобильное приложение).
API Gateway как секретарь — изучает запрос и направляет его в нужный сервис.
Сервис обрабатывает запрос по своей логике, используя свою базу данных.
Обработав — возвращает ответ.
API Gateway этот ответ получает и отправляет клиенту.
Давайте мысленно погрузимся на уровень глубже — в один из сервисов.
Внутри каждого сервиса есть структура, похожая на API Gateway. Она называется ASGI/WSGI-сервер (англ. Asynchronous Server Gateway Interface / Web Server Gateway Interface).
Эта структура передаёт запросы обработчикам (их ещё называют «ручки» или «хендлеры»). А они уже расшифровывают, что хотел клиент, и «ходят» в базу за данными.
Вот схема одного из сервисов:
Теперь поговорим о запросах.
Запрос — это строка, составленная по конкретной структуре, которая активизирует работу определённой ручки.
Запрос обычно выглядит так:
1[HTTP-метод]/[маршрут]
Например:
1GET /categories/123
Сопоставим сущности:
Элементы
Пояснение — название столбцов
GET
HTTP‑метод в одно слово. Может быть ещё POST, PUT, DELETE, PATCH
/categories
Маршрут. Может быть сколь угодно вложенным, но хорошая практика — не делать больше трёх уровней.
Тут 123 — это id в категории
/123
Кроме того, в запросе могут быть уточняющие параметры:
В зависимости от каждой части запроса ASGI/WSGI-сервер передаёт его на реализацию в определённую ручку. Для примера «Сервис каталога» с картинки выше описание и назначение запроса может выглядеть следующим образом:
Таблица 4×8
HTTP-метод
Маршрут
Назначение
Название ручки
GET
/categories
Получить список всех категорий
get_all_categories_handler
GET
/categories/{id}
Получить информацию о конкретной категории по ID
get_category_by_id_handler
GET
/categories/{id}/children
Получить подкатегории указанной категории
get_subcategories_handler
GET
/categories/{id}/filters
Получить доступные фильтры для этой категории
get_category_filters_handler
GET
/categories/{id}/products
Получить товары по категории (с пагинацией)
get_products_by_category_handler
POST
/categories
Создать новую категорию (только для админов)
create_category_handler
PUT
/categories/{id}
Обновить данные категории
update_category_handler
DELETE
/categories/{id}
Удалить категорию
delete_category_handler
И вот мы наконец подобрались к сути задачи.
Вам нужно будет написать структуру, которая имитирует работу WSGI-сервера. Будете на вход получать запрос, определять, какая ручка его обрабатывает, вызывать функцию — обработчик запроса и возвращать ответ.
Формулировка задачи
Вы работаете разработчиком в компании, которая готовит школьников к ЕГЭ. Продукт компании — веб-приложение, в котором школьники могут проходить варианты тестов.
Команда уже подготовила и протестировала несколько функциональных блоков (ручек). Вам передали задачу собрать WSGI-сервер, который ловит запросы, обрабатывает их и передаёт в уже готовые ручки.
Вы посидели над задачей и поняли, что в один присест её не решить. Нужно декомпозировать — то есть разделить на несколько задачек поменьше. Получились три следующие последовательные задачи:
Сначала напишем генератор и обработчик запросов.
Затем с помощью декораторов соберём логи работы сервиса и проверку, авторизован ли пользователь.
А в конце проанализируем логи работы сервера и соберём небольшое summary о его использовании.
Помимо описания задачи, технический директор оставил иллюстрацию устройства работы WSGI-сервера для нашего сервиса.
И оставил ссылку на документацию, где описаны ручки, с которыми работает наш сервис. Там была эта таблица:
Таблица 4×8
HTTP-метод
Маршрут
Назначение
Название ручки
GET
/
Возвращает домашнюю страницу
home_handler
GET
/admin
Возвращает страницу администратора
admin_handler
GET
/user?name=
Возвращает профиль пользователя
user_handler
GET
(любой неопределенный)
Возвращает нашу версию страницы с ошибкой 404 Not found
not_found_handler
POST
/attempt?name={user_name}&task=
Отправляем попытку пользователя по задаче
attempt_handler
Вот как-то так. В целом стало понятнее, что нужно сделать.
Но на всякий случай мы подготовили три шпаргалки, которые расскажут о важных нюансах.
Имитация работы WSGI-сервера
Важно: информация ниже — для «общего развития». Ничего из описанного реализовывать не придётся, мы уже реализовали это сами. Просто хотим рассказать, как работает код из шаблона, который имитирует работу сервера.
Чтобы ASGI/WSGI-сервер оставался в рабочем состоянии и в любой момент мог принимать запросы, вы будете использовать метод .send().
1# Имитация работы сервера2app = wsgi_server()
3app.send(None)
45for request in sys.stdin:
6print(app.send(request))
Рассмотрим эти сущности подробнее.
wsgi_server() — это генератор, который вы реализуете.
app.send(value) — метод генератора app, который возобновляет выполнение генератора и передаёт значение value в текущее выражение yield внутри него.
А дальше в цикле передаются все запросы и по итогам обработки каждого выводится ответ.
1for request in sys.stdin:
2print(app.send(request))
Обратите внимание, что этот цикл может работать «вечно». Главное, что он будет продолжать работу после отправки каждого запроса app.send(). Чего мы и хотим достичь.
Как мы уже сказали выше, самим прописывать и использовать метод .send() вам не нужно. Подробнее про метод .send() можете прочитать в документации.
Как работать с декораторами
Чтобы логи запросов и обработки сервера представляли ценность, важно сохранять название запускавшихся ручек, переданные им параметры и возвращённый результат. Этому будет посвящена вторая задача.
Пример логов:
1{"handler":"home_handler","params":{},"response":[200,"Home Page"]}2{"handler":"not_found_handler","params":{},"response":[404,"Not Found"]}3{"handler":"attempt_handler","params":{"name":"Max","task":"Snake"},"response":[200,"Good try, Max! Your attempt on task 'Snake' is accepted!"]}
Расшифруем:
handler — название ручки;
params — параметры, которые передали ручке;
response — ответ, который вернула ручка после обработки.
Сохранять название ручек и параметры помогают декораторы, которые в общем виде выглядят так:
Вместо wrapper может быть любое название функции. Но чтобы другой разработчик понимал, что это обёртка, её обычно так и называют — wrapper.
Аргумент, с которым работает декоратор, это функция. Значит, мы можем обращаться к определённым атрибутам, характерным для функции.
Например:
Таблица свойств функции
Код
Что достаёт
Пример значения
func.__name__
Имя функции
'my_function'
func.__doc__
Докстринг функции
'Описание функции'
func.__module__
Имя модуля, где определена функция
'main' или 'my_module'
func.__annotations__
Аннотации типов аргументов и результата
{'x': int, 'return': str}
func.__defaults__
Значения аргументов по умолчанию
(42, True)
func.__kwdefaults__
Значения именованных аргументов по умолчанию
{'verbose': True}
func.__code__.co_varnames
Имена всех переменных в функции
('x', 'y', 'result')
func.__code__.co_argcount
Кол‑во позиционных аргументов
2
func.__code__.co_filename
Имя файла, где определена функция
'script.py'
func.__qualname__
Полное имя (учитывает вложенность)
'MyClass.method' или 'func'
Чтобы наша обёртка вывела название функции, можем воспользоваться атрибутом __name__. Применим его для простой функции greet(name), которая получает на вход имя и здоровается с пользователем.
Далее научимся обращаться к переданным параметрам функции. Можно обращаться отдельно к *args и **kwargs, как в примере, гибко передавая параметры в виде любого количества аргументов.
1deflog_call(func):
2defwrapper(*args, **kwargs):
3print(f"Вызвана функция: {func.__name__}")
4if args:
5print(f"Имя: {args[0]}")
6iflen(args) > 1:
7print(f"Возраст: {args[1]}")
8if"name"in kwargs:
9print(f"Имя (по ключу): {kwargs['name']}")
10if"age"in kwargs:
11print(f"Возраст (по ключу): {kwargs['age']}")
12return func(*args, **kwargs)
13return wrapper
1415@log_call16defgreet(name, age):
17returnf"Hello, {name}! You are {age} years old."1819greet("Max", 18)
20greet(name="Alice", age=25)
Вывод:
1Вызвана функция: greet
2Имя: Max
3Возраст: 184Вызвана функция: greet
5Имя (по ключу): Alice
6Возраст (по ключу): 25
Однако, чтобы упростить себе жизнь, можно передавать в функцию определённый объект, будь то список, словарь, JSON, класс и так далее. Главное, чтобы параметры были заданы в определённой структуре.
Например, здесь передаём аргументы в формате словаря {params}. И намного удобнее обращаться к его параметрам.
Выше мы использовали одно и то же обозначение параметров params и для функции greet, и для декоратора. Это для удобства, они необязательно должны совпадать.
Теперь поднимемся на уровень выше. Разберём, как в целом используются декораторы на уровне программы.
Во второй задаче у вас будет шаблон со следующей конструкцией. Каждая ручка обёрнута одним или несколькими декораторами:
1# Ручки для разных маршрутов2@log_call3defhome_handler(params):
4return200, "Home Page"56@log_call7defadmin_handler(params):
8return200, "Admin Panel"910@log_call11@require_auth12defuser_handler(params):
13# Должен использовать параметр name из URL14return200, f"Hello, {params['name']}!"151617@log_call18@require_auth19defattempt_handler(params):
20# Должен использовать параметр name из URL21return (
22200,
23f"Good try, {params['name']}! Your attempt on task '{params['task']}' is accepted!",
24 )
2526@log_call27defnot_found_handler(params):
28return404, "Not Found"
В зависимости от того, что мы хотим логировать в определённой ручке, создаётся цепочка декораторов. Для ручки home_handler сохраняем только логи вызова (@log_call). А для user_handler дополнительно сохраняем параметры авторизации (@log_call и @require_auth).
Это делает логирование более гибким.
Как настроить вложенность декораторов
Добавим к примеру с приветствием ещё один декоратор. И посмотрим, как будет происходить взаимодействие с параметрами функций, когда попробуем обернуть функцию в два декоратора.
Например, давайте проверим, есть ли 18 лет нашему пользователю.
1Вызвана функция: wrapper
2Имя: Макс
3Возраст: 184Привет, Макс! Тебе 18 лет.
5Вызвана функция: wrapper
6Имя: Коля
7Возраст: 168Ошибка: доступ запрещён для несовершеннолетних
Как можно увидеть, вместо названия функции greet, которую оборачивали в декоратор, программа вывела название функции в декораторе wrapper. Это происходит потому, что декоратор @log_call получает на вход не функцию greet, а функцию wrapper из декоратора @check_age. Чтобы метаданные функции greet сохранялись через цепочку декораторов, используют декоратор для декораторов @wraps.
Суть @wraps — перенести все атрибуты функции func во wrapper, чтобы их можно было использовать дальше по цепочке.
Получается, с помощью @wraps(func) обернули функцию внутри декоратора. Теперь, в какой последовательности сколько бы декораторов ни запускали — атрибуты первоначальной функции не будут пропадать. Что позволит нам не потерять их при логировании, а также не терять гибкости при использовании декораторов.
Вывод кода выше:
1Вызвана функция: greet
2Имя: Макс
3Возраст: 184Привет, Макс! Тебе 18 лет.
5Вызвана функция: greet
6Имя: Коля
7Возраст: 168Ошибка: доступ запрещён для несовершеннолетних
Ура, вы разобрались, как с помощью декораторов выстроить логирование любой вложенности декораторов с гибким подходом! Теперь точно можно переходить к задачам.
Чтобы добавить в заметки выделенный текст, нажмите Ctrl + E
Оцените ясность обучающего материала
Будем признательны, если вы поделитесь своим отзывом по финальной задаче и по блоку «4. Функции и их особенности в Python»