Автоматическое написание кода на Python: от шаблонов до ИИ
Генерация Python кода: от шаблонов до искусственного интеллекта
В разработке часто возникает необходимость автоматически создавать файлы с кодом. Это может быть генерация классов для ORM, повторяющихся функций, бойлерплейта или даже целых модулей по заданной спецификации. Рассмотрим несколько подходов, от простых до продвинутых.
Как генерировать Python код с помощью шаблонов Jinja2?
Библиотека Jinja2 предоставляет мощный язык шаблонов. Установка: pip install jinja2. Пример шаблона class_template.j2:
from jinja2 import Environment
template_str = '''class {{ class_name }}:
def __init__(self, {{ params | join(', ') }}):
{% for p in params %}
self.{{ p }} = {{ p }}
{% endfor %}
def __repr__(self):
return '{{ class_name }}({{ params | join(', ') }})'
'''
Python generate py (генерация python кода)
Скрипт генерации:
env = Environment()
template = env.from_string(template_str)
rendered = template.render(class_name='Person', params=['name', 'age'])
print(rendered)
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __repr__(self):
return 'Person(name, age)'
Типичные проблемы и решения:
- Проблема экранирования: если в шаблоне есть фигурные скобки, используйте
{{ '{{' }}или блок{% raw %} ... {% endraw %}. - Проблема отступов: Jinja2 может нарушить форматирование. Рекомендуется использовать фильтр
indentили указывать точные пробелы в шаблоне.
Цель использования: генерация кода, который повторяет структуру, но варьируется по именам и параметрам. Подходит для создания классов, функций, конфигураций.
Вариант 1: Генерация через f-строки
Для простых случаев можно использовать f-строки, которые подставляют значения напрямую.
class_name = 'Calculator'
methods = ['add', 'subtract']
code = f'''class {class_name}:
pass
'''
for m in methods:
code += f'''
def {m}(self, a, b):
return a {'+' if m=='add' else '-'} b
'''
print(code)
class Calculator:
pass
def add(self, a, b):
return a + b
def subtract(self, a, b):
return a - b
Проблемы: сложно поддерживать сложные шаблоны с циклами и условиями, легко допустить ошибки в форматировании строки. Решение: для простоты использовать только для небольших фрагментов.
Когда применять: быстрая генерация однострочных деклараций, импортов, простых классов.
Вариант 2: Создание кода через AST (Abstract Syntax Tree)
Библиотеки ast (стандартная) и astunparse (или codegen) позволяют строить код программно, манипулируя узлами дерева.
import ast
import astunparse
# Строим AST для функции сложения
mod = ast.Module(body=[
ast.FunctionDef(
name='add',
args=ast.arguments(
posonlyargs=[],
args=[ast.arg(arg='x'), ast.arg(arg='y')],
kwonlyargs=[],
kw_defaults=[],
defaults=[]
),
body=[
ast.Return(value=ast.BinOp(
left=ast.Name(id='x'),
op=ast.Add(),
right=ast.Name(id='y')
))
],
decorator_list=[],
returns=None
)
], type_ignores=[])
code = astunparse.unparse(mod)
print(code)
def add(x, y):
return x + y
Сложность: требуется знание узлов AST. Ошибки: неправильное построение дерева приводит к невалидному коду. Решение: использовать ast.dump для отладки.
Применение: интеграция с кодогенераторами, метапрограммирование, автоматическое преобразование из других форматов.
Вариант 3: Использование языковых моделей (LLM)
Сервисы вроде OpenAI GPT могут генерировать код по текстовому описанию.
import openai
openai.api_key = 'YOUR_API_KEY'
response = openai.Completion.create(
engine='text-davinci-003',
prompt='Напиши функцию на Python, которая принимает список чисел и возвращает их сумму.',
max_tokens=100
)
generated_code = response.choices[0].text.strip()
print(generated_code)
def sum_list(numbers):
total = 0
for num in numbers:
total += num
return total
Проблемы: модель может выдать неверный или небезопасный код. Зависимость от сети и API. Решение: всегда проверять и тестировать сгенерированный код, устанавливать лимиты токенов.
Когда использовать: исследование, прототипирование, помощь в написании кода по запросу на естественном языке.
Вариант 4: Форматирование сгенерированного кода с помощью Black
Любой из предыдущих методов может выдать код с неидеальным форматированием. Black автоматически приводит его к стандарту PEP8.
import black
raw_code = '''def foo( x ):return x+1'''
formatted = black.format_str(raw_code, mode=black.Mode())
print(formatted)
def foo(x):
return x + 1
Black может изменить стиль, который не нравится команде. Решение: настроить конфигурацию Black, использовать как финальный шаг перед записью в файл.
Цель: гарантировать единообразие кода, особенно при автоматической генерации, когда отступы легко сбиваются.
Продвинутые сценарии генерации Python кода
1. Генерация модели данных с аннотациями типов
Пример шаблона Jinja2, который создает dataclass с полями из списка.
from jinja2 import Template
import black
fields = [{'name': 'id', 'type': 'int'}, {'name': 'name', 'type': 'str'}]
tpl = Template('''from dataclasses import dataclass
@dataclass
class {{ class_name }}:
{% for f in fields %}
{{ f.name }}: {{ f.type }}
{% endfor %}
''')
code = tpl.render(class_name='User', fields=fields)
formatted = black.format_str(code, mode=black.Mode())
print(formatted)
from dataclasses import dataclass
@dataclass
class User:
id: int
name: str
2. Генерация клиента REST API на основе OpenAPI спецификации
Используем парсинг JSON и AST для построения методов.
import json
import ast
import astunparse
spec_dict = {
'paths': {
'/users': {'get': {'operationId': 'listUsers'}},
'/users/{id}': {'get': {'operationId': 'getUser'}}
}
}
func_defs = []
for path, methods in spec_dict['paths'].items():
for method, details in methods.items():
func_name = details['operationId']
args = [ast.arg(arg='self')]
if '{id}' in path:
args.append(ast.arg(arg='user_id'))
func = ast.FunctionDef(
name=func_name,
args=ast.arguments(posonlyargs=[], args=args, kwonlyargs=[], kw_defaults=[], defaults=[]),
body=[ast.Pass()],
decorator_list=[],
returns=None
)
func_defs.append(func)
mod = ast.Module(body=func_defs, type_ignores=[])
code = astunparse.unparse(mod)
print(code)
class Client:
def listUsers(self):
pass
def getUser(self, user_id):
pass
3. Генерация тестов с параметризацией
Сценарий: из JSON-файла с тестовыми данными создаем модуль с pytest-тестами.
import json
test_data = [{'input': [1,2], 'expected': 3}, {'input': [5,0], 'expected': 5}]
test_code = '''import pytest
@pytest.mark.parametrize('a,b,expected', [
'''
for item in test_data:
a, b = item['input']
expected = item['expected']
test_code += f''' ({a}, {b}, {expected}),
'''
test_code += '''])
def test_add(a, b, expected):
assert a + b == expected
'''
print(test_code)
import pytest
@pytest.mark.parametrize('a,b,expected', [
(1, 2, 3),
(5, 0, 5),
])
def test_add(a, b, expected):
assert a + b == expected
4. Динамическое создание методов класса через exec
Не рекомендуется, но иногда используется для плагинов.
class_names = ['Car', 'Bike']
for name in class_names:
code = f'class {name}: pass'
exec(code)
car_instance = Car()
print(type(car_instance))
<class '__main__.Car'>
Предупреждение:
exec может быть опасен, используйте с осторожностью.5. Генерация SQLAlchemy моделей из словаря
Пример с использованием string.Template.
from string import Template
fields = {
'id': {'type': 'Integer', 'primary_key': True},
'name': {'type': 'String(50)'}
}
tpl = Template('''from sqlalchemy import Column, ${types}
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class ${class_name}(Base):
__tablename__ = '${tablename}'
$columns
''')
types = set()
columns = []
for col_name, col_props in fields.items():
types.add(col_props['type'].split('(')[0])
col_line = f" {col_name} = Column({col_props['type']}"
if col_props.get('primary_key'):
col_line += ', primary_key=True'
col_line += ')'
columns.append(col_line)
code = tpl.substitute(
class_name='User',
tablename='users',
columns='\\n'.join(columns),
types=', '.join(sorted(types))
)
print(code)
from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
name = Column(String(50))