Строим собственный фреймворк на 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 42Profile 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')]