Рефакторинг Python без паники: 10 промтов, которые превращают легаси в поддерживаемый код и не ломают прод
Легаси-код на Python редко выглядит как катастрофа в первый день. Проблема накапливается: модуль на 2000 строк, где бизнес-логика перемешана с SQL, утечки памяти в долгоживущих воркерах, эндпоинт FastAPI, который отвечает 3 секунды вместо 200 мс. По данным Google Engineering Practices и многолетних наблюдений в индустрии, стоимость поддержки кода превышает стоимость его написания в разы — и именно рефакторинг становится узким местом.
ChatGPT и другие LLM здесь работают как ускоритель: они не заменяют инженера, но снимают рутину — разбор функций, поиск N+1, генерацию тестов-«страховки» перед изменениями. Главный риск — сломать прод. Поэтому в подборке ниже каждый промт строится вокруг принципа «сначала контракт и тесты, потом изменения». Промты проверены на реальных сценариях: медленный FastAPI, раздутый модуль, дублирование логики, утечки памяти.
1. Карта легаси-модуля перед любыми правками
Задача: понять, что вообще делает файл на 2000 строк, прежде чем что-то менять.
Промт:
Ты — senior Python-инженер. Проанализируй модуль ниже и составь карту:
1) список публичных функций/классов с одной строкой назначения;
2) скрытые зависимости (глобальные переменные, синглтоны, побочные эффекты);
3) места, где смешаны слои (I/O, бизнес-логика, форматирование);
4) функции длиннее 50 строк и с цикломатической сложностью выше 10;
5) предложи порядок рефакторинга от самого безопасного к самому рискованному.
Не пиши код, только анализ в виде таблицы.
```python
<вставь код модуля>
**Пример:** модуль `orders.py` на 1800 строк. LLM выделяет 12 функций, 4 из которых пишут в БД внутри цикла, и предлагает сначала вынести чистые хелперы, потом трогать I/O.
**Ожидаемый результат:** таблица с рисками и планом. Это ваш чек-лист и защита от «рефакторинга вслепую».
## 2. Тесты-страховка до изменений
**Задача:** зафиксировать текущее поведение, чтобы рефакторинг не сломал прод.
**Промт:**
Напиши pytest-тесты, которые фиксируют ТЕКУЩЕЕ поведение функции, включая странности и баги. Используй parametrize, mock для внешних вызовов (requests, БД) через unittest.mock. Не исправляй логику — только зафиксируй.
<вставь функцию>
**Пример:** функция `calculate_discount`, которая при отрицательной сумме возвращает `None`. Тест фиксирует это как ожидаемое поведение — потом вы осознанно решите, баг это или фича.
**Совет:** запустите тесты на старом коде — они должны пройти. Это ваша страховочная сетка.
## 3. Разбиение «функции-монстра»
**Задача:** функция на 300 строк с вложенными `if`.
**Промт:**
Разбей функцию на маленькие с сохранением поведения. Требования:
- каждая новая функция ≤ 20 строк и делает одну вещь;
- без изменения публичного контракта;
- выдели чистые функции отдельно от I/O;
- покажи diff-подобно: было/стало.
<вставь функцию>
**Пример:** `process_order` разбивается на `validate_order`, `apply_discounts`, `persist_order`, `send_notification`. Тесты из промта №2 проходят без изменений — значит, поведение сохранено.
## 4. Поиск N+1 в SQLAlchemy
**Задача:** эндпоинт делает сотни запросов незаметно.
**Промт:**
Проанализируй код SQLAlchemy ниже. Найди N+1-запросы: обращения к relationship внутри циклов, ленивую загрузку там, где нужна жадная. Предложи фикс через selectinload/joinedload. Покажи, как проверить количество запросов через event.listen на engine (before_cursor_execute).
<вставь код>
**Пример:** `for user in users: print(user.orders)` генерирует N+1. Фикс: `select(User).options(selectinload(User.orders))`.
**Проверка:**
```python
from sqlalchemy import event
@event.listens_for(engine, "before_cursor_execute")
def count(conn, cursor, statement, params, context, executemany):
print(statement)
5. Профилирование вместо догадок
Задача: найти реальное узкое место, а не то, что «кажется медленным».
Промт:
Помоги найти узкое место. Предложи план профилирования:
1) cProfile для CPU-bound;
2) tracemalloc для памяти;
3) py-spy для прода без остановки процесса.
Напиши готовые команды и как читать вывод (cumtime vs tottime).
```python
<вставь подозрительный код>
**Пример:** `python -m cProfile -s cumtime app.py`. Если `tottime` высокий у функции — она сама тормозит; если высокий `cumtime` — тормозят её вызовы.
**Важно:** `py-spy top --pid <PID>` работает на проде без перезапуска — читайте документацию на github.com/benfred/py-spy.
## 6. Перевод синхронного кода на async
**Задача:** FastAPI-эндпоинт блокирует event loop синхронными вызовами.
**Промт:**
Переведи код на async/await. Требования:
- замени блокирующие requests/psycopg2 на httpx/asyncpg;
- отметь места, где нужен run_in_executor для CPU-bound;
- не делай await внутри list comprehension без asyncio.gather;
- покажи до/после и предупреди о типичных ловушках (блокировка loop, забытый await).
<вставь код>
**Пример:** `requests.get` → `httpx.AsyncClient().get`. Параллельные вызовы — через `asyncio.gather`.
## 7. Code review как второй пилот
**Задача:** быстрая проверка PR перед merge.
**Промт:**
Сделай code review как строгий тимлид. Проверь:
- обработку исключений (не глотает ли ошибки);
- утечки ресурсов (файлы, соединения — есть ли with/context manager);
- изменяемые дефолтные аргументы;
- SQL-инъекции и валидацию входа;
- гонки в многопоточном коде.
Формат: критично / желательно / nitpick. Без воды.
<вставь diff или файл>
## 8. Кеширование без боли
**Задача:** ускорить повторные дорогие вычисления.
**Промт:**
Предложи стратегию кеширования для функции. Варианты: functools.lru_cache, cachetools.TTLCache, Redis. Объясни выбор по критериям: размер данных, TTL, распределённость, инвалидация. Покажи код с корректной инвалидацией и предупреди о мемоизации нечистых функций.
<вставь функцию>
**Пример:** `lru_cache(maxsize=128)` для чистых вычислений; Redis с TTL для данных, общих между воркерами. Не кешируйте функции, зависящие от текущего времени или сессии.
## 9. Поиск утечек памяти
**Задача:** воркер растёт в памяти часами.
**Промт:**
Помоги найти утечку памяти. Проверь: циклические ссылки, растущие глобальные списки/словари, незакрытые генераторы, кеши без ограничения, слушатели событий без отписки. Предложи диагностику через tracemalloc и gc, покажи снимки до/после.
<вставь код воркера>
**Пример:**
```python
import tracemalloc
tracemalloc.start()
# ... работа ...
snapshot = tracemalloc.take_snapshot()
for stat in snapshot.statistics('lineno')[:10]:
print(stat)
10. Удаление дублирования логики
Задача: одна и та же валидация в 6 местах.
Промт:
Найди дублирование логики в файлах ниже. Предложи единую абстракцию: функцию, декоратор или базовый класс — что уместнее. Покажи рефакторинг и объясни, почему не переусложнил (избегай преждевременных абстракций).
```python
<вставь 2-3 файла>
```
Таблица: какой промт под какой сценарий
| Сценарий | Промт |
|---|---|
| Незнакомый легаси-модуль | №1 Карта модуля |
| Перед любыми правками | №2 Тесты-страховка |
| Функция на 300 строк | №3 Разбиение |
| Медленный эндпоинт с ORM | №4 N+1 |
| «Кажется, тормозит» | №5 Профилирование |
| Блокирующий FastAPI | №6 Async |
| Проверка PR | №7 Code review |
| Повторные вычисления | №8 Кеш |
| Рост памяти воркера | №9 Утечки |
| Копипаста в 6 местах | №10 Дублирование |
Как не сломать прод
Три правила, которые работают независимо от промтов. Первое: никогда не рефакторите без тестов — промт №2 идёт всегда первым. Второе: маленькие коммиты, каждый — отдельный смысловой шаг, чтобы git bisect находил проблему за минуты. Третье: фича-флаги для рискованных изменений — включили на 1% трафика, посмотрели метрики, откатили при аномалии.
Полезные источники: официальная документация Python по cProfile и tracemalloc (docs.python.org), руководство SQLAlchemy по загрузке связей (docs.sqlalchemy.org/en/20/orm/queryguide/relationships.html), репозиторий py-spy. Эти материалы — база, на которой промты дают точный результат, а не галлюцинации.
Промты — это не кнопка «сделать хорошо». Это ускоритель для инженера, который понимает, что делает. Начните с карты модуля и тестов, прогоните профилировщик, и только потом меняйте код. Так легаси превращается в поддерживаемый проект без ночных откатов.
Комментарии