Автоматическое написание кода на 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))

Генерация Python кода - comments

En
Python generate py (python)