ch
Feedback
Python для начинающих

Python для начинающих

前往频道在 Telegram

Python для начинающих

显示更多
1 240
订阅者
无数据24 小时
+17
+130
帖子存档
Работа с очередями с использованием модуля queue
Работа с очередями с использованием модуля queue

Как использовать zip и unpacking в Python: собираем пазл из данных zip — это маленькая функция, которая решает кучу рутинных задач: объединяет списки, разворачивает таблицы, помогает красиво перебирать данные. А в паре с распаковкой (unpacking, оператор *) она становится особенно мощной. --- ## Базовое использование zip zip “склеивает” несколько итерируемых объектов по позициям:
names = ["Alice", "Bob", "Charlie"]
scores = [95, 82, 78]

for name, score in zip(names, scores):
    print(name, "=>", score)
Результат:
Alice => 95
Bob => 82
Charlie => 78
Важно: zip обрезает результат по самому короткому списку. --- ## Создание словаря из двух списков Классический приём:
keys = ["host", "port", "debug"]
values = ["localhost", 8000, True]

config = dict(zip(keys, values))
print(config)
Результат:
{'host': 'localhost', 'port': 8000, 'debug': True}
--- ## Unpacking: оператор * в действии Оператор * “распаковывает” итерируемые объекты. В связке с zip это особенно полезно. ### Транспонирование “таблицы” Представим матрицу как список строк:
matrix = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9],
]

transposed = list(zip(*matrix))
print(transposed)
Результат:
[(1, 4, 7), (2, 5, 8), (3, 6, 9)]
Мы как бы “повернули” таблицу: строки стали столбцами. Это один из самых элегантных трюков с zip. --- ## Распаковка результата zip zip возвращает итератор, его тоже можно распаковать:
pairs = list(zip(names, scores))
print(pairs)          # [('Alice', 95), ('Bob', 82), ('Charlie', 78)]

unzipped_names, unzipped_scores = zip(*pairs)
print(unzipped_names)   # ('Alice', 'Bob', 'Charlie')
print(unzipped_scores)  # (95, 82, 78)
Так можно “разобрать” список пар обратно на два отдельных набора данных. --- ## С enumerate и zip: удобный перебор Иногда нужно и индекс, и значения из нескольких коллекций:
for idx, (name, score) in enumerate(zip(names, scores), start=1):
    print(f"{idx}. {name}: {score}")
--- zip и unpacking — это инструменты, которые делают код короче, читабельнее и “питоничнее”. Освоив их, вы начнёте мыслить данными как блоками, которые легко собирать и разбирать, словно конструктор.

Как использовать zip и unpacking в Python
Как использовать zip и unpacking в Python

### Создание простого REST API с Falcon: минимализм в деле Если Flask — это швейцарский нож, то Falcon — это скальпель. Минимум магии, максимум скорости и простоты. Он отлично подходит, когда нужно сделать легкий REST API без нагромождений. --- ## Установка Falcon не входит в стандартную библиотеку, ставим:
pip install falcon
--- ## Базовый пример: «Hello, API» Создадим минимальный REST API, который отвечает JSON-ом.
import json
import falcon


class HelloResource:
    def on_get(self, req, resp):
        resp.status = falcon.HTTP_200
        resp.media = {"message": "Hello, Falcon API!"}


app = falcon.App()
app.add_route("/hello", HelloResource())
Сохрани файл как app.py, а запускать удобнее через uWSGI, gunicorn или waitress. Для разработки можно использовать falcon.App вместе с wsgiref:
from wsgiref.simple_server import make_server

if __name__ == "__main__":
    with make_server("127.0.0.1", 8000, app) as httpd:
        httpd.serve_forever()
Переходим по http://127.0.0.1:8000/hello и получаем JSON-ответ. --- ## Обработка методов: GET и POST Добавим ресурс для работы со списком задач. Хранить будем в памяти, как простой пример.
import falcon


tasks = []


class TasksResource:
    def on_get(self, req, resp):
        resp.media = {"tasks": tasks}

    def on_post(self, req, resp):
        data = req.media  # Falcon сам распарсит JSON
        title = data.get("title")
        if not title:
            resp.status = falcon.HTTP_400
            resp.media = {"error": "title is required"}
            return

        task = {"id": len(tasks) + 1, "title": title}
        tasks.append(task)

        resp.status = falcon.HTTP_201
        resp.media = task


