Если по докладу Артёма Малышева (proofit404) будут снимать фильм, то режиссером выступит Квентин Тарантино — один фильм про Django он уже снял, снимет и второй. Все подробности из жизни внутренних механизмов Django от первого байта HTTP-запроса до последнего байта ответа. Феерия работы парсер-форм, остросюжетная компиляция SQL, спецэффекты реализации шаблонизатора для HTML. Кем и как управляется connection pool? Всё это в хронологическом порядке обработки WSGI-объектов. На всех экранах страны — расшифровка «Django under microscope».
О спикере: Артём Малышев — основатель проекта Dry Python и Core-разработчик Django Channels версии 1.0. Пишет на Python 5 лет, помогал организовывать митапы «Rannts» по Python в Нижнем Новгороде. Артём может быть знаком вам под ником PROOFIT404. Презентация к докладу хранится здесь.
Когда-то давным-давно мы запустили еще старую версию Django. Тогда она выглядела страшно и уныло.
Увидели, что
Процесс запускается, обрабатывает HTTP запросы и внутри происходит вся магия и исполняется весь тот код, который мы хотим показать пользователям в виде сайта.
Installation
Появляется
Bootstrap
Что происходит внутри функции? Bootstrap, который делится на две итерации.
Configure settings
Первая — это чтение конфигов:
Читаются настройки по умолчанию
Кто написал на Django хотя бы «Hello, world», знает, что там есть
Populate apps
Во второй части все эти applications, по сути пакеты, итерируем по одному. Создаем для каждого Config, импортируем модели для работы с базой данных и проверяем модели на целостность. Дальше фреймворк отрабатывает
Это минимальный sanity check для моделей, для админки, для чего угодно — без подключения к базе, без чего-то сверхсложного и специфичного. На этой стадии Django еще не знает, какую команду вы попросили исполнить, то есть не отличает
Дальше мы попадаем в модуль, который пытается угадать по аргументам командной строки, какую команду мы хотим исполнить и в каком приложении она лежит.
Management command
В данном случае в модуле runserver будет встроенный модуль
Дальше идем в модуль runserver и видим, что Django сделан из «regexp’ов и палок», про которые я буду сегодня подробно рассказывать:
Commands
Скроллим вниз на полтора экрана — наконец попадаем в определение нашей команды, которая запускает сервер.
WSGI server
Что же такое WSGIHandler? WSGI — это интерфейс, который позволяет обрабатывать HTTP-запросы с минимальным уровнем абстракции, и выглядит, как нечто в виде функции.
WSGI handler
Например, здесь это экземпляр класса, у которого определен
Дальше мы можем через объект response передавать тело response в сервер. Response — это генератор, по которому можно итерироваться.
Все сервера, которые написаны для WSGI — Gunicorn, uWSGI, Waitress, работают по этому интерфейсу и взаимозаменяемы. Мы сейчас рассматриваем сервер для девелопмента, но любой сервер приходит к тому, что в Django он стучится через environ и callback.
Что внутри God Object?
Что происходит внутри этой глобальной функции God Object внутри Django?
Вся машинерия, которую мы хотим от Django, происходит внутри одной функции, которая размазана на весь фреймворк.
Request
Оборачиваем environment WSGI, который есть простой dictionary, в какой-то специальный объект, для удобства работы с environment. Например, узнать длину пользовательского запроса удобнее через работу с чем-то похожим на dictionary, чем с байт-строкой, которую нужно парсить и искать в ней вхождения ключ-значение. При работе с куками, тоже не хочется вычислять вручную — истек срок хранения или нет, и как-то это интерпретировать.
Request содержит парсеры, а также набор handlers для управления обработкой тела POST-запроса: будет ли это файл в памяти или временный в хранилище на диске. Все решается внутри Request. Также Request в Django — это объект-агрегатор, в который все middlewares могут поместить необходимую нам информацию про сессию, аутентификацию и авторизацию пользователя. Можно сказать, что это тоже God Object, но поменьше.
Дальше Request попадает в middleware.
Middlewares
Middleware — это обертка, которая оборачивает другие функции как декоратор. Перед тем как отдать контроль middleware, в методе call мы отдаем response или вызываем уже оборачиваемую middleware.
Так выглядит middleware с точки зрения программиста.
Settings
Define
С точки зрения Django, middlewares выглядят как своеобразный стек:
Apply
Берем изначальную функцию
Мы прошли 7 кругов middlewares, наш request выжил и решил обрабатывать это во view. Дальше мы попадаем в модуль routing.
Routing
Это то, где мы решаем, какой handler вызвать для какого-то конкретного запроса. А решается это:
Urls
Берем резольвер, скармливаем ему текущий url запроса и ожидаем, что он вернет саму функцию view, и из этого же url достанет аргументы, с которыми надо вызвать view. Дальше
Resolver
Так выглядит резольвер:
Это тоже regexp, но рекурсивный. Он идет по частям url, ищет то, что хочет пользователь: других пользователей, посты, блоги, либо это какой-то конвертер, например, конкретный год, который нужно вычленить, положить в аргументы, привести к int.
Характерно, что глубина рекурсии метода resolve всегда равна количеству аргументов, с которым вызывается view. Если что-то пошло не так и мы не нашли конкретный url, возникает not found error.
Дальше мы наконец попадаем во view — в код, который написал программист.
View
В самом простом представлении — это функция, которая возвращает request от response, но внутри у нее мы выполняем логические задачи: «за, если, когда-нибудь» — много повторяющихся задач. Django нам предоставляет class based view, где можно указать конкретные детали, и все поведение будет интерпретировано в правильном формате уже самим классом.
Method flowchart
Метод
Form
Форма перед тем, как попадет в представление Django, должна быть прочитана из сокета — через тот самый файловый handler, который лежит в WSGI-environment. form-data представляет из себя byte stream, в котором описаны разделители — эти блоки мы можем прочитать и что-то из них сделать. Это может быть соответствие ключ-значение, если это поле, часть файла, потом снова какое-то поле — всё смешано.
Parser
Парсер состоит из 3 частей.
Chunk-итератор, который из byte stream создает ожидаемые чтения — превращает в итератор, который может выдавать
Дальше генератор оборачивается в LazyStream, который создает из него снова файл object, но с ожидаемым чтением. Так парсер уже может ходить по кусочкам байтов и строить из них ключ-значение.
field и data здесь всегда будет являться строками. Если к нам пришла datatime в ISO-формате, уже Django-форма (которая написана программистом) с помощью определенных полей получит, например, timestamp.
Дальше форма, скорее всего, захочет сохранить себя в базу данных, и здесь начинается Django ORM.
ORM
Примерно через такой DSL выполняются запросы на ORM:
С помощью ключей можно собирать подобные SQL-выражения:
Как это происходит?
Queryset
У метода
При обходе дерева, каждый из участков опрашивает свои дочерние ноды, получает вложенные SQL-запросы, и в результате мы сможем построить SQL, как строку. Например, ключ-значение будет не отдельным SQL-полем, а будет сравниваться с value-значением. Так же работает и конкатенация, и отрицание запросов — рекурсивным обходом по дереву, у каждой ноды которого вызывается каст к SQL.
Compiler
Output
В этот метод передается небольшой helper-compiler, который может отличить диалект MySQL от PostgreSQL и правильно расставить синтаксический сахар, который используется в диалекте конкретной базы данных.
DB routing
Когда мы получили SQL-запрос, модель стучится в DB routing и спрашивает, в какой базе данных она лежит. В 99% случаев это будет база данных default, в оставшемся 1% — какая-то своя.
Обертка над драйвером баз данных из специфичного интерфейса библиотеки, таких как Python MySQL или Psycopg2, создает универсальный объект, с которым Django может работать. Есть своя обертка для курсоров, своя обертка для транзакций.
Connecting pool
В этом конкретном connection мы отправляем запросы в сокет, который стучится в БД, и ждем выполнения. Обертка над библиотекой будет читать уже человеческий ответ от БД в виде записи, и Django из этих данных в Python типах собирает инстанс модели. Это не сложная итерация.
Мы что-то записали в базу, что-то прочитали и решили сказать об этом пользователю с помощью HTML-странички. Для этого у Django есть нелюбимый сообществом язык шаблонов, который выглядит, как что-то похожее на язык программирования, только в HTML-файле.
Template
Code
Parser
Сюрприз — опять regexp. Только в конце должна быть запятая, и список продолжится далеко вниз. Наверное, это самый сложный regexp, который я видел в этом проекте.
Lexer
Обработчик шаблона и интерпретатор устроен довольно просто. Есть lexer, который с помощью regexp переводит текст в список маленьких токенов.
Итерируемся по списку токенов, смотрим: «Ты кто? Обернем тебя в тэг-ноду». Например, если это старт какого-то
Операция опять идет в парсер.
Parser
Обработчик тэга дает нам конкретную ноду, например, с циклом
For loop
For node
Метод
Response
Наконец-то мы получили нашу строку с HTTP-response:
Мы можем отдавать строку пользователю.
Все известные стартапы, которые были написаны на Django, например, Bitbucket или Instagram, начинались с такого небольшого цикла, который проходил каждый программист.
Все это, и выступление на Moscow Python Conf++ нужно, чтобы вы лучше понимали, что находится у вас в руках и как этим пользоваться. В любой магии есть большая часть regexp, которые надо уметь готовить.
О спикере: Артём Малышев — основатель проекта Dry Python и Core-разработчик Django Channels версии 1.0. Пишет на Python 5 лет, помогал организовывать митапы «Rannts» по Python в Нижнем Новгороде. Артём может быть знаком вам под ником PROOFIT404. Презентация к докладу хранится здесь.
Когда-то давным-давно мы запустили еще старую версию Django. Тогда она выглядела страшно и уныло.
Увидели, что
self_check
прошел, мы все правильно установили, все заработало и теперь можно писать код. Чтобы всего этого добиться, мы должны были запустить команду django-admin runserver
.$ django-admin runserver
Performing system checks…
System check identified no issues (0 silenced).
You have unapplied migrations; your app may not work properly until they are applied. Run 'python manage.py migrate1 to apply them.
August 21, 2018 - 15:50:53
Django version 2.1, using settings 'mysite.settings'
Starting development server at http://127.0.0.1:8000/Quit the server with CONTROL-C.
Процесс запускается, обрабатывает HTTP запросы и внутри происходит вся магия и исполняется весь тот код, который мы хотим показать пользователям в виде сайта.
Installation
django-admin
появляется в системе, когда мы устанавливаем Django с помощью, например, pip — пакетного менеджера.$ pip install Django
# setup.py
from setuptools import find_packages, setup
setup(
name='Django',
entry_points={
'console_scripts': [
'django-admin =
django.core.management:execute_from_command_line'
]
},
)
Появляется
entry_points setuptools
, который указывает на функцию execute_from_command_line
. Эта функция — точка входа для любой операции с Django, для любого текущего процесса.Bootstrap
Что происходит внутри функции? Bootstrap, который делится на две итерации.
# django.core.management
django.setup().
Configure settings
Первая — это чтение конфигов:
import django.conf.global_settings
import_module(os.environ["DJANGO_SETTINGS_MODULE"])
Читаются настройки по умолчанию
global_settings
, потом из переменной среды мы пытаемся найти модуль с DJANGO_SETTINGS_MODULE
, который написал сам пользователь. Эти настройки объединяются в один name space.Кто написал на Django хотя бы «Hello, world», знает, что там есть
INSTALLED_APPS
— где мы как раз пишем пользовательский код.Populate apps
Во второй части все эти applications, по сути пакеты, итерируем по одному. Создаем для каждого Config, импортируем модели для работы с базой данных и проверяем модели на целостность. Дальше фреймворк отрабатывает
Check
, то есть проверяет, что у каждой модели есть primary key, все foreign key указывают на существующие поля и что в BooleanField не написано поле Null, а используется NullBooleanField.for entry in settings.INSTALLED_APPS:
cfg = AppConfig.create(entry)
cfg.import_models()
Это минимальный sanity check для моделей, для админки, для чего угодно — без подключения к базе, без чего-то сверхсложного и специфичного. На этой стадии Django еще не знает, какую команду вы попросили исполнить, то есть не отличает
migrate
от runserver
или shell
.Дальше мы попадаем в модуль, который пытается угадать по аргументам командной строки, какую команду мы хотим исполнить и в каком приложении она лежит.
Management command
# django.core.management
subcommand = sys.argv[1]
app_name = find(pkgutils.iter_modules(settings.INSTALLED_APPS))
module = import_module(
'%s.management.commands.%s' % (app_name, subcommand)
)
cmd = module.Command()
cmd.run_from_argv(self.argv)
В данном случае в модуле runserver будет встроенный модуль
django.core.management.commands.runserver
. После импорта модуля, по convention внутри вызывается глобальный класс Command
, инстанцируется, и мы говорим: " Я тебя нашел, вот тебе аргументы командной строки, которые передал пользователь, сделай с ними что-нибудь".Дальше идем в модуль runserver и видим, что Django сделан из «regexp’ов и палок», про которые я буду сегодня подробно рассказывать:
# django.core.management.commands.runserver
naiveip_re = re.compile(r"""^(?:
(?P<addr>
(?P<ipv4>\d{1,3}(?:\.\d{1,3}){3}) | # IPv4 address
(?P<ipv6>\[[a-fA-F0-9:]+\]) | # IPv6 address
(?P<fqdn>[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*) # FQDN
):)?(?P<port>\d+)$""", re.X)
Commands
Скроллим вниз на полтора экрана — наконец попадаем в определение нашей команды, которая запускает сервер.
# django.core.management.commands.runserver
class Command(BaseCommand):
def handle(self, *args, **options):
httpd = WSGIServer(*args, **options)
handler = WSGIHandler()
httpd.set_app(handler)
httpd.serve_forever()
BaseCommand
проводит минимальный набор операций, чтобы аргументы командной строки привести к аргументам вызова функции *args
и **options
. Мы видим, что здесь создается инстанс WSGI-сервера, в этот WSGI-сервер устанавливается глобальный WSGIHandler — это как раз и есть God Object Django. Можно сказать, что это единственный инстанс фреймворка. На сервер инстанс устанавливается глобально — через set application
и говорит: «Крутись в Event Loop, исполняй запросы».Всегда где-то есть Event Loop и программист, который ему дает задачи.
WSGI server
Что же такое WSGIHandler? WSGI — это интерфейс, который позволяет обрабатывать HTTP-запросы с минимальным уровнем абстракции, и выглядит, как нечто в виде функции.
WSGI handler
# django.core.handlers.wsgi
class WSGIHandler:
def __call__(self, environ, start_response):
signals.request_started.send()
request = WSGIRequest(environ)
response = self.get_response(request)
start_response(response.status, response.headers)
return response
Например, здесь это экземпляр класса, у которого определен
call
. Он ждет к себе на вход dictionary, в котором уже в виде байтов и файл-handler будут представлены headers. Handler нужен, чтобы прочитать <body>
у запроса. Также сам сервер дает callback start_response
, чтобы мы могли одной пачкой отослать response.headers
и его заголовок, например, status.Дальше мы можем через объект response передавать тело response в сервер. Response — это генератор, по которому можно итерироваться.
Все сервера, которые написаны для WSGI — Gunicorn, uWSGI, Waitress, работают по этому интерфейсу и взаимозаменяемы. Мы сейчас рассматриваем сервер для девелопмента, но любой сервер приходит к тому, что в Django он стучится через environ и callback.
Что внутри God Object?
Что происходит внутри этой глобальной функции God Object внутри Django?
- REQUEST.
- MIDDLEWARES.
- ROUTING запроса на view.
- VIEW — обработка пользовательского кода внутри view.
- FORM — работа с формами.
- ORM.
- TEMPLATE.
- RESPONSE.
Вся машинерия, которую мы хотим от Django, происходит внутри одной функции, которая размазана на весь фреймворк.
Request
Оборачиваем environment WSGI, который есть простой dictionary, в какой-то специальный объект, для удобства работы с environment. Например, узнать длину пользовательского запроса удобнее через работу с чем-то похожим на dictionary, чем с байт-строкой, которую нужно парсить и искать в ней вхождения ключ-значение. При работе с куками, тоже не хочется вычислять вручную — истек срок хранения или нет, и как-то это интерпретировать.
# django.core.handlers.wsgi
class WSGIRequest(HttpRequest):
@cached_property
def GET(self):
return QueryDict(self.environ['QUERY_STRING'])
@property
def POST(self):
self._load_post_and_files()
return self._post
@cached_property
def COOKIES(self):
return parse_cookie(self.environ['HTTP_COOKIE'])
Request содержит парсеры, а также набор handlers для управления обработкой тела POST-запроса: будет ли это файл в памяти или временный в хранилище на диске. Все решается внутри Request. Также Request в Django — это объект-агрегатор, в который все middlewares могут поместить необходимую нам информацию про сессию, аутентификацию и авторизацию пользователя. Можно сказать, что это тоже God Object, но поменьше.
Дальше Request попадает в middleware.
Middlewares
Middleware — это обертка, которая оборачивает другие функции как декоратор. Перед тем как отдать контроль middleware, в методе call мы отдаем response или вызываем уже оборачиваемую middleware.
Так выглядит middleware с точки зрения программиста.
Settings
# settings.py
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
]
Define
class Middleware:
def __init__(self, get_response=None):
self.get_response = get_response
def __call__(self, request):
return self.get_response(request)
С точки зрения Django, middlewares выглядят как своеобразный стек:
# django.core.handlers.base
def load_middleware(self):
handler = convert_exception_to_response(self._get_response)
for middleware_path in reversed(settings.MIDDLEWARE):
middleware = import_string(middleware_path)
instance = middleware(handler)
handler = convert_exception_to_response(instance)
self._middleware_chain = handler
Apply
def get_response(self, request):
set_urlconf(settings.ROOT_URLCONF)
response = self._middleware_chain(request)
return response
Берем изначальную функцию
get_response
, оборачиваем ее handler, который будет переводить, например, permission error
и not found error
в корректный HTTP-код. Всё оборачиваем в саму middleware из списка. Стек middlewares растет, и каждая следующая оборачивает предыдущую. Это очень похоже на применение одного и того же стека декораторов ко всем view в проекте, только централизованно. Не надо ходить и расставлять обертки руками по проекту, всё удобно и логично.Мы прошли 7 кругов middlewares, наш request выжил и решил обрабатывать это во view. Дальше мы попадаем в модуль routing.
Routing
Это то, где мы решаем, какой handler вызвать для какого-то конкретного запроса. А решается это:
- на основании url;
- в спецификации WSGI, где называется request.path_info.
# django.core.handlers.base
def _get_response(self, request):
resolver = get_resolver()
view, args, kwargs = resolver.resolve(request.path_info)
response = view(request, *args, **kwargs)
return response
Urls
Берем резольвер, скармливаем ему текущий url запроса и ожидаем, что он вернет саму функцию view, и из этого же url достанет аргументы, с которыми надо вызвать view. Дальше
get_response
вызывает view, обрабатывает исключения и что-то с этим делает.# urls.py
urlpatterns = [
path('articles/2003/', views.special_case_2003),
path('articles/<int:year>/', views.year_archive),
path('articles/<int:year>/<int:month>/', views.month_archive)
]
Resolver
Так выглядит резольвер:
# django.urls.resolvers
_PATH_RE = re.compile(
r'<(?:(?P<converter>[^>:]+):)?(?P<parameter>\w+)>'
)
def resolve(self, path):
for pattern in self.url_patterns:
match = pattern.search(path)
if match:
return ResolverMatch(
self.resolve(match[0])
)
raise Resolver404({'path': path})
Это тоже regexp, но рекурсивный. Он идет по частям url, ищет то, что хочет пользователь: других пользователей, посты, блоги, либо это какой-то конвертер, например, конкретный год, который нужно вычленить, положить в аргументы, привести к int.
Характерно, что глубина рекурсии метода resolve всегда равна количеству аргументов, с которым вызывается view. Если что-то пошло не так и мы не нашли конкретный url, возникает not found error.
Дальше мы наконец попадаем во view — в код, который написал программист.
View
В самом простом представлении — это функция, которая возвращает request от response, но внутри у нее мы выполняем логические задачи: «за, если, когда-нибудь» — много повторяющихся задач. Django нам предоставляет class based view, где можно указать конкретные детали, и все поведение будет интерпретировано в правильном формате уже самим классом.
# django.views.generic.edit
class ContactView(FormView):
template_name = 'contact.html'
form_class = ContactForm
success_url = '/thanks/'
Method flowchart
self.dispatch()
self.post()
self.get_form()
self.form_valid()
self.render_to_response()
Метод
dispatch
этого инстанса лежит уже в url mapping вместо функции. Dispatch на основании HTTP verb понимает, какой метод вызвать: к нам пришел POST и мы, скорее всего, хотим инстанцировать объект form, если form валиден, сохранить его в базу и показать шаблон. Это все делается через большое количество миксин, из которых состоит этот класс.Form
Форма перед тем, как попадет в представление Django, должна быть прочитана из сокета — через тот самый файловый handler, который лежит в WSGI-environment. form-data представляет из себя byte stream, в котором описаны разделители — эти блоки мы можем прочитать и что-то из них сделать. Это может быть соответствие ключ-значение, если это поле, часть файла, потом снова какое-то поле — всё смешано.
Content-Type: multipart/form-data;boundary="boundary"
--boundary
name="field1"
value1
--boundary
name="field2";
value2
Parser
Парсер состоит из 3 частей.
Chunk-итератор, который из byte stream создает ожидаемые чтения — превращает в итератор, который может выдавать
boundaries
. Он гарантирует, что если что-то и вернет, то это будет boundary. Это нужно, чтобы внутри парсера не надо было хранить состояние коннекта, читать из сокета или не читать, чтобы минимизировать логику обработки данных.Дальше генератор оборачивается в LazyStream, который создает из него снова файл object, но с ожидаемым чтением. Так парсер уже может ходить по кусочкам байтов и строить из них ключ-значение.
field и data здесь всегда будет являться строками. Если к нам пришла datatime в ISO-формате, уже Django-форма (которая написана программистом) с помощью определенных полей получит, например, timestamp.
# django.http.multipartparser
self._post = QueryDict(mutable=True)
stream = LazyStream(ChunkIter(self._input_data))
for field, data in Parser(stream):
self._post.append(field, force_text(data))
Дальше форма, скорее всего, захочет сохранить себя в базу данных, и здесь начинается Django ORM.
ORM
Примерно через такой DSL выполняются запросы на ORM:
# models.py
Entry.objects.exclude(
pub_date__gt=date(2005, 1, 3),
headline='Hello',
)
С помощью ключей можно собирать подобные SQL-выражения:
SELECT * WHERE NOT (pub_date > '2005-1-3' AND headline = 'Hello')
Как это происходит?
Queryset
У метода
exclude
под капотом есть объект Query
. Объекту в функцию передают аргументы, и он создает иерархию объектов, каждый из которых может превратить себя в отдельный кусочек SQL-запроса в виде строки.При обходе дерева, каждый из участков опрашивает свои дочерние ноды, получает вложенные SQL-запросы, и в результате мы сможем построить SQL, как строку. Например, ключ-значение будет не отдельным SQL-полем, а будет сравниваться с value-значением. Так же работает и конкатенация, и отрицание запросов — рекурсивным обходом по дереву, у каждой ноды которого вызывается каст к SQL.
# django.db.models.query
sql.Query(Entry).where.add(
~Q(
Q(F('pub_date') > date(2005, 1, 3)) &
Q(headline='Hello')
)
)
Compiler
# django.db.models.expressions
class Q(tree.Node):
AND = 'AND'
OR = 'OR'
def as_sql(self, compiler, connection):
return self.template % self.field.get_lookup('gt')
Output
>>> Q(headline='Hello')
# headline = 'Hello'
>>> F('pub_date')
# pub_date
>>> F('pub_date') > date(2005, 1, 3)
# pub_date > '2005-1-3'
>>> Q(...) & Q(...)
# ... AND ...
>>> ~Q(...)
# NOT …
В этот метод передается небольшой helper-compiler, который может отличить диалект MySQL от PostgreSQL и правильно расставить синтаксический сахар, который используется в диалекте конкретной базы данных.
DB routing
Когда мы получили SQL-запрос, модель стучится в DB routing и спрашивает, в какой базе данных она лежит. В 99% случаев это будет база данных default, в оставшемся 1% — какая-то своя.
# django.db.utils
class ConnectionRouter:
def db_for_read(self, model, **hints):
if model._meta.app_label == 'auth':
return 'auth_db'
Обертка над драйвером баз данных из специфичного интерфейса библиотеки, таких как Python MySQL или Psycopg2, создает универсальный объект, с которым Django может работать. Есть своя обертка для курсоров, своя обертка для транзакций.
Connecting pool
# django.db.backends.base.base
class BaseDatabaseWrapper:
def commit(self):
self.validate_thread_sharing()
self.validate_no_atomic_block()
with self.wrap_database_errors:
return self.connection.commit()
В этом конкретном connection мы отправляем запросы в сокет, который стучится в БД, и ждем выполнения. Обертка над библиотекой будет читать уже человеческий ответ от БД в виде записи, и Django из этих данных в Python типах собирает инстанс модели. Это не сложная итерация.
Мы что-то записали в базу, что-то прочитали и решили сказать об этом пользователю с помощью HTML-странички. Для этого у Django есть нелюбимый сообществом язык шаблонов, который выглядит, как что-то похожее на язык программирования, только в HTML-файле.
Template
from django.template.loader import render_to_string
render_to_string('my_template.html', {'entries': ...})
Code
<ul>
{% for entry in entries %}
<li>{{ entry.name }}</li>
{% endfor %}
</ul>
Parser
# django.template.base
BLOCK_TAG_START = '{%'
BLOCK_TAG_END = '%}'
VARIABLE_TAG_START = '{{'
VARIABLE_TAG_END = '}}'
COMMENT_TAG_START = '{#'
COMMENT_TAG_END = '#}'
tag_re = (re.compile('(%s.*?%s|%s.*?%s|%s.*?%s)' %
(re.escape(BLOCK_TAG_START),
re.escape(BLOCK_TAG_END),
re.escape(VARIABLE_TAG_START),
re.escape(VARIABLE_TAG_END),
re.escape(COMMENT_TAG_START),
re.escape(COMMENT_TAG_END))))
Сюрприз — опять regexp. Только в конце должна быть запятая, и список продолжится далеко вниз. Наверное, это самый сложный regexp, который я видел в этом проекте.
Lexer
Обработчик шаблона и интерпретатор устроен довольно просто. Есть lexer, который с помощью regexp переводит текст в список маленьких токенов.
# django.template.base
def tokenize(self):
for bit in tag_re.split(template_string):
lineno += bit.count('\n')
yield bit
Итерируемся по списку токенов, смотрим: «Ты кто? Обернем тебя в тэг-ноду». Например, если это старт какого-то
if
или for
или for
, тэг-нода возьмет соответствующий обработчик. Сам же обработчик for
опять говорит парсеру: «Прочитай мне список токенов вплоть до закрывающего тэга».Операция опять идет в парсер.
Нода, тэг и парсер — это взаимно рекурсивные вещи, и глубина рекурсии обычно равна вложенности самого шаблона по тэгам.
Parser
def parse():
while tokens:
token = tokens.pop()
if token.startswith(BLOCK_TAG_START):
yield TagNode(token)
elif token.startswith(VARIABLE_TAG_START):
...
Обработчик тэга дает нам конкретную ноду, например, с циклом
for
, у которой появляется метод render
.For loop
# django.template.defaulttags
@register.tag('for')
def do_for(parser, token):
args = token.split_contents()
body = parser.parse(until=['endfor'])
return ForNode(args, body)
For node
class ForNode(Node):
def render(self, context):
with context.push():
for i in self.args:
yield self.body.render(context)
Метод
render
представляет из себя render-дерево. Каждая верхняя нода может пойти в дочернюю, попросить ее отрендериться. Программисты привыкли, что показываются какие-то переменные в этом шаблоне. Это делается через context
— он представлен в виде обычного словарика. Это стек словарей для эмулирования области видимости, когда мы входим внутрь тэга. Например, если внутри цикла for
сам context
поменяет какой-то другой тэг, то, когда мы выйдем из цикла — изменения откатятся. Это удобно, потому что когда все глобально, работать тяжело.Response
Наконец-то мы получили нашу строку с HTTP-response:
Hello, World!
Мы можем отдавать строку пользователю.
- Возвращаем этот response из view.
- View отдает в список middlewares.
- Middlewares этот response модифицируют, дополняют и улучшают.
- Response начинает итерироваться внутри WSGIHandler, частично записывается в сокет, и браузер получает ответ нашего сервера.
Все известные стартапы, которые были написаны на Django, например, Bitbucket или Instagram, начинались с такого небольшого цикла, который проходил каждый программист.
Все это, и выступление на Moscow Python Conf++ нужно, чтобы вы лучше понимали, что находится у вас в руках и как этим пользоваться. В любой магии есть большая часть regexp, которые надо уметь готовить.
Артём Малышев и еще 23 отличных спикера 5 апреля снова дадут нам много пищи для размышления и дискуссий на тему Python на конференции Moscow Python Conf ++. Изучайте расписание и присоединяйтесь к обмену опытом решения самых разных задач с использованием Python.