4.5. Проект «Создание ASGI/WSGI-сервера»
Вы уже изучили все темы раздела и сейчас подходите к заключительному заданию. Эта задача похожа на те, что решают Python-разработчики на практике. Она поможет вам повторить пройденный материал и применить полученные знания.
Задача состоит из нескольких блоков:
- Контекст. В реальной жизни задача не существует сама по себе, а возникает из определённого бизнес-контекста. Поэтому мы добавили небольшое описание трансформации задачи, от первого запроса до её финального вида.
- Подсказки. Помощь в решении, если зайдёте в тупик. Комментарии к ключевым моментам решения задачи в формате FAQ.
- Разбор. Подробный разбор задачи от автора, специалиста из Яндекса: от составления алгоритма решения до реализации и пояснения каждого блока кода.
Важно
Эта задача «со звёздочкой», она посложнее предыдущих. Если не справитесь — ничего страшного: в программировании редко что-то получается с первого раза. Сделайте паузу, перечитайте теорию и возвращайтесь с новыми силами и хорошим настроением.
Перед решением задачи
Повторите:
- Функции
- Лямбда-функция
- Генераторы, декораторы
- Работа с JSON
И ещё один совет напоследок: рекомендуем работать с задачей последовательно.
- Изучите контекст.
- Ознакомьтесь с условием задачи.
- Попробуйте решить её самостоятельно.
- Если понадобится помощь — обратитесь к подсказкам. Если совсем тяжко — изучите разбор и попробуйте после самостоятельно решить.
- Когда решите задачу, сравните свой вариант с решением от специалиста.
Проект

Алексей Бледнов
Разработчик в Яндекс Образовании
Привет! Я Алексей, автор задачи. Решая её, вы погрузитесь в устройство современных веб-приложений и напишете имитацию сервера, который получает запросы от пользователей и перенаправляет их дальше.
Не переживайте — работать будем на уровне знаний из тем, которые вы недавно прошли. А когда решите задачу, покажу, как решал её я. Приглашаю вас пройти этот путь вместе со мной!
Контекст
О подходе к современным приложениям
В разработке ПО важны скорость доставки новой функциональности до пользователя и качество кода.
Это влияет как на устройство рабочих команд, так и на организацию процесса разработки. Мы работаем в небольших командах по спринтам. Спринт — фиксированный отрезок времени от одной до четырёх недель, за который команда должна выкатить небольшую, но готовую функциональность.
Как правило, над одним продуктом параллельно работает несколько команд разработчиков. Чтобы они не «толкались локтями» и не мешали друг другу, умные люди придумали микросервисную архитектуру.
Архитектура — это способ организовать код в проекте. Таких способов может быть много: например, в предыдущей задаче наше приложение было спроектировано по трёхзвенной архитектуре (интерфейс/клиент, веб-сервер, база данных).
Конкретно микросервисная архитектура предполагает, что наша кодовая база состоит из множества небольших, изолированных сервисов, каждый из которых можно модернизировать независимо, не нарушая работу всего приложения.
Обычно такие сервисы проектируют так, чтобы их устройство и логика полностью умещались в голове одного разработчика.
Прежде чем двинуться дальше — аналогия
Представьте — вы с друзьями поехали на озеро с ночёвкой: пожарить шашлыки, половить рыбу, вот это вот всё. Приехали — и дальше есть много способов разбить лагерь.
Можно всем одновременно делать всё и сразу: и лодку надувать, и палатки ставить, и угли разжигать. Но каждый привык и умеет делать по-своему: в процессе все гарантированно переругаются и потратят больше времени на выяснение, «как надо».
Гораздо эффективнее назначить ответственного на каждый «сервис»: Андрей надувает лодку, Никита ставит палатки, Альберт занимается шашлыками, Костя накрывает на стол, Вадим готовит снасти.
Закончили, похвалили друг друга — дело сделано, можно отдыхать.
Продолжаем. Ниже на картинке пример устройства микросервисной архитектуры для маркетплейса. Маркетплейс состоит из нескольких сервисов, каждый из которых отвечает за свой кусок работы.
Вот как это всё работает:
- Сначала поступает
запросотклиента(это веб- или мобильное приложение). API Gatewayкак секретарь — изучает запрос и направляет его в нужныйсервис.Сервисобрабатываетзапроспо своей логике, используя своюбазу данных.- Обработав — возвращает
ответ. API Gatewayэтотответполучает и отправляетклиенту.
Давайте мысленно погрузимся на уровень глубже — в один из сервисов.
Внутри каждого сервиса есть структура, похожая на API Gateway. Она называется ASGI/WSGI-сервер (англ. Asynchronous Server Gateway Interface / Web Server Gateway Interface).
Эта структура передаёт запросы обработчикам (их ещё называют «ручки» или «хендлеры»). А они уже расшифровывают, что хотел клиент, и «ходят» в базу за данными.
Вот схема одного из сервисов:
Теперь поговорим о запросах.
Запрос — это строка, составленная по конкретной структуре, которая активизирует работу определённой ручки.
Запрос обычно выглядит так:
[HTTP-метод]/[маршрут]
Например:
GET /categories/123
Сопоставим сущности:
| Элементы | Пояснение — название столбцов |
|---|---|
| GET | HTTP‑метод в одно слово. Может быть ещё POST, PUT, DELETE, PATCH |
| /categories | Маршрут. Может быть сколь угодно вложенным, но хорошая практика — не делать больше трёх уровней. Тут 123 — это id в категории |
| /123 | Маршрут. Может быть сколь угодно вложенным, но хорошая практика — не делать больше трёх уровней. Тут 123 — это id в категории |
Кроме того, в запросе могут быть уточняющие параметры:
GET /categories/123/products?price_range=1000-5000&attributes=color:red
В зависимости от каждой части запроса ASGI/WSGI-сервер передаёт его на реализацию в определённую ручку. Для примера «Сервис каталога» с картинки выше описание и назначение запроса может выглядеть следующим образом:
| 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-сервера для нашего сервиса.
Также он оставил уточнения по формату запросов.
Запросы приходят в формате:
[HTTP-метод] /[маршрут]?[параметр1]=[значение1]&[параметр2]=[значение2]
Например:
GET /user?name=John&session=15
POST /attempt?name=Max&task=Snake
И оставил ссылку на документацию, где описаны ручки, с которыми работает наш сервис. Там была эта таблица:
| 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().
# Имитация работы сервера
app = wsgi_server()
app.send(None)
for request in sys.stdin:
print(app.send(request))
Рассмотрим эти сущности подробнее.
wsgi_server()— это генератор, который вы реализуете.app.send(value)— метод генератораapp, который возобновляет выполнение генератора и передаёт значениеvalueв текущее выражениеyieldвнутри него.
А дальше в цикле передаются все запросы и по итогам обработки каждого выводится ответ.
for request in sys.stdin:
print(app.send(request))
Обратите внимание, что этот цикл может работать «вечно». Главное, что он будет продолжать работу после отправки каждого запроса app.send(). Чего мы и хотим достичь.
Как мы уже сказали выше, самим прописывать и использовать метод .send() вам не нужно. Подробнее про метод .send() можете прочитать в документации.
Как работать с декораторами
Чтобы логи запросов и обработки сервера представляли ценность, важно сохранять название запускавшихся ручек, переданные им параметры и возвращённый результат. Этому будет посвящена вторая задача.
Пример логов:
{"handler": "home_handler", "params": {}, "response": [200, "Home Page"]}
{"handler": "not_found_handler", "params": {}, "response": [404, "Not Found"]}
{"handler": "attempt_handler", "params": {"name": "Max", "task": "Snake"}, "response": [200, "Good try, Max! Your attempt on task 'Snake' is accepted!"]}
Расшифруем:
handler— название ручки;params— параметры, которые передали ручке;response— ответ, который вернула ручка после обработки.
Сохранять название ручек и параметры помогают декораторы, которые в общем виде выглядят так:
def log_call(func):
def wrapper(*args, **kwargs):
…
return func(*args, **kwargs)
return wrapper
Вместо 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), которая получает на вход имя и здоровается с пользователем.
def log_call(func):
def wrapper(*args, **kwargs):
print(f"Вызвана функция: {func.__name__}")
return func(*args, **kwargs)
return wrapper
@log_call
def greet(name):
return f"Hello, {name}"
greet("Max")
Вывод будет следующий:
Вызвана функция: greet
Далее научимся обращаться к переданным параметрам функции. Можно обращаться отдельно к *args и **kwargs, как в примере, гибко передавая параметры в виде любого количества аргументов.
def log_call(func):
def wrapper(*args, **kwargs):
print(f"Вызвана функция: {func.__name__}")
if args:
print(f"Имя: {args[0]}")
if len(args) > 1:
print(f"Возраст: {args[1]}")
if "name" in kwargs:
print(f"Имя (по ключу): {kwargs['name']}")
if "age" in kwargs:
print(f"Возраст (по ключу): {kwargs['age']}")
return func(*args, **kwargs)
return wrapper
@log_call
def greet(name, age):
return f"Hello, {name}! You are {age} years old."
greet("Max", 18)
greet(name="Alice", age=25)
Вывод:
Вызвана функция: greet
Имя: Max
Возраст: 18
Вызвана функция: greet
Имя (по ключу): Alice
Возраст (по ключу): 25
Однако, чтобы упростить себе жизнь, можно передавать в функцию определённый объект, будь то список, словарь, JSON, класс и так далее. Главное, чтобы параметры были заданы в определённой структуре.
Например, здесь передаём аргументы в формате словаря {params}. И намного удобнее обращаться к его параметрам.
def log_call(func):
def wrapper(params):
print(f"Вызвана функция: {func.__name__}")
if "name" in params:
print(f"Имя: {params['name']}")
if "age" in params:
print(f"Возраст: {params['age']}")
return func(params)
return wrapper
@log_call
def greet(params):
return f"Привет, {params['name']}! Тебе {params['age']} лет."
greet({"name": "Макс", "age": 18})
greet({"name": "Алиса", "age": 25})
Вывод:
Вызвана функция: greet
Имя: Макс
Возраст: 18
Вызвана функция: greet
Имя: Алиса
Возраст: 25
Выше мы использовали одно и то же обозначение параметров params и для функции greet, и для декоратора. Это для удобства, они необязательно должны совпадать.
Теперь поднимемся на уровень выше. Разберём, как в целом используются декораторы на уровне программы.
Во второй задаче у вас будет шаблон со следующей конструкцией. Каждая ручка обёрнута одним или несколькими декораторами:
# Ручки для разных маршрутов
@log_call
def home_handler(params):
return 200, "Home Page"
@log_call
def admin_handler(params):
return 200, "Admin Panel"
@log_call
@require_auth
def user_handler(params):
# Должен использовать параметр name из URL
return 200, f"Hello, {params['name']}!"
@log_call
@require_auth
def attempt_handler(params):
# Должен использовать параметр name из URL
return (
200,
f"Good try, {params['name']}! Your attempt on task '{params['task']}' is accepted!",
)
@log_call
def not_found_handler(params):
return 404, "Not Found"
В зависимости от того, что мы хотим логировать в определённой ручке, создаётся цепочка декораторов. Для ручки home_handler сохраняем только логи вызова (@log_call). А для user_handler дополнительно сохраняем параметры авторизации (@log_call и @require_auth).
Это делает логирование более гибким.
Как настроить вложенность декораторов
Добавим к примеру с приветствием ещё один декоратор. И посмотрим, как будет происходить взаимодействие с параметрами функций, когда попробуем обернуть функцию в два декоратора.
Например, давайте проверим, есть ли 18 лет нашему пользователю.
def check_age(func):
def wrapper(params):
if params.get("age", 0) < 18:
return "Ошибка: доступ запрещён для несовершеннолетних"
return func(params)
return wrapper
def log_call(func):
def wrapper(params):
print(f"Вызвана функция: {func.__name__}")
if "name" in params:
print(f"Имя: {params['name']}")
if "age" in params:
print(f"Возраст: {params['age']}")
return func(params)
return wrapper
@log_call
@check_age
def greet(params):
return f"Привет, {params['name']}! Тебе {params['age']} лет."
print(greet({"name": "Макс", "age": 18}))
print(greet({"name": "Коля", "age": 16}))
Вывод будет следующим:
Вызвана функция: wrapper
Имя: Макс
Возраст: 18
Привет, Макс! Тебе 18 лет.
Вызвана функция: wrapper
Имя: Коля
Возраст: 16
Ошибка: доступ запрещён для несовершеннолетних
Как можно увидеть, вместо названия функции greet, которую оборачивали в декоратор, программа вывела название функции в декораторе wrapper. Это происходит потому, что декоратор @log_call получает на вход не функцию greet, а функцию wrapper из декоратора @check_age. Чтобы метаданные функции greet сохранялись через цепочку декораторов, используют декоратор для декораторов @wraps.
Суть @wraps — перенести все атрибуты функции func во wrapper, чтобы их можно было использовать дальше по цепочке.
Импортируем его:
from functools import wraps
Таким образом, код станет следующим:
from functools import wraps
def check_age(func):
@wraps(func)
def wrapper(params):
if params.get("age", 0) < 18:
return "Ошибка: доступ запрещён для несовершеннолетних"
return func(params)
return wrapper
def log_call(func):
@wraps(func)
def wrapper(params):
print(f"Вызвана функция: {func.__name__}")
if "name" in params:
print(f"Имя: {params['name']}")
if "age" in params:
print(f"Возраст: {params['age']}")
return func(params)
return wrapper
@log_call
@check_age
def greet(params):
return f"Привет, {params['name']}! Тебе {params['age']} лет."
print(greet({"name": "Макс", "age": 18}))
print(greet({"name": "Коля", "age": 16}))
Получается, с помощью @wraps(func) обернули функцию внутри декоратора. Теперь, в какой последовательности сколько бы декораторов ни запускали — атрибуты первоначальной функции не будут пропадать. Что позволит нам не потерять их при логировании, а также не терять гибкости при использовании декораторов.
Вывод кода выше:
Вызвана функция: greet
Имя: Макс
Возраст: 18
Привет, Макс! Тебе 18 лет.
Вызвана функция: greet
Имя: Коля
Возраст: 16
Ошибка: доступ запрещён для несовершеннолетних
Подробнее про использование wraps и модуль functools можете посмотреть в документации.
Ура, вы разобрались, как с помощью декораторов выстроить логирование любой вложенности декораторов с гибким подходом! Теперь точно можно переходить к задачам.
Если возникли сложности, обратитесь к подсказкам по каждой из трёх задач.
Подсказки
Не понимаю, с чего начать решение задачи № 1
Для начала разберитесь, чего от вас хотят, а потом изучите шаблон.
Шаблон состоит из нескольких частей:
- Перечень ручек, которые запускаются по запросу.
- Функция маршрутизации
router. Используйте её, чтобы получать нужную ручку для запроса. - Генератор запросов. Кусочек, который считывает текстовые запросы и загружает их в генераторную функцию wsgi_server().
- Сама генераторная функция
wsgi_server(). Её вам и нужно написать.
Для вас уже подготовили структуру функции, осталось только придумать, как обработать запрос, чтобы он вызвал нужную ручку с нужными параметрами. И передать ответ в нужном формате в response.
# WSGI-сервер (генератор) — эту функцию нужно реализовать!
def wsgi_server(requests):
"""Обрабатывает запросы и возвращает каждый из ответов через yield"""
request = yield None #запуск генератора, приём первого запроса app.send(None)
while True:
# Здесь необходимо написать обработку request и вызов нужной ручки для получения response
request = yield response
Для этого вам нужно:
- Разложить входящий запрос
requestна части: метод, путь, параметры. - Найти необходимую ручку с помощью функции
router. - Вернуть соответствующий
response.
Как получить необходимую ручку из функции маршрутизации router?
Если посмотрите на функцию маршрутизации router, то заметите, что она возвращает нетипичную конструкцию: не результат запуска выбранной по пути функции, а саму функцию.
# Функция маршрутизации
def router(path):
if path == "/":
return home_handler
elif path.startswith("/user"):
return user_handler
elif path.startswith("/attempt"):
return attempt_handler
elif path.startswith("/admin"):
return admin_handler
return not_found_handler
В Python функция является объектом. Это значит, что функции можно:
- передавать как аргументы,
- присваивать переменным,
- хранить в структурах данных,
- возвращать результатом из других функций.
Таким образом, чтобы забрать ручку из функции маршрутизации router и запустить, достаточно присвоить переменной результат исполнения и дальше работать с ним, как с функцией. В данном случае в переменной handler будет лежать функция attempt_handler.
request = “POST /attempt?name=Dima&task=1”
path = "/attempt?name=Dima&task=1"
params = {"name": "Dima", "task": "1"}
handler = router(path) #Идентично handler = attempt_handler
response = handler(params) #Применяете как обычную функцию и получаете ответ
Не понимаю, с чего начать решение задачи № 2
Сформулируйте, как должны работать декораторы. Что они должны получить на вход, что должны вывести на выход.
Для декоратора проверки авторизации @require_auth:
- На вход нужны переданные в ручку параметры.
- Внутри проверяем наличие параметра
name. - Если параметр
nameесть — то возвращаемresponseот ручки. - Если параметра
nameнет, то возвращаем в response код состояния ответа 403 — Forbidden.
Подробнее о кодах состояния HTTP можно почитать в статье по кодам состояний ответов HTTP.
Для декоратора логирования @log_call:
- На вход нужны название вызываемой ручки, переданные в неё параметры и итоговый
response. - Внутри всё необходимое собираем в JSON.
- Этот JSON выводим.
Формат вывода в JSON приведён в примере задачи:
{"handler": "home_handler", "params": {}, "response": [200, "Home Page"]}
{"handler": "not_found_handler", "params": {}, "response": [404, "Not Found"]}
{"handler": "attempt_handler", "params": {"name": "Max", "task": "Snake"}, "response": [200, "Good try, Max! Your attempt on task 'Snake' is accepted!"]}
Как вывести логи в необходимом формате в задаче № 2?
Формат вывода логов — JSON с определённым набором данных. Ниже пример похожей структуры и вывода с помощью функции dumps(). Она позволяет как раз вывести JSON в нужном нам формате.
from json import dumps
log_entry_en = {
"handler": "greet",
"params": {"name": "Max", "age": 18},
"response": "Hello, Max! You are 18 years old."
}
print(dumps(log_entry_en))
Вывод:
{"handler": "greet", "params": {"name": "Max", "age": 18}, "response": "Hello, Max! You are 18 years old."}
Если исходный текст написан кириллицей, то необходимо добавить параметр ensure_ascii=False, чтобы текст остался читаемым.
log_entry_ru = {
"handler": "greet",
"params": {"name": "Алиса", "age": 25},
"response": "Привет, Алиса! Тебе 25 лет."
}
print(dumps(log_entry_ru, ensure_ascii=False))
Вывод:
{"handler": "greet", "params": {"name": "Алиса", "age": 25}, "response": "Привет, Алиса! Тебе 25 лет."}
Без ensure_ascii=False вывод будет следующим:
{"handler": "greet", "params": {"name": "\u0410\u043b\u0438\u0441\u0430", "age": 25}, "response": "\u041f\u0440\u0438\u0432\u0435\u0442, \u0410\u043b\u0438\u0441\u0430! \u0422\u0435\u0431\u0435 25 \u043b\u0435\u0442."}
Подробнее о функции dumps() можете прочитать в документации.
Не понимаю, с чего начать решение задачи № 3
Это максимально прямолинейная задача, в которой:
- Считываем данные в нужном формате.
- Рассчитываем нужные параметры.
- Делаем вывод в нужном формате.
Для расчёта данных используйте лямбда-функцию в комбинации с функциями для работы с коллекциями: filter(), map(), set(), max().
Как в задаче № 3 считать входные данные?
Входные данные подаются в формате построчного JSON.
{"handler": "attempt_handler", "params": {"name": "Max", "task": "Snake"}, "response": [200, "Good try, Max! Your attempt on task 'Snake' is accepted!"]}
{"handler": "attempt_handler", "params": {"name": "Ivan", "task": "Queens"}, "response": [200, "Good try, Ivan! Your attempt on task 'Queens' is accepted!"]}
{"handler": "attempt_handler", "params": {"name": "Max", "task": "TwoSum"}, "response": [200, "Good try, Max! Your attempt on task 'TwoSum' is accepted!"]}
{"handler": "attempt_handler", "params": {"name": "Alex", "task": "Queens"}, "response": [200, "Good try, Alex! Your attempt on task 'Queens' is accepted!"]}
{"handler": "attempt_handler", "params": {"name": "Mari", "task": "Parenthesis"}, "response": [200, "Good try, Mari! Your attempt on task 'Parenthesis' is accepted!"]}
{"handler": "attempt_handler", "params": {"name": "Max", "task": "Parenthesis"}, "response": [200, "Good try, Max! Your attempt on task 'Parenthesis' is accepted!"]}
{"handler": "attempt_handler", "params": {"name": "Alex", "task": "Snake"}, "response": [200, "Good try, Alex! Your attempt on task 'Snake' is accepted!"]}
{"handler": "attempt_handler", "params": {"name": "Ivan", "task": "Snake"}, "response": [200, "Good try, Ivan! Your attempt on task 'Snake' is accepted!"]}
Вам нужна функция, которая бы могла считать строку и перевести её, например, в словарь. Это функция называется loads(). Чтобы её вызвать, необходимо будет подключить модуль json.
import json
json_string = '{"handler": "attempt_handler", "params": {"name": "Max", "task": "Snake"}, "response": [200, "Good try, Max! Your attempt on task \'Snake\' is accepted!"]}'
data = json.loads(json_string)
По итогам работы программы в переменной data окажется словарь:
{
'handler': 'attempt_handler',
'params': {
'name': 'Max',
'task': 'Snake'
},
'response': [
200,
"Good try, Max! Your attempt on task 'Snake' is accepted!"
]
}
Если читаете этот текст — значит, у вас получилось справиться с задачами или возникли трудности. В любом случае вы взялись за эту задачу — а это похвально!
Дальше советуем изучить, как эту задачу решал автор. Это поможет вам перенять часть полезных фишек в программировании и в мышлении. Подсматривая за работой старших товарищей, вы научитесь полезным привычкам и прокачаете насмотренность.
Ниже — код автора. Изучите его и сравните со своим решением. Или изучите основные идеи и попробуйте самостоятельно их реализовать. А дальше автор описал размышления о подходе к задачам 1–3 и разобрал каждый кусочек кода.
Загляните в блоки, к которым остались вопросы или в которых вы увидели новые элементы решения.
Разбор
Посмотреть решение задачи 1
import sys
def home_handler(params):
return 200, "Home Page"
def admin_handler(params):
return 200, "Admin Panel"
def user_handler(params):
# Должен использовать параметр name из URL
return 200, f"Hello, {params['name']}!"
def attempt_handler(params):
# Должен использовать параметр name из URL
return (
200,
f"Good try, {params['name']}! Your attempt on task '{params['task']}' is accepted!",
)
def not_found_handler(params):
return 404, "Not Found"
# Функция маршрутизации
def router(path):
if path == "/":
return home_handler
elif path.startswith("/user"):
return user_handler
elif path.startswith("/attempt"):
return attempt_handler
elif path.startswith("/admin"):
return admin_handler
return not_found_handler
def wsgi_server():
"""Обрабатывает запрос и возвращает ответ через yield"""
request = yield None
while True:
# Парсинг запроса
first_line = request.split("\r\n")[0]
method, path = first_line.split()
# Разделяем путь и параметры
params = {}
if "?" in path:
query = path.split("?")[1]
for pair in query.split("&"):
if "=" in pair:
key, value = pair.split("=")
params[key] = value
# Выбор handler'а
handler = router(path)
# Получение ответа (все сразу, без разделения на header/body)
response = handler(params)
request = yield response
# Имитация работы сервера
app = wsgi_server()
app.send(None)
for request in sys.stdin:
print(app.send(request))
Посмотреть решение задачи 2
import sys
from functools import wraps
from json import dumps
def require_auth(func):
"""Декоратор проверки авторизации"""
@wraps(func)
def wrapper(params):
if "name" not in params:
return 403, "Forbidden"
return func(params)
return wrapper
def log_call(func):
"""Декоратор логирования"""
@wraps(func)
def wrapper(params):
result = func(params)
log_entry = {
"handler": func.__name__,
"params": params,
"response": result,
}
print(dumps(log_entry))
return result
return wrapper
# Ручки для разных маршрутов
@log_call
def home_handler(params):
return 200, "Home Page"
@log_call
def admin_handler(params):
return 200, "Admin Panel"
@log_call
@require_auth
def user_handler(params):
# Должен использовать параметр name из URL
return 200, f"Hello, {params['name']}!"
@log_call
@require_auth
def attempt_handler(params):
# Должен использовать параметр name из URL
return (
200,
f"Good try, {params['name']}! Your attempt on task '{params['task']}' is accepted!",
)
@log_call
def not_found_handler(params):
return 404, "Not Found"
# Функция маршрутизации
def router(path):
if path == "/":
return home_handler
elif path.startswith("/user"):
return user_handler
elif path.startswith("/attempt"):
return attempt_handler
elif path.startswith("/admin"):
return admin_handler
else:
return not_found_handler
def wsgi_server():
"""Обрабатывает запрос и возвращает ответ через yield"""
request = yield None
while True:
# Парсинг запроса
first_line = request.split("\r\n")[0]
method, path = first_line.split()
# Разделяем путь и параметры
params = {}
if "?" in path:
query = path.split("?")[1]
for pair in query.split("&"):
if "=" in pair:
key, value = pair.split("=")
params[key] = value
# Выбор ручки
handler = router(path)
# Получение ответа (все сразу, без разделения на header/body)
response = handler(params)
request = yield response
# Имитация работы сервера
app = wsgi_server()
app.send(None)
for request in sys.stdin:
app.send(request)
Посмотреть решение задачи 3
import sys
from json import loads
from collections import defaultdict
# читаем строки ДО выхода из with
with open("app.log", "r", encoding="utf-8") as f:
lines = f.readlines()
# теперь файл можно закрыть, данные у нас
lines_to_dicts = map(lambda line: loads(line), lines)
attempt_logs = [
log for log in lines_to_dicts
if log.get("handler") == "attempt_handler"
]
if not attempt_logs:
print("No attempt logs found")
sys.exit(1)
unique_users = sorted({log["params"]["name"] for log in attempt_logs})
task_counts = defaultdict(int)
for log in attempt_logs:
task_counts[log["params"]["task"]] += 1
most_popular_task = max(task_counts.items(), key=lambda item: item[1])
print("Analytics Results:")
print(f"Unique users who solved tasks: {len(unique_users)}")
print(f"Unique users list: {', '.join(unique_users)}")
print(f"Task popularity: {dict(sorted(task_counts.items()))}")
print(
f"Most popular task: '{most_popular_task[0]}' with {most_popular_task[1]} attempts"
)
Задача № 1. WSGI-сервер как функция
Ключевое открытие — оказывается, генераторная функция может не только возвращать, но и получать данные на вход и с ними работать. Всё благодаря методу .send(), который мы рассмотрели в описании задачи.
Прежде чем переходить к решению, разберём, из чего состоит шаблон и что от нас вообще хотят.
Из каких частей состоит шаблон
Шаблон состоит из четырёх частей. Разберём их подробнее.
Часть № 1: генератор запросов
Реализован с помощью метода .send(). Каждый новый запрос улетает в генераторную функцию wsgi_server().
# Имитация работы сервера
app = wsgi_server()
app.send(None)
for request in sys.stdin:
print(app.send(request))
Часть № 2: генераторная функция
Её нужно написать. Основная роль этой функции — находить нужного адресата-ручку и запускать его с конкретными параметрами.
# WSGI-сервер (генератор) — эту функцию нужно реализовать!
def wsgi_server(requests):
"""Обрабатывает запросы и возвращает каждый из ответов через yield"""
request = yield None #запуск генератора, приём первого запроса app.send(None)
while True:
# Здесь необходимо написать обработку request и вызов нужной ручки для получения response
request = yield response
Часть № 3: функция-маршрутизатор
Можно её использовать, чтобы по заданному пути вызвать нужную ручку. Это вспомогательный помощник для генераторной функции.
# Функция маршрутизации
def router(path):
if path == "/":
return home_handler
elif path.startswith("/user"):
return user_handler
elif path.startswith("/attempt"):
return attempt_handler
elif path.startswith("/admin"):
return admin_handler
return not_found_handler
Часть № 4: сами ручки (handler)
Перечень ручек, к которым обращается генераторная функция. В зависимости от запроса запустится одна из них.
# Ручки для разных маршрутов
def home_handler(params):
return 200, "Home Page"
def admin_handler(params):
return 200, "Admin Panel"
def user_handler(params):
# Должен использовать параметр name из URL
return 200, f"Hello, {params['name']}!"
def attempt_handler(params):
# Должен использовать параметр name из URL
return (
200,
f"Good try, {params['name']}! Your attempt on task '{params['task']}' is accepted!",
)
def not_found_handler(params):
return 404, "Not Found"
Шаги решения задачи
Получается, вам нужно собрать генераторную функцию, которая на вход получает строку вида:
[метод] [путь] [параметры]
Например, GET /user?name=John
- метод — в основном в задаче используется GET или POST, бывают ещё PUT, PATCH. Не будем вдаваться в подробности, что делает каждый метод: об этом мы расскажем в одном из дальнейших статей.
- путь — путь до ручки, как директория в проводнике. Например, просто
/или/user. - параметры — параметры, передаваемые в ручку для обработки, в формате
?Параметр_1=Значение_1&Параметр_2=Значение2. Например,?name=John.
А на выходе выдаёт ответ от вызванных ручек.
То есть, чтобы решить задачу, нужно:
- Вычленить из строки метод, путь и параметры.
- Найти ручки, которые будем запускать.
- Получить и вернуть ответ от нужной ручки с переданными параметрами.
Разберём каждый шаг.
Шаг № 1: вычленить из строки метод, путь и параметры
Для этого последовательно вызовем метод .split().
- Метод отделяется от остальной части с помощью пробела.
- Путь и параметры отделяются друг от друга с помощью символа
?. - Сами различные параметры и их значения отделяются друг от друга с помощью символов
&и=.
def wsgi_server():
"""Обрабатывает запрос и возвращает ответ через yield"""
request = yield None
while True:
# Парсинг запроса
first_line = request.split('\r\n')[0]
method, path = first_line.split()
# Разделяем путь и параметры
params = {}
if '?' in path:
query = path.split('?')[1]
for pair in query.split('&'):
if '=' in pair:
key, value = pair.split('=')
params[key] = value
…
Шаг № 2: найти ручки, которые будете запускать
Это можно сделать с помощью уже прописанной функции router().
def wsgi_server():
"""Обрабатывает запрос и возвращает ответ через yield"""
request = yield None
while True:
# Парсинг запроса
…
# Выбор ручки
handler = router(path)
…
Шаг № 3: получить и вернуть ответ от нужной ручки с переданными параметрами
Для этого запустим функцию, найденную router.
def wsgi_server():
"""Обрабатывает запрос и возвращает ответ через yield"""
request = yield None
while True:
# Парсинг запроса
…
# Выбор ручки
handler = router(path)
# Получение ответа
response = handler(params)
request = yield response
…
Про тестирование
В рамках задачи не просят обрабатывать крайние значения и уверяют, что обязательно будут нужные параметры в нужном виде, если ручкам они нужны.
Поэтому главное — организовать вывод всех ручек, с разными методами и избыточным количеством параметров.
Например, можно проверить корректность следующей цепочкой вызовов:
GET /
GET /admin
GET /user?name=John
GET /unknown
POST /attempt?name=Alex&task=queens
GET /attempt?name=Vasya&task=snake
Вывод будет:
(200, 'Home Page')
(200, 'Admin Panel')
(200, 'Hello, John!')
(404, 'Not Found')
(200, "Good try, Alex! Your attempt on task 'queens' is accepted!")
(200, "Good try, Vasya! Your attempt on task 'snake' is accepted!")
Что в итоге
Давайте зафиксируем промежуточные итоги после первой задачи:
- Разобрались в чужом коде.
- Познакомились с работой генератора с помощью метода
.send() - Повторили использование
.split()
Задача № 2. Декораторы
Ключевое открытие — можно использовать несколько декораторов вместе. Как устроено их взаимодействие, разобрали на странице с описанием задачи.
Прежде чем переходить к решению, разберём, из чего состоит шаблон и что от нас вообще хотят.
Из каких частей состоит шаблон
Шаблон состоит из двух частей. Расскажем о каждой подробнее.
Часть № 1: блок из предыдущей задачи
Генераторную функцию нужно вставить из первой задачи. Набор ручек, маршрутизатор и генератор запросов остались неизменны.
При этом декораторы логирования и авторизации (@log_call и @require_auth) уже расставлены для разных ручек. Расставлять их не нужно.
# Ручки для разных маршрутов
@log_call
def home_handler(params):
return 200, "Home Page"
@log_call
def admin_handler(params):
return 200, "Admin Panel"
@log_call
@require_auth
def user_handler(params):
# Должен использовать параметр name из URL
return 200, f"Hello, {params['name']}!"
@log_call
@require_auth
def attempt_handler(params):
# Должен использовать параметр name из URL
return (
200,
f"Good try, {params['name']}! Your attempt on task '{params['task']}' is accepted!",
)
@log_call
def not_found_handler(params):
return 404, "Not Found"
Часть № 2: функции декораторов
Их нам и нужно прописать.
def require_auth(func):
"""Декоратор проверки авторизации"""
pass
def log_call(func):
"""Декоратор логирования"""
pass
Декоратор проверки авторизации проверяет, есть ли имя пользователя (name) в параметрах, и возвращает код ошибки (403, “Forbidden”), если не находит параметр.
Декоратор логирования выводит в формате JSON, какая ручка была запущена, с какими параметрами и какой ответ обработки нужно вернуть.
{"handler": "attempt_handler", "params": {"name": "Alex", "task": "Queens"}, "response": [200, "Good try, Alex! Your attempt on task 'Queens' is accepted!"]}
Шаги решения задачи
Задача сводится к определённой работе с параметрами вызываемых ручек.
- Для проверки авторизации — анализируем конкретный параметр и делаем проверку.
- Для логирования — собираем определённый набор данных из работы ручки и переданных параметров.
Разберём каждый шаг подробнее.
Шаг № 1: пишем декоратор проверки авторизации
На вход он берёт параметры функции и проходится по ним в поиске параметра name. Если не находит, то возвращает соответствующий ответ.
def require_auth(func):
"""Декоратор проверки авторизации"""
@wraps(func)
def wrapper(params):
if "name" not in params:
return 403, "Forbidden"
return func(params)
return wrapper
При этом обратите внимание, что в некоторых частях кода декоратор @require_auth будет входной функцией для декоратора логирования @log_call.
@log_call
@require_auth
def attempt_handler(params):
…
А для декоратора логирования @log_call важно, чтобы он получил метаданные первичной функции, так как они нужны будут ему для вывода. Поэтому не забывайте использовать декоратор для декораторов @wraps. Подробнее о нём мы рассказали в описании задачи.
Шаг № 2: пишем декоратор логирования
Ему для работы нужно название вызываемой ручки, перечень параметров, с которыми происходил вызов, и какой ответ после обработки вернулся.
def log_call(func):
"""Декоратор логирования"""
@wraps(func)
def wrapper(params):
result = func(params)
log_entry = {
"handler": func.__name__,
"params": params,
"response": result,
}
print(dumps(log_entry))
return result
return wrapper
Чтобы случайно в func.__name__ не прилетело название другого декоратора, убеждаемся, что в них используется декоратор для декораторов @wraps. Рассмотрим, как будет работать декоратор в связке с декоратором проверки авторизации @require_auth на примере.
@log_call
@require_auth
def attempt_handler(params):
# Должен использовать параметр name из URL
return (
200,
f"Good try, {params['name']}! Your attempt on task '{params['task']}' is accepted!",
)
Допустим, на вход подаётся следующий вызов:
POST /attempt?name=Alex&task=Queens
Генераторная функция wsgi_server() запустит вызов ручки attempt_handler.
- Декораторы срабатывают снизу вверх при оборачивании. Так что первым сработает декоратор
@require_auth. - Он найдёт параметр name и вернёт из ручки значение
200, "Good try, Alex! Your attempt on task 'Queens' is accepted!" - Декоратор
@log_callсоздаст лог, состоящий из следующих параметров:
—handler— благодаря декоратору декораторов@wrapsв@require_auth.
—params— это переданные в ручку параметры.
—response— это ответ от декоратора@require_auth.
На выходе получаем JSON в нужном формате:
{"handler": "attempt_handler", "params": {"name": "Alex", "task": "Queens"}, "response": [200, "Good try, Alex! Your attempt on task 'Queens' is accepted!"]}
Если бы входной запрос был таким:
POST /attempt?nam=Alex&task=Queens
Тогда бы декоратор @require_auth не нашёл параметр name в запросе и вернул значение 403, “Forbidden”.
А на выходе мы получили бы такой JSON:
{"handler": "attempt_handler", "params": {"nam": "Alex", "task": "Queens"}, "response": [403, "Forbidden"]}
Также обратите внимание на вывод JSON с помощью функции dumps(). Подробнее о ней рассказали в подсказках.
Про тестирование
Надо проверить, что:
- Для любой ручки получаем корректный response при наличии одного или нескольких параметров в запросе.
- Параметры вызываемой ручки передаются корректно.
- Финальный вывод в JSON организован верно.
Так как первый пункт включает автоматически проверку второго и третьего пунктов, то достаточно прогнать через ввод тесты для вызовов всех возможных response с одним или несколькими параметрами на входе (где параметры ожидаются).
Например, можно проверить корректность следующей цепочкой вызовов:
GET /
GET /admin
GET /unknown
POST /attempt?name=Max&task=Frogs
POST /attempt?name=Alex&task=Queens
POST /user?name=Ivan
POST /user?name=Mari&group=158
POST /attempt?nam=Pasha
POST /attempt?nam=Grisha&task=Snakes
POST /user?nam=Petr
POST /user?nam=Sasha&group=104
Вывод будет:
{"handler": "home_handler", "params": {}, "response": [200, "Home Page"]}
{"handler": "admin_handler", "params": {}, "response": [200, "Admin Panel"]}
{"handler": "not_found_handler", "params": {}, "response": [404, "Not Found"]}
{"handler": "attempt_handler", "params": {"name": "Max", "task": "Frogs"}, "response": [200, "Good try, Max! Your attempt on task 'Frogs' is accepted!"]}
{"handler": "attempt_handler", "params": {"name": "Alex", "task": "Queens"}, "response": [200, "Good try, Alex! Your attempt on task 'Queens' is accepted!"]}
{"handler": "user_handler", "params": {"name": "Ivan"}, "response": [200, "Hello, Ivan!"]}
{"handler": "user_handler", "params": {"name": "Mari", "group": "158"}, "response": [200, "Hello, Mari!"]}
{"handler": "attempt_handler", "params": {"nam": "Pasha"}, "response": [403, "Forbidden"]}
{"handler": "attempt_handler", "params": {"nam": "Grisha", "task": "Snakes"}, "response": [403, "Forbidden"]}
{"handler": "user_handler", "params": {"nam": "Petr"}, "response": [403, "Forbidden"]}
{"handler": "user_handler", "params": {"nam": "Sasha", "group": "104"}, "response": [403, "Forbidden"]}
Что в итоге
Зафиксируем промежуточные итоги после второй задачи:
- Реализовали рабочую цепочку декораторов.
- Поняли, зачем нужен декоратор для декораторов
@wraps() - Изучили, как обращаться к метапараметрам функции.
- Ещё раз познакомились с выводом JSON через
dumps().
Задача № 3. Лямбда-функция
Ключевое открытие — можно пользоваться лямбда-функциями для быстрой реализации типичных действий при работе со списками, кортежами или словарями: фильтрации, сортировки, поиска минимального или максимального значения, считывания информации.
Шаги решения задачи
Это самая прямолинейная задача из всех трёх!
Нам нужно:
-
Считать данные.
-
Найти всё, что нас просят:
2.1. логи только для
attempt_handler;2.2. множество уникальных пользователей;
2.3. количество решений для каждой задачи;
2.4. самые популярные задачи.
-
Вывести всё собранное в нужном формате.
Разберём каждый шаг подробнее.
Шаг № 1: считать данные
Используем функцию loads(), которая преобразует строку в формате JSON в словарь. Подробнее про неё мы рассказали в подсказке.
import sys
from json import loads
from collections import defaultdict
with open("app.log", "r", encoding="utf-8") as f:
lines = f.readlines()
lines_to_dicts = map(lambda line: loads(line), lines)
Давайте разберёмся, что тут происходит.
Первым делом считываем все строчки из файла app.log.
with open("app.log", "r", encoding="utf-8") as f:
lines = f.readlines()
Затем сохраняем их в ленивый итератор lines_to dicts. Реализуем с помощью связки лямбда-функции и map().
lambda line: loads(line) — это лямбда-функция, которая преобразовывает строку в словарь:
# Было
{"handler":"attempt_handler","params":{"name":"Max","task":"Snake"},"response":[200,"Good try, Max! Your attempt on task 'Snake' is accepted!"]}
# Стало
{
'handler': 'attempt_handler',
'params': {
'name': 'Max',
'task': 'Snake'
},
'response': [
200,
"Good try, Max! Your attempt on task 'Snake' is accepted!"
]
}
А map() — итератор, который по одной строке применяет лямбда-функцию к строкам в lines.
Таким образом, в переменной lines_to_dicts лежит ленивый итератор, который можно развернуть по запросу. Самый простой способ такой: list(lines_to_dicts), получите список словарей.
Для оптимизации кода в этом кусочке используем лямбда-функцию:
lambda line: loads(line)
Альтернативно можно написать отдельную функцию, и тогда код выглядел бы так:
def parse(line):
return loads(line)
lines_to_dicts = map(parse, sys.stdin)
Но здесь три строчки вместо одной и дополнительная сущность в виде именованной функции parse(). Налицо усложнение!
Шаг № 2: найти всё, что нас просят
Пойдём по порядку.
Чтобы отфильтровать словари, которые соответствуют ручке attempt_handler, воспользуемся функцией filter(). Она принимает на вход функцию и коллекцию, а возвращает ленивый итератор с теми элементами, для которых функция дала True.
А далее с помощью функции list прогоняем ленивый итератор и получаем список словарей с нужной ручкой.
# Фильтруем логи только для attempt_handler
attempt_logs = list(
filter(lambda log: log["handler"] == "attempt_handler", lines_to_dicts)
)
Чтобы создать множество уникальных пользователей, воспользуемся функцией set().
# Уникальные пользователи
unique_users = set(map(lambda log: log["params"]["name"], attempt_logs))
Что тут происходит:
- Проходимся по каждому словарю в отфильтрованном списке
attempt_logs. - С помощью лямбда-функции
lambda log: log["params"]["name"]достаём имя пользователя. - С помощью функции
map()проходимся по каждому элементу изattempt_logs. - Проходя по каждому элементу, добавляем его во множество с помощью функции
set(). Это позволяет убрать повторы.
Здесь функция map() тоже играет роль оптимизации, заменяя работу цикла for. Альтернативно код можно написать так, но в таком виде получаем три строчки вместо одной и лишний цикл:
users = set()
for log in attempt_logs:
users.add(log["params"]["name"])
Для подсчёта решений каждой задачи пройдёмся циклом по отфильтрованным логам и на каждой итерации будем учитывать название задачи.
from collections import defaultdict
# Подсчет задач
task_counts = defaultdict(int)
for log in attempt_logs:
task_counts[log["params"]["task"]] += 1
Хранить всё будем в defaultdict(int) — это словарь, в котором при первом обращении к новому ключу он автоматически получает значение 0. Этот «улучшенный словарь» идеально подходит для роли счётчика
Таким образом, в task_counts лежит словарь с количеством решений по каждой задаче.
{
"Snake": 3,
"Queens": 2,
"Parenthesis": 2,
"TwoSum": 1
}
Подробнее про коллекцию defaultdict() и ещё несколько заготовленных коллекций из модуля collections можете прочитать в документации.
Для поиска самой популярной задачи воспользуемся функцией max().
# Самая популярная задача
most_popular_task = max(task_counts.items(), key=lambda item: item[1])
Функция max(iterable, key=...) работает следующим образом: на вход получает итерируемый объект и функцию key, по которой элементы будут сравниваться друг с другом.
То есть — передаём словарь task_counts и просим провести сравнение по второму параметру (по количеству).
Шаг № 3: вывести всё собранное в нужном формате
Обратите внимание: чтобы не плодить дополнительные переменные, базовые операции в виде подсчёта количества пользователей или сортировки списка можно сразу обозначать в выводе.
print("Analytics Results:")
print(f"Unique users who solved tasks: {len(unique_users)}")
print(f"Unique users list: {', '.join(sorted(unique_users))}")
print(f"Task popularity: {dict(sorted(task_counts.items()))}")
print(
f"Most popular task: '{most_popular_task[0]}' with {most_popular_task[1]} attempts"
)
Про тестирование
В условии указано, что граничные случаи (нет строк с ручкой attempt_handler и есть несколько задач с максимальным количеством решений) пропускаем.
В таком случае нам нужно проверить, что всё правильно считается и сортируется.
Для этого собираем тесты, в которых:
- Имена пользователей в логах идут не в лексикографическом порядке (проверить работу сортировки).
- Названия задач идут не в лексикографическом порядке (проверить работу сортировки).
- Есть пользователи, которые решили одну задачу, и есть пользователи, которые решили несколько задач (проверить работу отбора уникальных элементов).
Например, можно проверить корректность следующими логами:
{"handler": "attempt_handler", "params": {"name": "Max", "task": "Snake"}, "response": [200, "Good try, Max! Your attempt on task 'Snake' is accepted!"]}
{"handler": "user_handler", "params": {"name": "Ivan"}, "response": [200, "Hello, Ivan!"]}
{"handler": "attempt_handler", "params": {"name": "Ivan", "task": "Queens"}, "response": [200, "Good try, Ivan! Your attempt on task 'Queens' is accepted!"]}
{"handler": "attempt_handler", "params": {"name": "Max", "task": "TwoSum"}, "response": [200, "Good try, Max! Your attempt on task 'TwoSum' is accepted!"]}
{"handler": "attempt_handler", "params": {"name": "Alex", "task": "Queens"}, "response": [200, "Good try, Alex! Your attempt on task 'Queens' is accepted!"]}
{"handler": "user_handler", "params": {"name": "Mari"}, "response": [403, "Forbidden"]}
{"handler": "attempt_handler", "params": {"name": "Mari", "task": "Parenthesis"}, "response": [200, "Good try, Mari! Your attempt on task 'Parenthesis' is accepted!"]}
{"handler": "attempt_handler", "params": {"name": "Max", "task": "Parenthesis"}, "response": [200, "Good try, Max! Your attempt on task 'Parenthesis' is accepted!"]}
{"handler": "attempt_handler", "params": {"name": "Alex", "task": "Snake"}, "response": [200, "Good try, Alex! Your attempt on task 'Snake' is accepted!"]}
{"handler": "attempt_handler", "params": {"name": "Ivan", "task": "Snake"}, "response": [200, "Good try, Ivan! Your attempt on task 'Snake' is accepted!"]}
{"handler": "not_found_handler", "params": {"name": "Mari"}, "response": [404, "Not Found"]}
На выходе получаем:
Analytics Results:
Unique users who solved tasks: 4
Unique users list: Alex, Ivan, Mari, Max
Task popularity: {'Parenthesis': 2, 'Queens': 2, 'Snake': 3, 'TwoSum': 1}
Most popular task: 'Snake' with 3 attempts
Что в итоге
И вот итоги после третьей задачи:
- Познакомились со способами оптимизации с помощью лямбда-функции,
defaultdict()и промежуточных расчётов прямо в выводе. - Научились считывать строчный JSON с помощью
loads().
Итог
Если собрать воедино решения всех трёх задач, вы получите полноценный небольшой сервис, который:
- Принимает запросы и запускает их обработку. При этом может принимать запросы вечно, не прекращая свою работу.
- Логирует действия пользователей при работе с сервисом и проверяет наличие авторизации пользователя.
- Проводит небольшую аналитику логов, чтобы понять, как сервисом пользуются.
Это конструкция, которая туда-сюда переносит по системе запросы, следит за правильностью выполнения и изучает поведение пользователей.
Примечание
В большинстве приложений работа бэкенд-части похожа как раз на ту, что вы проделали.
Надеемся, что у вас получилось на кончиках пальцев почувствовать это прямо в разработке. Ещё раз хотим вас похвалить — это большой шаг!