app = falcon.App()
app.add_route("/tasks", TasksResource())
Теперь: - GET /tasks возвращает список задач, - POST /tasks с JSON {"title": "Learn Falcon"} добавляет новую. --- ## Обработка одного ресурса по ID Добавим получение одной задачи:
class TaskItemResource:
    def on_get(self, req, resp, task_id):
        task_id = int(task_id)
        for task in tasks:
            if task["id"] == task_id:
                resp.media = task
                return
        resp.status = falcon.HTTP_404
        resp.media = {"error": "task not found"}


app = falcon.App()
app.add_route("/tasks", TasksResource())
app.add_route("/tasks/{task_id}", TaskItemResource())
--- Falcon хорош тем, что не навязывает структуру проекта: вы просто пишете классы-ресурсы и явно связываете их с маршрутами. Минимум магии — максимум контроля. Для небольшого, понятного REST API этого часто более чем достаточно.

Создание простого REST API с Falcon
Создание простого REST API с Falcon

Разбор стандартного модуля collections: deque, defaultdict и Counter Модуль collections — это такой «апгрейд» стандартных структур данных в Python. Те же списки и словари, но с турбонаддувом. Разберём три самых полезных инструмента: deque, defaultdict и Counter. --- ## deque: очередь без мучений Обычный список в Python плохо подходит для частых операций в начале: pop(0) и insert(0, x) работают медленно. deque (double-ended queue) решает это.
from collections import deque

queue = deque()

queue.append('task_1')
queue.append('task_2')
queue.appendleft('urgent_task')

print(queue)           # deque(['urgent_task', 'task_1', 'task_2'])

task = queue.popleft() # достаем слева O(1)
print(task)            # urgent_task
deque идеально подходит для очередей, буферов, реализации алгоритмов обхода графов (BFS) и «скользящих окон». --- ## defaultdict: словарь, который не ругается Обычный словарь бросает KeyError, если ключа нет. defaultdict вместо этого создаёт значение «по умолчанию».
from collections import defaultdict

word_count = defaultdict(int)

text = "python is fun and python is powerful".split()
for word in text:
    word_count[word] += 1

print(word_count['python'])  # 2
print(word_count['missing']) # 0, а не KeyError
Другой частый сценарий — группировка:
from collections import defaultdict

groups = defaultdict(list)

pairs = [('a', 1), ('b', 2), ('a', 3)]
for key, value in pairs:
    groups[key].append(value)

print(groups)  # {'a': [1, 3], 'b': [2]}
--- ## Counter: просто посчитай это Counter — специализированный словарь для подсчёта объектов.
from collections import Counter

nums = [1, 2, 2, 3, 3, 3]
c = Counter(nums)

print(c)               # Counter({3: 3, 2: 2, 1: 1})
print(c[2])            # 2
print(c.most_common(1))# [(3, 3)]
Отлично подходит для анализа текста:
from collections import Counter

text = "banana bandana"
c = Counter(text.replace(" ", ""))

print(c)               # Counter({'a': 6, 'n': 3, 'b': 1, 'd': 1})
print(c.most_common(2))# самые частые буквы
--- Итог: - deque — быстрая очередь с двух концов. - defaultdict — словарь с автоматическими значениями по умолчанию. - Counter — быстрый подсчёт повторяющихся элементов. Все три входят в стандартную библиотеку, не требуют установки и моментально делают код чище и эффективнее. Попробуйте в своих маленьких скриптах — разницу почувствуете сразу.

Разбор стандартного модуля collections: deque, defaultdict и Counter
Разбор стандартного модуля collections: deque, defaultdict и Counter

Модуль enum: читаемые и безопасные константы вместо “магических чисел” Когда в коде появляются числа вроде 1, 2, 3 с неочевидным смыслом, проект начинает расползаться на баги. Сегодня — про модуль enum, который превращает такие “магические числа” в понятные и безопасные константы. --- ### Проблема “магических значений” Представим статус заказа:
status = 2  # что это? "отправлен"? "отменен"?
Через пару недель вы уже не помните, что означает 2. А при рефакторинге можно легко перепутать значения. --- ### Enum: строгие и говорящие значения Модуль enum позволяет задать набор именованных констант:
from enum import Enum

class OrderStatus(Enum):
    NEW = 1
    IN_PROGRESS = 2
    DONE = 3

status = OrderStatus.NEW

if status == OrderStatus.DONE:
    print("Order completed")
