Django under microscope

    Если по докладу Артёма Малышева (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. Тогда она выглядела страшно и уныло.



    Увидели, что 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.
    Конференции Олега Бунина (Онтико)
    711,00
    Конференции Олега Бунина
    Поддержать автора
    Поделиться публикацией

    Похожие публикации

    Комментарии 17

      0
      Ощущение что это очень-очень-очень краткий обзор какой-то (Вашей) книги. Ссылку бы на неё. Если нет, то пишите книгу)
        +1
        До книги ещё очень-очень далеко :)
        +2
        Я плохо разбираюсь в современной веб-разработке. Можете сказать, почему Django на Хабре критикуют, пишут что он сильно устарел по сравнению с якобы современными фреймворками и не годится для масштабируемых highload систем? И как вы к этому относитесь?
        Я достаточно давно ковырял Rails, на тот момент они были сходи концепцией с Django — MVC, и др.
          0
          В тред призывается proofit404
            +5
            Тяжело будет оправдывать Django без конкретных примеров критики, но я попробую предположить.

            1. Django это не про SPA.

            Сейчас наверное большая часть приложений пишется с каким-нибудь frontend фреймворком. Например, Angular, React или Vue. Django по умолчанию предлагает нам относительно медленный язык шаблонов, которые обрабатываются на сервере. Во-первых, можно взять Django Rest Framework (отдельный пакет) и Django будет отличным API сервисом backend для вашего SPA приложения. Во-вторых, если шаблонов на сервере достаточно для сложности приложения, но не устраивает скорость, можно взять Jinja2, который поддерживается из коробки. Он быстрый в силу компиляции в python bytecode.

            2. Django это не про WebSockets.

            Опять же огромное количество приложений хотят живой контент без постоянного хождения на сервер по HTTP. Уже несколько лет Django поддерживает Websockets и HTTP2 протоколы с помощью официального расширения Django Channels.

            3. Django плохо масштабируется.

            На самом деле у Django есть db router, который позволяет горизонтально масштабировать базу данных. Например, пользователей от А до Е и всё что с ними связано мы будет хранить на одном сервере, остальных на другом. У нас есть программный интерфейс чтобы этим самим управлять. Тоже самое есть для слоя кэширования.

            Из хорошего от себя могу сказать что это фреймворк с адекватным project management. Есть LTS версии. Исправления безопасности выходят для нескольких актуальных версий сразу. Есть ясный и понятный процесс принятия нововведений Django Enhancement Proporans.

            Надеюсь сумел угадать мифы.
              –2
              4) Джанго с более-менее сложным набором моделей стартует до двух секунд.
                0
                Java машина безотносительно любого фреймворка стартует сильно дольше. Плохой платформой это её ещё не делает. Можно где-то посмотреть сравнение времени старта с SQLAlchemy, Peewee и Orator на том же количестве моделей? О каком наборе моделей вообще идёт речь?
                  –2
                  Ну так джава ж никогда и не позиционировалася как скриптовый язык.

                  Насколько я понял после профилирования, оно больше зависит от количества зависимостей между классами.

                  Ну вот последний проект. Полторы тысячи строк у меня в моделях всего. 81 класс. Время старта простого скрипта с django.setup() от 0.855 до 1.100. Проект нельзя считать особо крупным.
                –1

                Django разрабатывался как фреймворк для новостных сайтов, еще в те времена, когда основная логика фронтенда жила на сервере и все писали синхронный код. Scaffolding, шаблонизация, RSS, простейшее управление асетами. Все это было прям круто и актуально.


                Современный веб живет на клиенте и фронтендные технологии драйваются JS сообществом. Здесь же server side rendering и изоморфный код. Какие-то решения портируются на питон, но инноваций не приносят и погоды не делают. Поэтому толку от джанги чуть, решение для фанатов.

                  0
                  Говорите как фанат чего-то другого. Все фронтенд технологии, кроме изоморфного подхода, прекрасно будут жить рядом с Django. Дельные инновации как раз таки портируются не только на питон и погоду делают в целом везде.
                  0
                  1. DRF
                  2. Channels
                  3. Если представления без состояния, то django-проект прекрасно вписывается в "12 факторов" и хорошо размазывается в кластер.
                    0
                    Я примерно так и написал.
                  0
                  Критиковать фреймворк за то, что он не подходит для highload — это как критиковать семейный минивен за то, что он не болид F1 или не карьерный самосвал. В подавляющем большинстве случаев проекты не являются высоконагруженными и для них самым главным фактором является простота разработки и сопровождения.
                    0
                    Прекрасно подходит для hi-load.
                    Не весь hi-load является именно web high-load`ом.
                  0
                  А есть хорошие книги по джанго? Желательно не слишком начального уровня. Документацию на оф сайте не предлагать)

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое