Создание своего веб-фреймворка на Python - Часть 1
Веб-программирование » Python для начинающих » Общие вопросы
“Не нужно изобретать велосипед” - одна из тех мантр, которую нам повторяют время от времени. Но что, если мы хотим узнать больше о велосипеде? Что, если я хочу научиться делать велосипеды? Я думаю в таком случае, заново изобрести велосипед - отличный способ обучения. Поэтому, в этом руководстве мы напишем собственный веб-фреймворк, чтобы увидеть, как работает магия Flask, Django, и других фреймворков.
В этом руководстве мы построим наиболее важные части фреймворка. В конце у нас появятся обработчики запросов (к примеру, Django views), и маршрутизации: простая (как /books/
) и параметризованная (как /greet/{name}
). Если интересно, в разделе комментариев вы можете рассказать о других функциях, которые на ваш взгляд, стоит реализовать в нашем фреймворке.
Перед тем, как приступить к чему-нибудь новому, я хочу обдумать итоговый результат. В данном случае, в конце дня, мы хотим иметь возможность использовать данный фреймворк в работе, а это значит, мы хотим, чтобы наш фреймворк поддерживался быстрым, легким и эффективным сервером. В своих проектах я использую gunicorn на протяжении нескольких лет, и очень доволен результатами. В связи с этим, я решил использовать gunicorn и для данного проекта.
Gunicorn это WSGI HTTP сервер, так что для него нужна особенная точка входа в наше приложение.
Ознакомились с WSGI? Отлично! Давайте продолжим.
Чтобы добиться WSGI совместимости, нам нужен вызываемый объект (функция или класс), который принимает два параметра (environ и start_response), и возвращает совместимый с WSGI ответ. Не волнуйтесь о том, что написанное кажется непонятным. Суть может стать яснее, когда мы перейдем к коду.
VPS для практики
Если вы начинающий программист, то рано или поздно вам придется познакомиться с Linux и запуском своего приложения сразу на рабочий VPS для клиента или для собственного проекта. Мы рекомендуем VPS от Fornex, т.к. данный хостинг отлично подходит для тех кто хочет быстро получить рабочий и надежный VPS.
Какой VPS выбрать?
Это самый сложный вопрос который может появится у новичка над которым вываливают весь спектр услуг.
Первым делом нужно завести аккаунт на Fornex.com
На данном этапе нашего проекта подойдет и обычный VPS.
Выбираем SSD CLOUD 1GB и операционную систему Debian 9. Всегда нужно выбирать последнюю самую новую версию. Это избавит вас от проблем со старыми библиотеками.
Подключение по SSH
После заказа нашего VPS, мы получим данные от сервера. Нам понадобиться логин и пароль от SSH. Для того чтобы войти по SSH мы воспользуемся Putty (для Windows) либо в обычный терминал на Linux пишем:
ssh root@IP-нашего-VPS
Обновляемся:
$ apt update
$ apt upgrade
Устанавливаем Python:
$ apt install python3
Для корректной работы, нужно установить необходимые библиотеки:
$ apt install python3-setuptools python3-dev python3-venv
$ apt install libtiff5-dev libjpeg8-dev zlib1g-dev
$ apt install libfreetype6-dev liblcms2-dev libwebp-dev tcl8.6-dev tk8.6-dev python-tk
Ниже мы будем использовать менеджер пакетов pip, устанавливаем его:
$ apt install python3-pip
Создание веб-фреймворка
Придумайте название вашего фреймворка и создайте одноименную папку. Свой я назвал bumbo
:
cd /home
mkdir bumbo
Переходим к этой папке, создаем виртуальное окружение и активируем её:
cd bumbo
python3.6 -m venv venv
source venv/bin/activate
Теперь мы создаем файл под названием app.py
, где будет находиться наша точка входа для gunicorn:
touch app.py
Внутри нашего файла app.py
мы впишем простую функцию, чтобы узнать, будет ли она работать с gunicorn
:
# app.py
def app(environ, start_response):
response_body = b"Hello, World!"
status = "200 OK"
start_response(status, headers=[])
return iter([response_body])
Как говорилось ранее, вызываемый точкой входа объект получает два параметра. Один из них - environ
, где вся информация о запросах хранится в качестве метода request, url, параметров запроса, и тому подобное. Второй параметр - start_response
, который высылает предполагаемый ответ. Теперь, попробуем запустить этот код с gunicorn
. Для этого нам нужно его установить и запустить следующим образом:
pip install gunicorn
gunicorn app:app
Первая app
- это файл, который мы создали, вторая - это название функции, которую мы только что написали. Если все прошло удачно, вы увидите выдачу наподобие следующей:
[2019-03-20 17:58:56 +0500] [30962] [INFO] Starting gunicorn 19.9.0
[2019-03-20 17:58:56 +0500] [30962] [INFO] Listening at: http://127.0.0.1:8000 (30962)
[2019-03-20 17:58:56 +0500] [30962] [INFO] Using worker: sync
[2019-03-20 17:58:56 +0500] [30966] [INFO] Booting worker with pid: 30966
Если вы видите такую выдачу, откройте свой браузер и перейдите на http://localhost:8000
. Вы должны увидеть нашего старого доброго друга, сообщение Hello, World!
Далее мы будем работать исходя из этого.
Теперь, давайте сделаем эту функцию классом, так как нам понадобятся несколько вспомогательных методов, и их намного проще прописать внутри класса.
Создадим файл api.py
:
touch api.py
Внутри этого файла создадим следующий класс API
. Вкратце объясню, что он делает.
# api.py
class API:
def __call__(self, environ, start_response):
response_body = b"Hello, World!"
status = "200 OK"
start_response(status, headers=[])
return iter([response_body])
Теперь, удалите все внутри app.py
и впишите следующее:
# app.py
from api import API
app = API()
Перезапустите gunicorn
и проверьте результат в браузере. Он должен быть таким же, как и раньше, так как мы просто конвертировали нашу функцию под названием арр
в класс под названием API
и переопределили его метод __call__
, который вызывается при вызове экземпляров этого класса:
app = API()
app() # здесь вызывается __call__
Теперь, когда мы создали наш класс, я хочу сделать код более элегантным, так как наши байты (b"Hello World"
) и start_response
могут запутать нас.
К счастью, есть замечательный пакет под названием WebOb, который предоставляет объекты для HTTP запросов и ответов, заворачивая среду запросов WSGI и статус, заголовки и тело ответов. Используя данный пакет, мы можем передать environ и start_response классам, которые предоставляются этим пакетом, без необходимости разбираться с этим самостоятельно.
Перед тем как продолжить, я предлагаю вам ознакомиться с документацией WebOb, чтобы понять о чем я толкую, а также обратить внимание на API нашего WebOb.
Здесь мы приступим к рефакторингу данного кода. Для начала, установим WebOb:
pip install webob
Импортируйте классы Request и Response в начало файла api.py:
# api.py
from webob import Request, Response
...
Теперь мы можем использовать их внутри метода __call__
:
# api.py
from webob import Request, Response
class API:
def __call__(self, environ, start_response):
request = Request(environ)
response = Response()
response.text = "Hello, World!"
return response(environ, start_response)
Выглядит намного лучше! Перезапустите gunicorn и увидите тот же результат, что и раньше. Лучшая часть в том, что мне не нужно объяснять, что здесь происходит! Всё говорит само за себя. Мы создаем запрос, и возвращаем этот ответ.
Отлично! Хочу обратить внимание на то, что request здесь еще не используется, так как мы ничего для этого не сделали. Итак, давайте используем эту возможность и также используем объект request. Кстати, давайте проведем рефакторинг создания ответа, превратив его в собственный метод. Почем так лучше? Мы узнаем позже:
# api.py
from webob import Request, Response
class API:
def __call__(self, environ, start_response):
request = Request(environ)
response = self.handle_request(request)
return response(environ, start_response)
def handle_request(self, request):
user_agent = request.environ.get("HTTP_USER_AGENT", "No User Agent Found")
response = Response()
response.text = f"Здравствуй, мой друг с браузером: {user_agent}"
return response
Перезапустите gunicorn и увидите новое сообщение в браузере, а мы будем двигаться дальше.
С этого момента, все запросы обработаны общим путем. Вне зависимости от того, какой запрос мы получили, мы просто возвращаем один и тот же ответ, который создан в методе handle_request. В конечном счете, нам нужно быть динамичными. Таким образом, нам нужно обработать запрос от /home/
, иначе чем обработка запроса из /about/
.
Для этого, создадим два метода внутри app.py
. Они будут обрабатывать эти два запроса:
# app.py
from api.py import API
app = API()
def home(request, response):
response.text = "Привет! Это ГЛАВНАЯ страница"
def about(request, response):
response.text = "Привет! Это страница О НАС!"
Теперь нам нужно как-то связать эти два метода с упомянутыми ранее путями: /home/
и /about/
. Мне нравится как Flask справляется с данной задачей и я решил вдохновиться от него:
# app.py
from api.py import API
app = API()
@app.route("/home")
def home(request, response):
response.text = "Привет! Это ГЛАВНАЯ страница"
@app.route("/about")
def about(request, response):
response.text = "Привет! Это страница О НАС!"
Что скажете? Выглядит неплохо. Давайте это реализуем.
Как вы видите, метод route является декоратором, принимает путь и оборачивает методы. Это будет несложно реализовать:
# api.py
class API:
def __init__(self):
self.routes = {}
def route(self, path):
def wrapper(handler):
self.routes[path] = handler
return handler
return wrapper
...
Что было сделано? В методе __init__
мы просто определили словарь под названием self.routes, в котором мы будем хранить пути в качестве ключей, а обработчики - в качестве значений. Это может выглядеть следующим образом:
print(self.routes)
{
"/home": ,
"/about":
}
В методе route, мы возьмем путь в качестве аргумента и в методе wrapper просто внесем этот путь в словарь self.routes в качестве ключа, а обработчик - в качестве значения.
Сейчас у нас есть все необходимые детали. У нас есть обработчики и связанные с ними пути. Теперь, при получении запроса, нам нужно проверить его путь, подобрать подходящий обработчик, вызвать его и вернуть соответствующий ответ. Давайте сделаем это:
# api.py
from webob import Request, Response
class API:
...
def handle_request(self, request):
response = Response()
for path, handler in self.routes.items():
if path == request.path:
handler(request, response)
return response
...
Не так уж и сложно, не так ли? Мы просто провели итерацию над self.routes, сравнили пути с путем запроса, и при совпадении, вызвали обработчик, связанный с этим путем.
Перезапустите gunicorn и проверьте эти пути в браузере. Сначала, перейдите по http://localhost:8000/home/
, и затем по http://localhost:8000/about/
. Вы должны увидеть соответствующие сообщения. Удобно, не так ли?
Следующим нашим действием будет найти ответ на вопрос “Что случится, если путь не будет найден?”. Давайте создадим метод, который возвращает простой HTTP ответ “не найдено” со статусом кода 404:
# api.py
from webob import Request, Response
class API:
...
def default_response(self, response):
response.status_code = 404
response.text = "Not found."
...
Теперь, используем это в нашем методе handle_request
:
# api.py
from webob import Request, Response
class API:
...
def handle_request(self, request):
response = Response()
for path, handler in self.routes.items():
if path == request.path:
handler(request, response)
return response
self.default_response(response)
return response
...
Перезапустите gunicorn и попробуйте посетить несуществующие пути. Вы должны увидеть страницу “Not found”. Теперь, выполним рефакторинг таким образом, чтобы найти обработчик для его собственного метода ради читаемости:
# api.py
from webob import Request, Response
class API:
...
def find_handler(self, request_path):
for path, handler in self.routes.items():
if path == request_path:
return handler
...
Как и в предыдущем случае, он просто итерирует над self.route
, сравнивает пути с путем запроса и возвращает обработчик, если пути совпадают. Он возвращает None, если обработчик не был найден. Теперь, мы можем использовать его в нашем методе handle_request:
# api.py
from webob import Request, Response
class API:
...
def handle_request(self, request):
response = Response()
handler = self.find_handler(request_path=request.path)
if handler is not None:
handler(request, response)
else:
self.default_response(response)
return response
...
На мой взгляд, все выглядит намного лучше и понятнее. Перезапустите gunicorn, чтобы убедиться в там, что все работает так же, как и раньше.
Теперь у нас есть пути и обработчики. Это замечательно, но наши пути достаточно простые. Они не поддерживают сложные параметры ключевых слов в пути URL. Что если нам нужен путь наподобие @app.route("/hello/{person_name}")
и иметь возможность использовать значение person_name внутри наших обработчиков, вот так:
def say_hello(request, response, person_name):
resp.text = f"Hello, {person_name}"
Для этого, если кто-то перейдет по /hello/Matthew/
, нам нужно иметь возможность сопоставить этот путь с зарегистрированным /hello/{person_name}/
и найти надлежащий обработчик. К счастью, есть готовый пакет под названием parse который делает именно то, что нам нужно. Давайте установим его:
pip install parse
И протестируем:
>>> from parse import parse
>>> result = parse("Hello, {name}", "Hello, Matthew")
>>> print(result.named)
{'name': 'Matthew'}
Как вы видите, он проанализировал строку Hello, Matthew
и определил, что Matthew соответствует предоставленному {name}
.
Давайте используем его в нашем методе find_handler, чтобы не только найти метод, который соответствует пути, но и параметрам, которые мы предоставляем:
# api.py
from webob import Request, Response
from parse import parse
class API:
...
def find_handler(self, request_path):
for path, handler in self.routes.items():
parse_result = parse(path, request_path)
if parse_result is not None:
return handler, parse_result.named
return None, None
...
Мы все еще итерируем над self.routes, и теперь вместо сравнения пути с путем запроса, мы попытаемся проанализировать его, и если будет результат, мы вернем обработчик и параметры ключевых слов как словарь. Теперь, мы можем использовать наш handle_request для отсылки этих параметров в обработчик вот так:
# api.py
from webob import Request, Response
from parse import parse
class API:
...
def handle_request(self, request):
response = Response()
handler, kwargs = self.find_handler(request_path=request.path)
if handler is not None:
handler(request, response, **kwargs)
else:
self.default_response(response)
return response
...
Единственное, что здесь меняется - это то, что мы получаем обработчик и аргументы ключевых слов kwargs от self.find_handler, и передаем kwargs обработчику вот так: **kwargs.
Давайте напишем обработчик с таким типом пути и испробуем его:
# app.py
...
@app.route("/hello/{name}")
def greeting(request, response, name):
resp.text = f"Hello, {name}"
...
Перезапустите gunicorn и перейдите по http://localhost:8000/hello/Matthew/
. Вы увидите замечательное сообщение Hello, Matthew
. Шикарно, да? Добавьте немного своих подобных обработчиков.
Вы также можете указать тип заданных параметров. Например, вы можете выполнить @app.route("/tell/{age:d}")
, чтобы получить age вашего параметра внутри обработчика в виде цифры.
Вывод
Это был длинный путь, но я думаю он был просто замечательным. Я лично узнал много нового, пока писал это. Если вам понравилось данное руководство, дайте мне знать в комментариях, какие другие функции должны быть реализованы в нашем фреймворке. Лично я подумываю об основанных на классах обработчиках, поддержку шаблонов и статичных файлах.
Хорошая статья, а вторая часть есть?
Автор, пиши ещё! А если писать свою CMS на Python с нуля, начало будет таким же?
Норм перевод, а почему остальное не стал переводить?
играть онлайн казино Вулкан Платинум супер
Хорошая статья, благодарю!