Плюсы: - читаемость: OrderStatus.DONE понятнее, чем 3; - защита от опечаток: OrderStatus.DNOE вызовет ошибку, а не тихо примет неверное число; - автодополнение в IDE. --- ### Сравнение и хранение Enum-члены уникальны:
OrderStatus.NEW == OrderStatus.NEW      # True
OrderStatus.NEW == OrderStatus.DONE     # False

print(status.name)   # NEW
print(status.value)  # 1
Для хранения в БД удобно использовать value, а в коде работать с Enum:
stored_value = 2
status = OrderStatus(stored_value)  # восстановили из числа
Если значение некорректно, будет ValueError, а не тихая ошибка. --- ### Автоматические значения с auto() Когда вам не важны конкретные числа:
from enum import Enum, auto

class UserRole(Enum):
    ADMIN = auto()
    MANAGER = auto()
    USER = auto()
Значения будут 1, 2, 3 — но вам все равно, вы работаете только с именами. --- ### Enum как замена строковым константам Строки тоже легко перепутать:
role = "admni"  # опечатка, и код падает в неожиданных местах
Вместо этого:
class Role(Enum):
    ADMIN = "admin"
    USER = "user"

def has_access(role: Role) -> bool:
    return role is Role.ADMIN
Теперь неправильное значение просто так не проскочит. --- ### Немного безопасности и порядка Enum — это: - централизованный список допустимых значений; - самодокументирующийся код; - меньше скрытых багов из-за опечаток и “магических чисел”. Если вы пишете игру с типами юнитов, веб‑приложение со статусами заказов или API с ролями, enum — простой способ сделать код понятнее и надежнее уже сегодня.

Модуль enum: создание читаемых и безопасных констант
Модуль enum: создание читаемых и безопасных констант

### Модуль time: замер и имитация задержек в коде Когда ваш код “летает”, это приятно. Но иногда нужно не только скорость, но и… паузы. Звучит странно, но без задержек и точного измерения времени не обойтись: от тестирования до симуляции медленных внешних сервисов. Разберём модуль time: как измерять выполнение кода и как “тормозить” программу по собственному желанию. --- ## Базовая задержка: time.sleep Функция sleep просто приостанавливает выполнение программы на указанное число секунд (можно дробное).
import time

print("Start")
time.sleep(2.5)  # пауза 2.5 секунды
print("End")
Полезно для: - имитации долгих операций (запросов к БД, API), - создания задержек между запросами (во избежание банов), - тестирования поведения интерфейсов. --- ## Измеряем время выполнения кода Наивный способ — time.time(). Он возвращает текущее время в секундах с начала эпохи (обычно 01.01.1970). Разница между двумя вызовами — затраченное время.
import time

start = time.time()
result = sum(range(10_000_000))
end = time.time()

print(f"Result: {result}")
print(f"Elapsed: {end - start:.4f} seconds")
Но у этого подхода есть минус: точность зависит от системных часов, которые могут “прыгать”. --- ## Более точный замер: time.perf_counter Для профилирования (измерения скорости) лучше использовать perf_counter(). Это монотонный счётчик: не зависит от системного времени и даёт максимальную доступную точность.
import time

start = time.perf_counter()
data = [x ** 2 for x in range(1_000_000)]
end = time.perf_counter()

print(f"Elapsed: {end - start:.6f} seconds")
Используйте perf_counter везде, где важна точность измерений, особенно в тестах производительности. --- ## Имитируем “медленный” внешний сервис Представьте, что вы пишете функцию, которая обращается к API, но настоящего API ещё нет. Можно сделать фейковую задержку:
import time
import random

def fake_api_call():
    delay = random.uniform(0.5, 1.5)
    time.sleep(delay)
    return {"status": "ok", "delay": delay}

start = time.perf_counter()
response = fake_api_call()
end = time.perf_counter()

print(response)
print(f"Real elapsed: {end - start:.3f} seconds")
Так удобно тестировать: - обработку долгих ответов, - таймауты, - индикаторы загрузки. --- ## Мини-профайлер на коленке Можно написать простый декоратор для измерения времени выполнения любой функции.
import time
from functools import wraps

