В любой компании рано или поздно появляется легаси-код. Иногда это устаревшие скрипты, иногда большие модули, которые «пишет только один человек», а чаще — просто код, который давно никто не трогал, но все боятся менять. И пусть кажется, что если система работает, то и трогать ее не нужно, на практике все иначе: любая остановка развития или усложнение поддержки со временем превращается в дополнительные издержки и головную боль для бизнеса.
Рефакторинг помогает «оздоровить» такой код, сделать его чище и удобнее для развития. Но стандартный ручной рефакторинг — задача дорогая и трудоемкая. В последнее время на волне развития искусственного интеллекта появились инструменты, которые обещают упростить этот процесс. Мы решили провести эксперимент и проверить, способны ли нейросети действительно помочь с рефакторингом легаси-кода на Python, и если да, то в каких случаях.
Откуда берется Python-легаси и чем он опасен
Python за последние годы стал языком «по умолчанию». Его используют для прототипирования, автоматизации и разработки сложных сервисов. Среди причин популярности — низкий порог входа и огромное количество библиотек, благодаря которым запуск новых проектов идет быстро. Однако по мере развития продукта, смены требований и изменений в команде отдельные участки кода могут со временем устаревать или терять поддержку. Такие фрагменты принято называть легаси-кодом: они могут мешать внедрению новых функций, усложнять тестирование и сопровождение.
Даже в крупных успешных Python-проектах иногда встречаются подобные примеры. Рассмотрим несколько пунктов, которые помогут их распознать.
- Длинные функции и модули. Сегодня добавили «еще одну проверку», завтра — кусок логики, и вот уже в одной функции 100+ строк. Такие «монолиты» сложно тестировать и переиспользовать, их боятся трогать даже опытные коллеги.
- Ручной парсинг данных и конфигов. Казалось бы, проще прочитать пару строк из файла и разбить их через
.split('='), чем подключать отдельную библиотеку для работы с настройками. Но это быстро приводит к ошибкам, дублированию кода и трудностям при масштабировании. - Повторяющиеся фрагменты. Вспомнили полезную функцию — скопировали в другой файл, чуть изменили. Теперь однотипные баги разбросаны по всему проекту.
- Слабая обработка ошибок. Исключения ловятся «на глаз», сообщения об ошибках неинформативны, логи заполняются неструктурированными данными.
- «Магические» числа и строки в коде. «Порог ошибки — 80, давление — 30, напряжение — 5» — такие значения часто зашиваются прямо в функции. Потом никто не помнит, почему они выбраны, а изменить их без страха сложно.
Для инфраструктурных и серверных проектов эти проблемы еще острее: автоматизация деплоя, мониторинг и интеграция с внешними сервисами требуют, чтобы код был не только рабочим, но и поддерживаемым, читаемым и гибким к изменениям. Именно поэтому рефакторинг — не каприз разработчиков, а важная инвестиция в устойчивость и скорость проекта.
Наш подопытный. Код анализа данных с космического телескопа «Джеймс Уэбб»
Чтобы проверить возможности автоматического рефакторинга на практике, мы решили взять не абстрактный пример из учебника, а рабочее решение с открытым исходным кодом. Для эксперимента выбрали репозиторий spacetelescope/jwql — проект, разработанный для автоматизации мониторинга, сбора и анализа данных с телескопа «Джеймс Уэбб».
Почему этот проект?
- Это живой Python‑код, используемый в инфраструктуре реального научного инструмента.
- Проект существует давно, активно развивается и содержит в себе как современные решения, так и классические «боли» крупных Python‑баз: большие функции, ручной парсинг, нестандартизированную обработку ошибок.
- Лицензия BSD‑3 позволяет свободно использовать код для анализа и демонстрации.
Что мы ищем?
В этом репозитории мы специально отобрали фрагменты, которые чаще всего встречаются в легаси‑коде инфраструктурных сервисов.
- Функции с длинной логикой без деления на подзадачи.
- Обработка конфигов «на коленке» через открытие и разбор файла.
- Ручное форматирование строк для отчетов.
- Однотипные проверки и условия, разбросанные по проекту.
- Явные «магические» значения прямо в коде.
Автоматический рефакторинг с помощью AI: алгоритм
Автоматический рефакторинг с помощью нейросетей пока нельзя назвать универсальным решением, но тема активно обсуждается и вызывает интерес. Мы решили на практике проверить, каким может быть процесс улучшения кода с использованием искусственного интеллекта и что из этого получится, на примере ChatGPT.
1. Выбор фрагмента кода. Из всей кодовой базы выбираем функцию или участок, который кажется «узким местом»: слишком длинный, повторяющийся, с ручными операциями или дублирующимися данными. Это мы уже сделали.
2. Подготовка промта. Для получения качественного результата важно корректно сформулировать запрос к AI. Мы используем универсальную инструкцию, которую легко адаптировать под любой Python-код:
Ты опытный Python-разработчик и эксперт по рефакторингу. Твоя задача — улучшить предоставленный код так, чтобы он стал чище, современнее и удобнее для поддержки. Придерживайся лучших практик Python, ориентируйся на стандарты PEP8. Постарайся:
— Упростить и структурировать код.
— Избавиться от дублирования и «магических» чисел.
— Сделать код более читаемым и понятным для других разработчиков.
— Улучшить обработку ошибок, добавить необходимые проверки.
— По возможности применить современные паттерны и стандартные библиотеки Python.
– Обязательно сохраняй интерфейс функций: не изменяй список параметров, возвращаемые значения и поведение при ошибочных/граничных ситуациях, если иное не согласовано отдельно.
— Избегай изменений, затрагивающих бизнес-логику, если она не содержит явных ошибок.
— При необходимости добавь краткие комментарии, поясняющие внесенные улучшения.
В ответе:
- Сначала приведи полный переписанный вариант кода.
- Далее кратко и по пунктам опиши, что именно и зачем было улучшено.
3. Отправка кода и промта в ChatGPT. Вставляем выбранный фрагмент вместе с промтом в окно чата. Можно использовать как веб-интерфейс, так и интеграции с API.
4. Получение и разбор ответа. AI предложит переписанный код и кратко объяснит внесенные улучшения. Важно внимательно изучить ответ — особенно логику обработки ошибок и работу с данными.
5. Проверка и тестирование. Обязательно прогоняем новые функции через тесты (или хотя бы руками проверяем в dev-среде), чтобы убедиться: бизнес-логика не изменилась, а ошибки и предупреждения действительно стали обрабатываться лучше.
6. Интеграция в проект. Если результат устроил — добавляем переписанный код в проект, фиксируем изменения через pull request, описываем плюсы рефакторинга для команды.
Примеры легаси-кода и его преображение
В качестве примеров для эксперимента выбрали функции log_info из файла logging_functions.py и amplifier_info из файла instrument_properties.py. Они показались яркими представителями легаси-кода по ряду причин:
- выполняют слишком много разных задач сразу;
- сильно зависят от окружения и внешних утилит;
- содержат скрытые побочные эффекты, сложны для тестирования;
- имеют много «магических» чисел.
Посмотрим, удалось ли с помощью рефакторинга и инструментов AI сделать этот участок кода более чистым и удобным для поддержки.
Работа с путями и зависимостями
До рефакторинга: используется os.path, нет обработки ошибок, имена файлов жестко сшиты.
toml_file = os.path.join(os.path.dirname(get_config()['setup_file']), 'pyproject.toml')
with open(toml_file, "rb") as f:
data = tomllib.load(f)
required_modules = data['project']['dependencies']
После рефакторинга: используется pathlib, вся логика вынесена в отдельную функцию, добавлена обработка ошибок.
setup_file = Path(get_config()["setup_file"])
pyproject = setup_file.parent / "pyproject.toml"
required_modules = _read_required_modules(pyproject)
Вывод: код стал компактнее и удобнее для поддержки, однако после рефакторинга предполагается, что setup_file в конфиге всегда указывает на файл, а не на директорию. В легаси-проектах это не всегда так, поэтому такой подход может потребовать дополнительной проверки конфигурации или доработки логики для поддержки разных вариантов.
Очистка и обработка списка зависимостей
До рефакторинга: длинная цепочка манипуляций со строкой, неочевидно, какие случаи она покрывает.
module_list = [item.strip().replace("'", "").replace(",", "")
.split("=")[0].split(">")[0].split("<")[0] for item in required_modules]
После рефакторинга: вся логика вынесена, появились комментарии, стало понятно, что и зачем делается.
def _read_required_modules(pyproject_path: Path) -> Iterable[str]:
"""Extract top-level package names from ``project.dependencies`` in pyproject.toml."""
try:
with pyproject_path.open("rb") as f:
data = tomllib.load(f)
deps = data.get("project", {}).get("dependencies", []) or []
except Exception as exc: # log later in decorator
logging.exception("Failed to read dependencies from %s", pyproject_path)
deps = []
# Normalize entries like "package>=1.0,<2.0" -> "package"
cleaned = []
for item in deps:
name = (
item.strip()
.split(";", 1)[0]
.split("[", 1)[0]
.split(">=", 1)[0]
.split("==", 1)[0]
.split("<=", 1)[0]
.split("<", 1)[0]
.split(">", 1)[0]
.split("~=", 1)[0]
.strip()
)
if name:
cleaned.append(name)
return cleaned
Вывод: теперь модуль обработки зависимостей можно использовать и тестировать отдельно, он стал прозрачнее и надежнее.
Логирование информации о модулях
До рефакторинга: логика размазана, используются строковые конкатенации, ошибки сливаются в одну кучу.
for module in module_list:
try:
mod = importlib.import_module(module)
logging.info(module + ' Version: ' + importlib.metadata.version(module))
logging.info(module + ' Path: ' + mod.__path__[0])
except (ImportError, AttributeError) as err:
logging.warning(err)
После рефакторинга: выделен отдельный чистый блок для логирования, улучшено форматирование логов, расширена обработка ошибок.
def _log_module_info(modules: Iterable[str]) -> None:
for module in modules:
try:
imported = importlib.import_module(module)
version = importlib.metadata.version(module)
path = getattr(imported, "__path__", [getattr(imported, "__file__", "N/A")])[0]
logging.info("%s Version: %s", module, version)
logging.info("%s Path: %s", module, path)
except Exception as err:
logging.warning("Could not import/log %s: %s", module, err)
Вывод: код стал более структурированным, логика вынесена в отдельную функцию, а обработка ошибок стала прозрачнее. Однако использование сложных цепочек с getattr и работа с путями в «магическом» стиле сохраняет риск появления неочевидных багов и затрудняет поддержку.
Запуск внешних команд (conda env export)
До рефакторинга: бизнес-логика и работа с окружением спрятаны внутри основного тела функции.
try:
environment = subprocess.check_output('conda env export', universal_newlines=True, shell=True) # nosec
logging.info('Environment:')
for line in environment.split('\n'):
logging.info(line)
except Exception as err:
logging.exception(err)
После рефакторинга: работа с окружением вынесена в отдельную функцию, обработка ошибок стала информативнее.
def _log_environment() -> None:
try:
env_txt = subprocess.check_output(
"conda env export", universal_newlines=True, shell=True,
)
logging.info("Environment:")
for line in env_txt.splitlines():
logging.info(line)
except Exception as err:
logging.exception("Failed to export conda environment: %s", err)
Вывод: сбои при экспорте окружения не мешают основной логике, поведение теперь легче тестировать отдельно.
Логирование времени выполнения
До рефакторинга: много однотипных операций в теле основной функции, повторение логики для разных метрик.
t1_cpu = time.perf_counter()
t1_time = time.time()
func(*args, **kwargs)
t2_cpu = time.perf_counter()
t2_time = time.time()
hours_cpu, remainder_cpu = divmod(t2_cpu - t1_cpu, 60 * 60)
minutes_cpu, seconds_cpu = divmod(remainder_cpu, 60)
hours_time, remainder_time = divmod(t2_time - t1_time, 60 * 60)
minutes_time, seconds_time = divmod(remainder_time, 60)
logging.info('Elapsed Real Time: {}:{}:{}'.format(int(hours_time), int(minutes_time), int(seconds_time)))
logging.info('Elapsed CPU Time: {}:{}:{}'.format(int(hours_cpu), int(minutes_cpu), int(seconds_cpu)))
После рефакторинга: логика форматирования вынесена в отдельную функцию, стало короче и яснее.
def _format_duration(seconds: float) -> str:
hours, remainder = divmod(int(seconds), 3600)
minutes, secs = divmod(remainder, 60)
return f"{hours:02d}:{minutes:02d}:{secs:02d}"
# Внутри log_info:
logger.info("Elapsed Real Time: %s", _format_duration(t_wall_end - t_wall_start))
logger.info("Elapsed CPU Time: %s", _format_duration(t_cpu_end - t_cpu_start))
t_cpu_start = time.perf_counter()
t_wall_start = time.time()
func(*args, **kwargs)
t_cpu_end = time.perf_counter()
t_wall_end = time.time()
Вывод: появилась новая абстракция, которая делает основной код компактнее и проще для поддержки. Но подобное изменение меняет внутреннюю структуру функции, поэтому при рефакторинге важно убедиться, что внешний контракт и ожидаемое поведение остаются неизменными.
Работа с «магическими» числами
До рефакторинга: 2048 — фиксированный размер кадра для всех инструментов, чистое «магическое» число.
def amplifier_info(filename, omit_reference_pixels=True):
…
if instrument.lower() == 'miri' or ((x_dim == 2048) and (y_dim == 2048)) or \
subarray_name in FOUR_AMP_SUBARRAYS:
num_amps = 4
amp_bounds = deepcopy(AMPLIFIER_BOUNDARIES[instrument])
else:
if subarray_name not in NIRCAM_SUBARRAYS_ONE_OR_FOUR_AMPS:
num_amps = 1
amp_bounds = {'1': [(0, x_dim, 1), (0, y_dim, 1)]}
После рефакторинга: «магическое» число вынесено в отдельную константу _FULL_FRAME_SIZE.
_FULL_FRAME_SIZE = 2048
def amplifier_info(filename: str, omit_reference_pixels: bool = True):
…
if (
instrument == "miri"
or (x_dim == _FULL_FRAME_SIZE and y_dim == _FULL_FRAME_SIZE)
or subarray_name in FOUR_AMP_SUBARRAYS
):
# Full-frame (or known 4-amp subarrays) → always 4 amps
num_amps = 4
amp_bounds = deepcopy(AMPLIFIER_BOUNDARIES[instrument])
Вывод: нейросеть правильно идентифицировала «магическое» число и вынесла его в отдельную константу, что благоприятно отразится на дальнейшей поддержке кода.
В результате рефакторинга функции действительно стали чище и понятнее: обязанности разделены, вспомогательные задачи вынесены в отдельные блоки, логирование и обработка ошибок стали более прозрачными. Однако даже при самом аккуратном рефакторинге важно тщательно проверять результат: запускать тесты, анализировать граничные случаи и следить за тем, чтобы не поменялось поведение, от которого зависит остальной код.
Полный результат автоматического рефакторинга этих функций →
Итоги эксперимента
Вернемся к основным признакам легаси-кода, которые мы назвали вначале, и посмотрим, как AI-рефакторинг справился с каждым из них.
- Длинные функции и модули — справился. ChatGPT корректно разбивает монолиты, выносит вспомогательные функции, код становится проще для поддержки.
- Ручной парсинг данных и конфигов — не удалось. В большинстве случаев AI оставляет ручную обработку конфигов, если в оригинале не использованы сторонние библиотеки (например, configparser). Полностью автоматизировать или стандартизировать этот кусок без контекста AI не смог.
- Повторяющиеся фрагменты — справился. ChatGPT умеет определять повторяющиеся куски кода и предлагает выносить их в отдельные функции или методы.
- Слабая обработка ошибок — справился. После рефакторинга обработка ошибок становится более структурированной и информативной, добавляются try/except, логи становятся понятнее.
- «Магические» числа и строки — справился. AI умеет определять зашитые в код «непонятные» значения и выносить их в отдельные переменные и константы.
AI‑инструменты уже умеют брать на себя рутинную часть рефакторинга: дробить длинные функции, устранять дублирование, вычищать «магические» числа. Это экономит часы (а то и дни) разработчиков и ускоряет вывод новых фич. Но важно помнить: нейросеть — это лишь усилитель, а не замена профессионала. Финальное слово всегда остается за командой, которая знает бизнес‑логику и несет ответственность за код.
Как начать безопасно
- Экспериментируйте точечно. Выберите пару некритичных модулей, прогоните их через наш промт, посмотрите diff, запустите тесты.
- Автоматизируйте окружение. Для контейнерной разработки подключите Managed Kubernetes или Container Registry, чтобы быстро разворачивать тестовые стенды и катить обновленные сервисы.
- Делитесь знаниями. Поддерживайте внутреннюю «академию»: короткие гайды, как правильно формировать промты, на какие метрики смотреть до/после, какие паттерны чаще всего предлагает AI.