Строим собственный фреймворк на Python: архитектурные решения и практические примеры

Раздел: Продвинутый Python -> Разработка фреймворков

При разработке собственного фреймворка на Python важно понимать, что это не просто набор утилит, а продуманная архитектура, упрощающая повторяющиеся задачи. В этой статье рассматриваются ключевые компоненты: маршрутизация, обработка запросов, middleware и шаблонизация. Каждый этап сопровождается примерами кода и разбором типичных затруднений.

Архитектура и основные компоненты

Базовое ядро фреймворка

Наиболее эффективное решение для быстрого старта включает класс Application, который хранит маршруты и обрабатывает входящие запросы. Пример реализации:

import json
from wsgiref.simple_server import make_server

class Application:
    def __init__(self):
        self.routes = {}

    def route(self, path):
        def wrapper(func):
            self.routes[path] = func
            return func
        return wrapper

    def __call__(self, environ, start_response):
        path = environ['PATH_INFO']
        if path in self.routes:
            response = self.routes[path](environ)
        else:
            response = {'status': '404 Not Found', 'body': 'Not Found'}
        start_response(response['status'], [('Content-Type', 'text/html')])
        return [response['body'].encode()]

app = Application()

@app.route('/')
def home(environ):
    return {'status': '200 OK', 'body': '<h1>Home</h1>'}

if __name__ == '__main__':
    server = make_server('localhost', 8000, app)
    server.serve_forever()

создание фреймворка на python (создание собственного фреймворка на python)

Этот фреймворк использует декораторы для привязки функций к URL. Простота позволяет быстро прототипировать, но отсутствует поддержка динамических маршрутов.

Как реализовать динамические маршруты с параметрами?

Вместо статического словаря можно использовать регулярные выражения или шаблоны {param}.

import re

class Route:
    def __init__(self, pattern, handler):
        self.pattern = re.compile(pattern)
        self.handler = handler

app.routes = []

def add_route(pattern, handler):
    app.routes.append(Route(pattern, handler))

@app.route('/user/(\d+)')
def show_user(environ, user_id):
    return {'status': '200 OK', 'body': f'User {user_id}'}

Недостаток - ручное конструирование регулярных выражений. Более удобный подход - использовать pathlib-подобный синтаксис с преобразованием в regex автоматически.

Типичная ошибка - конфликт маршрутов при добавлении в неправильном порядке. Если сначала определён /user/{id}, а затем /user/profile, то второй никогда не сработает, если первый более общий. Решение - сортировать маршруты по специфичности или использовать древовидную структуру (radix tree).

Другая частая проблема - некорректное кодирование тела ответа. В Python 3 строки должны быть закодированы в байты, иначе wsgiref вызовет ошибку TypeError. Всегда используйте .encode().

Как организовать middleware (промежуточное ПО)?

Middleware - это обёртка вокруг приложения, которая модифицирует запрос или ответ. Простейший способ - цепочка функций, вызывающих следующую.

class Middleware:
    def __init__(self, app):
        self.app = app

    def __call__(self, environ, start_response):
        # до обработки
        return self.app(environ, start_response)
        # после обработки

Вариант с использованием стека: хранить список middleware и применять их последовательно.

При реализации middleware нужно учитывать, что start_response вызывается только один раз, и после этого нельзя изменить заголовки. Поэтому все изменения заголовков должны быть сделаны до вызова start_response внутренним приложением. Иначе возникнет исключение AssertionError.

Как реализовать полноценный маршрутизатор с поддержкой регулярных выражений?

Пример
import re
from functools import partial

class Router:
    def __init__(self):
        self.routes = []

    def add(self, pattern, handler, methods=None):
        compiled = re.compile(pattern)
        self.routes.append((compiled, handler, methods or ['GET']))

    def match(self, path, method):
        for pattern, handler, methods in self.routes:
            match = pattern.match(path)
            if match and method in methods:
                return handler, match.groups()
        return None, None

# Пример использования
router = Router()
router.add(r'^/$', lambda: 'Home')
router.add(r'^/user/(\d+)$', lambda uid: f'Profile {uid}', methods=['GET'])

handler, args = router.match('/user/42', 'GET')
if handler:
    print(handler(*args))  # Profile 42
Profile 42

Как добавить поддержку шаблонов Jinja2?

Пример
from jinja2 import Environment, FileSystemLoader
import os

class TemplateRenderer:
    def __init__(self, template_dir='templates'):
        self.env = Environment(loader=FileSystemLoader(template_dir))

    def render(self, template_name, **context):
        template = self.env.get_template(template_name)
        return template.render(context)

# Создаём каталог templates и файл index.html
os.makedirs('templates', exist_ok=True)
with open('templates/index.html', 'w') as f:
    f.write('<h1>Hello, {{ name }}!</h1>')

renderer = TemplateRenderer()
print(renderer.render('index.html', name='Python'))
<h1>Hello, Python!</h1>

Как реализовать асинхронный фреймворк с помощью asyncio?

Пример
import asyncio
from aiohttp import web

async def handle(request):
    return web.Response(text='Async works!')

app = web.Application()
app.router.add_get('/', handle)

if __name__ == '__main__':
    web.run_app(app, host='localhost', port=8080)
(При запуске сервер слушает порт 8080, при переходе на / выводится 'Async works!')

Как внедрить ORM-подобное взаимодействие с базой данных?

Пример
import sqlite3

class Model:
    def __init__(self, db_path='app.db'):
        self.conn = sqlite3.connect(db_path)
        self.cursor = self.conn.cursor()

    def create_table(self, name, columns):
        cols = ', '.join(f'{col} {dtype}' for col, dtype in columns.items())
        self.cursor.execute(f'CREATE TABLE IF NOT EXISTS {name} ({cols})')
        self.conn.commit()

    def insert(self, table, **kwargs):
        cols = ', '.join(kwargs.keys())
        placeholders = ', '.join(['?' for _ in kwargs])
        self.cursor.execute(f'INSERT INTO {table} ({cols}) VALUES ({placeholders})', tuple(kwargs.values()))
        self.conn.commit()

model = Model()
model.create_table('users', {'id': 'INTEGER PRIMARY KEY', 'name': 'TEXT'})
model.insert('users', name='Alice')

result = model.cursor.execute('SELECT * FROM users').fetchall()
print(result)
[(1, 'Alice')]

Создание собственного фреймворка на Python - comments

En
создание фреймворка на python (python)