Алексей Бледнов
Разработчик в Яндекс Образовании
Хочу подвести итог и поделиться несколькими пожеланиями на будущее.
C Python вы можете очень быстро проверять свои гипотезы. Python — язык, на котором можно быстро создавать рабочие бэкенд-приложения. Благодаря простому синтаксису, удобной работе с объектами и богатой экосистеме библиотек многие задачи решаются быстрее, чем в большинстве других языков. Надеюсь, разбирая различные техники и приёмы оптимизации, вы нашли новые способы писать код лаконично, выразительно и эффективно — без потери результата.
В Python есть много готовых инструментов. Сейчас вы изучаете основы, чтобы понимать, что происходит под капотом. В дальнейшем многие задачи вы сможете решать с помощью уже готовых решений. Например, можно не реализовать ASGI/WSGI-сервер, который вы создавали вручную, а воспользоваться веб-фреймворком FastAPI: он там встроен как часть стандартной работы приложения.
Изучите дополнительные материалы, если вам понравились конкретные темы или задачи. Делюсь парой статей на Хабре, посвящённых генераторам (вы затронули небольшую часть) и ASGI/WSGI-серверам.
Также давайте подведём итоги: что изучили и что можно взять с собой дальше.
В рамках этих задач вы освоили:
- Создание генераторной функции. Которая не только отдаёт, но и принимает данные за счёт метода
.send(). - Функциональную работу декораторов. Разработали цепочку декораторов, научились доставать метаданные функции, а также передавать их через цепочку с помощью декоратора для декораторов
@wraps. - Применение
лямбда-функцийдля базовых операций работы над коллекциями. Использовали их при построчном считывании информации, фильтрации, подсчёте определённых параметров. - Использование функции
defaultdict(int)для упрощения кода в задачах с подсчётами. - Считывание строчного формата JSON с помощью функции
dumps().
Напоследок хотим посоветовать одну вещь: чаще задумывайтесь, как сделать свой код более лаконичным и оптимальным.
Если есть возможность — попросите у более опытных разработчиков ревью кода, чтобы они подсказали, как можно сделать по-другому. Это поможет вам перенять опыт, который копился годами.
На этом освоение раздела завершено. Не устаём повторять: вы очень большой молодец! Продолжайте в том же духе, впереди нас ждёт важная и интересная тема: научимся работать с классами и методами.
Увидимся в следующем разделе!