def timeit(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        end = time.perf_counter()
        print(f"{func.__name__} took {end - start:.5f} seconds")
        return result
    return wrapper

@timeit
def slow_function():
    time.sleep(1)
    return "done"

slow_function()
Так вы быстро находите “узкие места” без тяжёлых инструментов. --- Модуль time кажется простым, но это мощный инструмент: с ним вы можете и измерять производительность, и контролировать темп выполнения программы. А это уже шаг от “просто работает” к “работает предсказуемо и управляемо”.

Модуль time: замер и имитация задержек в коде
Модуль time: замер и имитация задержек в коде

Создание и использование генераторов: yield в действии Когда цикл for в Python перебирает список, он хранит весь список в памяти. Удобно, но не всегда эффективно. Генераторы позволяют «лениво» выдавать значения по одному — экономя память и иногда ускоряя код. ### Что такое генератор? Генератор — это объект, который «помнит», где он остановился, и при следующем запросе продолжает с этого места. Создать его можно двумя способами: 1. Функция с ключевым словом yield 2. Генераторное выражение: (x * 2 for x in range(10)) ### Простейший пример с yield
def countdown(n):
    while n > 0:
        yield n
        n -= 1

for number in countdown(5):
    print(number)
Функция countdown не возвращает список. Она каждый раз «выдает» следующее значение через yield. Память тратится только на текущее состояние, а не на весь набор чисел. ### Генератор против списка
def squares_list(n):
    result = []
    for i in range(n):
        result.append(i * i)
    return result

def squares_gen(n):
    for i in range(n):
        yield i * i

print(sum(squares_list(10_000_000)))
print(sum(squares_gen(10_000_000)))
Оба варианта дадут одинаковый результат, но squares_list создаст огромный список в памяти, а squares_gen будет считать по одному значению. Разница станет особенно заметна при больших n или при обработке потоков данных (логов, файлов, сетевых запросов). ### Бесконечные последовательности Генераторы легко порождают бесконечные последовательности — такое невозможно в виде обычного списка:
def infinite_counter(start=0):
    current = start
    while True:
        yield current
        current += 1

counter = infinite_counter()
for _ in range(5):
    print(next(counter))
Функция никогда не завершится сама, но генератор выдает значения, пока вы их запрашиваете. ### Генераторные выражения Краткая запись:
evens = (x for x in range(100) if x % 2 == 0)
print(sum(evens))
Здесь evens — генератор, а не список. Память используется минимально. --- yield — это не просто «альтернатива return». Это способ думать о данных как о потоке значений: не «хранить всё», а «выдавать по запросу». Для обработки больших объемов данных и написания эффективного кода в Python это один из ключевых инструментов.

Создание и использование генераторов: yield в действии
Создание и использование генераторов: yield в действии

Работа с модулем calendar: удобное отображение дат и событий Если вы когда‑нибудь пытались “вручную” вычислить день недели или красиво вывести календарь в консоль, то модуль calendar создан именно для того, чтобы вы перестали страдать. ### Быстрый старт: печатаем календарь месяца
import calendar

year = 2025
month = 3

cal = calendar.month(year, month)
print(cal)
Результат — готовый текстовый календарь марта 2025. Для целого года есть аналог:
print(calendar.calendar(2025))
Уже можно делать простенькие консольные планировщики. ### Как устроен месяц “внутри” Чтобы работать с датами программно, удобен метод monthcalendar:
import calendar

year = 2025
month = 3

c = calendar.Calendar(firstweekday=0)  # 0 – понедельник
month_matrix = c.monthdayscalendar(year, month)

for week in month_matrix:
    print(week)
Каждая неделя — список из 7 чисел. Нули означают “нет дня” (ячейка принадлежит соседнему месяцу). Это удобно для логики, например, подсветки выходных или праздников. ### Добавляем события в календарь Сделаем простую текстовую “раскраску” дат с событиями:
import calendar

events = {
    (2025, 3, 8): "Holiday",
    (2025, 3, 15): "Deadline",
}

year, month = 2025, 3
c = calendar.Calendar(firstweekday=0)

for week in c.monthdayscalendar(year, month):
    line = []
    for day in week:
        if day == 0:
            line.append("   ")
            continue

        key = (year, month, day)
        if key in events:
            # отмечаем события звездочкой
            line.append(f"{day:2d}*")
        else:
            line.append(f"{day:2d} ")
    print(" ".join(line))
Вы увидите сетку чисел, а дни с событиями будут помечены *. Такой вывод легко адаптировать под любой интерфейс: консоль, web, GUI. ### Дни недели и “рабочий” календарь Модуль calendar знает всё о буднях и выходных:
import calendar

year, month = 2025, 3

workdays = []
for week in calendar.monthcalendar(year, month):
    for i, day in enumerate(week):
        if day == 0:
            continue
        # 0–4: понедельник–пятница
        if i < 5:
            workdays.append(day)

print("Workdays:", workdays)
Так можно быстро считать количество рабочих дней, искать все пятницы или строить графики дежурств. ### Дополнительно - calendar.isleap(year) — проверка високосного года. - calendar.weekday(year, month, day) — номер дня недели (0 — понедельник). - setfirstweekday() — глобально изменить первый день недели. calendar — отличный инструмент, чтобы перестать думать о том, на какой день недели падает 1 апреля, и сосредоточиться на логике приложения.

Работа с модулем calendar: удобное отображение дат и событий
Работа с модулем calendar: удобное отображение дат и событий

Pathlib: современный способ подружиться с путями и файлами в Python Модуль pathlib — это попытка Python сказать: «Хватит мучиться со строками путей и os.path». Он даёт удобный объект Path, который понимает операционные системы, красиво соединяет части пути и умеет работать с файлами почти как с объектами. --- ### Создание путей
from pathlib import Path

# Текущая директория
current_dir = Path.cwd()

# Домашняя директория
home_dir = Path.home()

# Относительный путь
project_dir = Path("projects") / "my_app"

# Путь с "магическим" слэшем
config_file = project_dir / "config.yaml"
print(config_file)  # projects/my_app/config.yaml (или с \ на Windows)
Оператор / — это не деление, а аккуратное склеивание частей пути под вашу ОС. --- ### Проверка существования и свойств
from pathlib import Path

path = Path("data/example.txt")

print(path.exists())      # файл или папка существует?
print(path.is_file())     # это файл?
print(path.is_dir())      # это директория?
print(path.parent)        # родительская директория
print(path.name)          # имя файла с расширением
print(path.stem)          # имя без расширения
print(path.suffix)        # расширение (.txt)
--- ### Чтение и запись файлов Для текстовых файлов pathlib даёт очень лаконичный синтаксис:
from pathlib import Path

text_file = Path("notes/todo.txt")

# Запись
text_file.parent.mkdir(parents=True, exist_ok=True)
text_file.write_text("Learn pathlib\nUse it everywhere!", encoding="utf-8")

# Чтение
content = text_file.read_text(encoding="utf-8")
print(content)
Для бинарных данных есть методы write_bytes() и read_bytes(). --- ### Обход директорий и поиск файлов
from pathlib import Path

logs_dir = Path("logs")

# Все .log-файлы в директории (без поддиректорий)
for log_file in logs_dir.glob("*.log"):
    print(log_file)

# Рекурсивно найти все .py-файлы
for py_file in Path(".").rglob("*.py"):
    print(py_file)
glob() и rglob() понимают шаблоны (*, ?), так что можно быстро фильтровать нужные файлы. --- ### Безопасные операции с файлами
from pathlib import Path

src = Path("data/raw/data.csv")
dst = Path("data/processed/data.csv")

# Создаём директорию, если её нет
dst.parent.mkdir(parents=True, exist_ok=True)

# Переименование или перенос
if src.exists():
    src.rename(dst)
--- pathlib заменяет кучу разрозненных функций из os, os.path и shutil единым, логичным интерфейсом. Стоит привыкнуть к Path — и строковые пути начнут казаться пережитком прошлого.

Как использовать pathlib для работы с путями и файлами
Как использовать pathlib для работы с путями и файлами

Изучение itertools: комбинации, перестановки и бесконечные итераторы Модуль itertools — это чемоданчик с инструментами для работы с последовательностями. Он не шумит, не требует сложной настройки, но иногда одним-двумя его вызовами можно заменить десяток строк кода с циклами. Разберём три ключевые группы: комбинации, перестановки и бесконечные итераторы. --- ## Комбинации и перестановки ### combinations combinations(iterable, r) перебирает все уникальные наборы по r элементов без повторов и без учёта порядка.
from itertools import combinations

items = ['a', 'b', 'c', 'd']

for combo in combinations(items, 2):
    print(combo)
Результат: ('a', 'b'), ('a', 'c'), ... — но не будет ('b', 'a'), потому что порядок не важен. Полезно для перебора возможных пар/наборов параметров, команд, вариантов выбора. ### permutations permutations(iterable, r=None) — все возможные варианты с учётом порядка.
from itertools import permutations

letters = ['A', 'B', 'C']

for p in permutations(letters, 2):
    print(p)
Здесь и ('A', 'B'), и ('B', 'A') — уже разные результаты. Удобно, когда порядок критичен: генерация пароля, маршрута, очередности задач. ### product product — декартово произведение. Можно представить как вложенные циклы.
from itertools import product

colors = ['red', 'green']
sizes = ['S', 'M', 'L']

for item in product(colors, sizes):
    print(item)
Это быстрый способ перебрать все сочетания параметров при тестировании, конфигурациях и т.п. --- ## Бесконечные итераторы Иногда удобно иметь “бесконечный источник” данных, который вы сами ограничиваете. ### count count(start=0, step=1) — бесконечный счётчик.
from itertools import count, islice

for n in islice(count(10, 2), 5):
    print(n)
islice здесь обрезает бесконечную последовательность до 5 элементов: 10, 12, 14, 16, 18. ### cycle cycle(iterable) — крутит последовательность по кругу.
from itertools import cycle, islice

states = ['loading', 'processing', 'done']

for s in islice(cycle(states), 7):
    print(s)
Получаем повторяющийся цикл состояний, полезно для простых симуляций или циклических индикаторов. ### repeat repeat(object, times=None) возвращает один и тот же объект много раз.
from itertools import repeat

for msg in repeat("ping", 3):
    print(msg)
В связке с map удобно передавать одинаковый аргумент множеству вызовов. --- itertools хорош тем, что он “мысленно сокращает” код: когда вы видите combinations или product, сразу понятно, что именно происходит, без чтения вложенных циклов. Попробуйте заменить пару своих циклов этими функциями — и код станет и короче, и чище.

Изучение itertools: комбинации, перестановки и бесконечные итераторы
Изучение itertools: комбинации, перестановки и бесконечные итераторы

Создание текстовых отчетов и логов с записью в файл день за днем Рано или поздно любой скрипт вырастает до состояния: «Хочу понимать, что он делал вчера ночью». Тут на сцену выходят отчеты и логи. Давайте разберем пару рабочих приемов: ежедневные файлы логов и простые текстовые отчеты. --- ## Ежедневный лог-файл Частая практика — один файл лога на день: log_2025-01-21.txt, log_2025-01-22.txt и т.д. Сделаем простую функцию-логгер:
from datetime import datetime
from pathlib import Path

LOG_DIR = Path("logs")
LOG_DIR.mkdir(exist_ok=True)

def get_log_file_path() -> Path:
    today_str = datetime.now().strftime("%Y-%m-%d")
    return LOG_DIR / f"log_{today_str}.txt"

def log_message(level: str, message: str) -> None:
    log_file = get_log_file_path()
    time_str = datetime.now().strftime("%H:%M:%S")
    line = f"[{time_str}] [{level.upper()}] {message}\n"
    with log_file.open("a", encoding="utf-8") as f:
        f.write(line)

# Пример использования
log_message("info", "Script started")
log_message("warning", "Low disk space")
log_message("error", "Failed to connect to server")
Что здесь важно: - Path из pathlib удобнее обычных строк для путей. - Режим "a" — дозапись в конец файла, не стирая старые логи. - Имя файла зависит от даты — архив логов сам формируется по дням. --- ## Простой текстовый отчет по результатам работы Представим скрипт, который что-то обрабатывает и в конце дня создает итоговый отчет: сколько задач прошло, сколько упало, какой процент успеха.
from datetime import datetime
from pathlib import Path

REPORT_DIR = Path("reports")
REPORT_DIR.mkdir(exist_ok=True)

def save_daily_report(total: int, success: int, failed: int) -> Path:
    today_str = datetime.now().strftime("%Y-%m-%d")
    report_path = REPORT_DIR / f"report_{today_str}.txt"
    success_rate = (success / total * 100) if total else 0

    lines = [
        f"Report date: {today_str}",
        f"Total tasks: {total}",
        f"Successful: {success}",
        f"Failed: {failed}",
        f"Success rate: {success_rate:.2f}%",
    ]

    with report_path.open("w", encoding="utf-8") as f:
        f.write("\n".join(lines))

    return report_path

# Пример
report_file = save_daily_report(total=120, success=110, failed=10)
log_message("info", f"Daily report saved to {report_file}")
Фишки: - Отчет — обычный текстовый файл, его легко открыть в любом редакторе или отправить по почте. - Формат предельно простой, но уже дает картину дня. - Логгер и отчет связаны: лог фиксирует факт создания отчета. --- ## Идея для развития Дальше можно: - добавлять в отчет «ТОП-5 ошибок дня»; - архивировать старые логи в zip; - использовать модуль logging, чтобы прокачать формат и уровни логов. Но фундамент один: аккуратная, ежедневная запись в файлы с понятными именами и структурой. Это уже делает ваш код ближе к «боевому» уровню.