Автоматическое создание скриптов на Python: инструменты и методики
Основные методы генерации кода Python
Как программно создать исполняемый код Python, который будет корректен с точки зрения синтаксиса и структуры?
Наиболее эффективным решением является использование модуля ast (Abstract Syntax Tree). Он позволяет конструировать дерево синтаксического разбора, а затем с помощью функции ast.unparse (Python 3.9+) или сторонних библиотек (например, astor) превращать его обратно в строку кода. Такой подход гарантирует валидность кода и упрощает сложные модификации.
import ast
# Создаём узлы для функции
func_def = ast.FunctionDef(
name='add',
args=ast.arguments(
posonlyargs=[],
args=[ast.arg(arg='a'), ast.arg(arg='b')],
kwonlyargs=[],
kw_defaults=[],
defaults=[]
),
body=[
ast.Return(value=ast.BinOp(
left=ast.Name(id='a', ctx=ast.Load()),
op=ast.Add(),
right=ast.Name(id='b', ctx=ast.Load())
))
],
decorator_list=[]
)
# Превращаем AST в код
code = ast.unparse(func_def)
print(code)генерировать код python (генерировать код python)
def add(a, b):
return a + b
Пошаговое описание:
- Импортируем модуль ast.
- Создаём объект FunctionDef, передавая имя, аргументы, тело и декораторы.
- В теле указываем объект Return с выражением сложения (BinOp).
- Вызываем ast.unparse для получения строки кода.
Проблемы и их решение:
- Сложность ручного создания узлов – помогает изучение документации по ast и использование ast.dump для анализа существующего кода.
- Отличия в версиях Python – в Python 3.8 нет ast.unparse, требуется astor или astunparse.
Как быстро сгенерировать небольшой фрагмент кода без сторонних библиотек?
Простейший способ – прямая конкатенация строк с учётом отступов. Для удобства используют тройные кавычки и textwrap.dedent.
import textwrap
code = textwrap.dedent('''\
def multiply(a, b):
return a * b
''')
print(code)
def multiply(a, b):
return a * b
Этот метод пригоден для маленьких статических шаблонов, но становится громоздким при параметризации.
Частая ошибка: неправильное количество пробелов в отступах. Решение – всегда использовать одинаковое число пробелов (4) и применять textwrap.dedent для выравнивания.
Как параметризовать генерацию кода, подставляя разные имена и выражения?
Использование f-строк позволяет вставлять переменные в шаблон кода. Например, для создания функции с произвольным именем:
func_name = 'calc'
params = ['x', 'y']
expr = 'x ** 2 + y'
code = f"def {func_name}({', '.join(params)}):\n return {expr}\n"
print(code)
def calc(x, y):
return x ** 2 + y
Такой подход удобен для простых генераций, но рискован при включении пользовательского ввода – возможна инъекция кода.
Ошибка: забыть экранировать фигурные скобки внутри f-строки. Если в шаблоне нужна литеральная фигурная скобка, её удваивают: {{ }}.
Как генерировать целые файлы или проекты с многоуровневой структурой?
Для этого применяется шаблонизатор Jinja2. Он поддерживает циклы, условия, наследование шаблонов. Пример шаблона для генерации модуля:
from jinja2 import Template
template = Template('''
def {{ func_name }}({{ args|join(', ') }}):
"""{{ docstring }}"""
return {{ expression }}
''')
code = template.render(
func_name='hello',
args=['name'],
docstring='Prints a greeting',
expression="f'Hello, {name}!'"
)
print(code)
def hello(name):
"""Prints a greeting"""
return f'Hello, {name}!'
Jinja2 подходит для генерации конфигурационных файлов, тестов, DTO и других повторяющихся структур.
Типичная проблема: экранирование специальных символов Python (например, обратного слеша). Решение – использовать raw-строки в шаблоне или экранировать через |e.
Как получить код на основе естественно-языкового описания?
Современные языковые модели (GPT-4, Claude) позволяют генерировать код по текстовому запросу. Пример через библиотеку openai (требуется API-ключ):
import openai
response = openai.ChatCompletion.create(
model="gpt-4",
messages=[
{"role": "user", "content": "Напиши функцию на Python, которая сортирует список чисел по убыванию."}
]
)
code = response.choices[0].message.content
print(code)
def sort_descending(numbers):
return sorted(numbers, reverse=True)
Этот способ эффективен для быстрого прототипирования, но результат может содержать синтаксические ошибки или неоптимальные решения.
Недостатки: зависимость от внешнего API, затраты, задержки, отсутствие контроля над форматированием. Решение – комбинировать генерацию с последующей верификацией через ast.parse.
Расширенные примеры генерации кода
Генерация класса с конструктором и методами через AST
Создадим класс Person с атрибутами имени и возраста, а также методом greet.
import ast
# Конструируем класс
class_def = ast.ClassDef(
name='Person',
bases=[],
keywords=[],
body=[
# Метод __init__
ast.FunctionDef(
name='__init__',
args=ast.arguments(
posonlyargs=[],
args=[ast.arg(arg='self'), ast.arg(arg='name'), ast.arg(arg='age')],
kwonlyargs=[],
kw_defaults=[],
defaults=[]
),
body=[
ast.Assign(
targets=[ast.Attribute(value=ast.Name(id='self', ctx=ast.Load()), attr='name', ctx=ast.Store())],
value=ast.Name(id='name', ctx=ast.Load())
),
ast.Assign(
targets=[ast.Attribute(value=ast.Name(id='self', ctx=ast.Load()), attr='age', ctx=ast.Store())],
value=ast.Name(id='age', ctx=ast.Load())
)
],
decorator_list=[]
),
# Метод greet
ast.FunctionDef(
name='greet',
args=ast.arguments(
posonlyargs=[],
args=[ast.arg(arg='self')],
kwonlyargs=[],
kw_defaults=[],
defaults=[]
),
body=[
ast.Return(
value=ast.Call(
func=ast.Attribute(value=ast.Constant(value='Hello, my name is '), attr='format', ctx=ast.Load()),
args=[ast.Attribute(value=ast.Name(id='self', ctx=ast.Load()), attr='name', ctx=ast.Load())],
keywords=[]
)
)
],
decorator_list=[]
)
],
decorator_list=[]
)
# Преобразуем в строку
code = ast.unparse(class_def)
print(code)
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def greet(self):
return 'Hello, my name is '.format(self.name)
Результат – валидный Python-класс, готовый к выполнению.
Генерация декоратора для замера времени исполнения
Создадим шаблон декоратора с использованием f-строк и текстового шаблона.
import textwrap
def generate_timer_decorator(import_time=True):
parts = []
if import_time:
parts.append('import time')
parts.append('')
parts.append('def timer(func):')
parts.append(' def wrapper(*args, **kwargs):')
parts.append(' start = time.time()')
parts.append(' result = func(*args, **kwargs)')
parts.append(' end = time.time()')
parts.append(' print(f"Function {func.__name__} took {end-start:.4f} sec")')
parts.append(' return result')
parts.append(' return wrapper')
return textwrap.dedent('\n'.join(parts))
code = generate_timer_decorator(import_time=True)
print(code)
import time
def timer(func):
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(f"Function {func.__name__} took {end-start:.4f} sec")
return result
return wrapper
Этот код можно сразу сохранить в файл decorators.py и импортировать.
Генерация кода для Flask-эндпоинта на основе конфигурации
Используем Jinja2 для создания REST-обработчика.
from jinja2 import Template
route_template = Template('''
@app.route('{{ path }}', methods=[{{ methods | map('quote') | join(', ') }}])
def {{ func_name }}({{ params }}):
"""{{ doc }}"""
# TODO: implement
return {{ response }}
''')
config = {
'path': '/api/users/',
'methods': ['GET', 'PUT'],
'func_name': 'user_detail',
'params': 'user_id',
'doc': 'Get or update user by ID',
'response': '{"message": "ok"}'
}
code = route_template.render(config)
print(code)
@app.route('/api/users/', methods=['GET', 'PUT'])
def user_detail(user_id):
"""Get or update user by ID"""
# TODO: implement
return {"message": "ok"}
Так можно автоматически сгенерировать весь набор эндпоинтов, если есть список конфигураций.
Использование inspect для воссоздания функции из существующей сигнатуры
Иногда требуется сгенерировать код, эквивалентный уже существующей функции, но с изменёнными именами.
import inspect
def original_function(a, b: int = 10, *args, **kwargs) -> float:
"""Original docstring."""
return a + b
# Получаем сигнатуру
sig = inspect.signature(original_function)
source = inspect.getsource(original_function)
# Создаём новую функцию с другим именем
new_name = 'new_function'
new_source = source.replace('original_function', new_name)
new_source = new_source.replace('Original docstring.', 'Generated copy.')
print(new_source)
def new_function(a, b: int = 10, *args, **kwargs) -> float:
"""Generated copy."""
return a + b
Этот приём полезен для кодогенерации на основе рефлексии, например, при создании ORM-моделей.