Какова цель контекстных стеков Flask? - программирование
Подтвердить что ты не робот

Какова цель контекстных стеков Flask?

Я использую контекст запроса/приложения в течение некоторого времени, не до конца понимая, как он работает, или почему он был разработан так, как он был. Какова цель "стека", когда дело доходит до контекста запроса или приложения? Являются ли эти два отдельных стека или они оба входят в один стек? Является ли контекст запроса нажатым в стек, или это сам стек? Могу ли я нажимать/нажимать несколько контекстов поверх каждого другого? Если да, то зачем мне это делать?

Извините за все вопросы, но я все еще запутался после прочтения документации для контекста запроса и контекста приложения.

4b9b3361

Ответ 1

Несколько приложений

Контекст приложения (и его назначение) действительно запутывает, пока вы не поймете, что Flask может иметь несколько приложений. Представьте себе ситуацию, когда вы хотите, чтобы один интерпретатор WSGI Python запускал несколько приложений Flask. Мы здесь не говорим о чертежах, мы говорим о совершенно разных приложениях Flask.

Вы можете установить это аналогично в разделе документации к фляге на примере "Диспетчер приложений" :

from werkzeug.wsgi import DispatcherMiddleware
from frontend_app import application as frontend
from backend_app import application as backend

application = DispatcherMiddleware(frontend, {
    '/backend':     backend
})

Обратите внимание, что существует два совершенно разных приложения для фляг, которые создаются "frontend" и "backend". Другими словами, конструктор приложения Flask(...) был вызван дважды, создав два экземпляра приложения Flask.

контексты

Когда вы работаете с Flask, вы часто получаете глобальные переменные для доступа к различным функциям. Например, у вас, вероятно, есть код, который читает...

from flask import request

Затем во время просмотра вы можете использовать request для доступа к информации текущего запроса. Очевидно, что request не является нормальной глобальной переменной; в действительности это контекстное локальное значение. Другими словами, существует некоторая магия за кулисами, в которой говорится: "Когда я вызываю request.path, получаю атрибут path от объекта request запроса CURRENT". Два разных запроса будут иметь разные результаты для request.path.

На самом деле, даже если вы запускаете Flask с несколькими потоками, Flask достаточно умен, чтобы изолировать объекты запроса. При этом становится возможным, чтобы два потока, каждый из которых обрабатывал другой запрос, одновременно вызывали request.path и получали правильную информацию для своих соответствующих запросов.

Совместное использование

Итак, мы уже видели, что Flask может обрабатывать несколько приложений в одном и том же интерпретаторе, а также из-за того, что Flask позволяет использовать "контекстные локальные" глобальные переменные, должен быть некоторый механизм для определения того, что означает "текущий", запрос (для выполнения таких действий, как request.path).

Объединяя эти идеи вместе, должно также иметь смысл, что Flask должен каким-то образом определить, что такое "текущее" приложение!

Вероятно, у вас также есть код, похожий на следующий:

from flask import url_for

Как и наш пример request, функция url_for имеет логику, зависящую от текущей среды. В этом случае, однако, очевидно, что логика сильно зависит от того, какое приложение считается "текущим" приложением. В примере с интерфейсом/бэкэнд, показанным выше, оба приложения "frontend" и "backend" могут иметь маршрут "/login", поэтому url_for('/login') должен возвращать что-то другое в зависимости от того, обрабатывается ли представление запроса для интерфейса или backend.

Чтобы ответить на ваши вопросы...

Какова цель "стека", когда дело доходит до запроса или контекст приложения?

В документах контекста запроса:

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

Другими словами, даже если у вас обычно есть 0 или 1 элемент в этом стеке "текущих" запросов или "текущих" приложений, возможно, что у вас может быть больше.

Приведенный пример - это то, где у вас будет запрос на возврат результатов внутреннего переадресации. Скажем, пользователь запрашивает A, но вы хотите вернуться к пользователю B. В большинстве случаев вы отправляете перенаправление пользователю и указываете пользователю на ресурс B, то есть пользователь будет запускать второй запрос для извлечения B. A немного другим способом обработки этого было бы сделать внутреннюю переадресацию, что означает, что при обработке A Flask сделает новый запрос для себя для ресурса B и использует результаты этого второго запроса в качестве результатов исходного запроса пользователя.

Являются ли эти два отдельных стека или они являются частью одного стека?

Они два отдельных стека. Однако это детализация реализации. Что еще важнее не столько в том, что есть стек, сколько в том, что в любое время вы можете получить "текущее" приложение или запрос (верхняя часть стека).

Является ли контекст запроса нажатым в стек или сам стек?

"Контекст запроса" - это один элемент "стека контекста запроса". Аналогично "контекст приложения" и "стек контекста приложения".

Можно ли нажимать/всплывать несколько контекстов на вершине eachother? Если так, почему я хочу это сделать?

В приложении Flask вы обычно этого не делали. Одним из примеров того, где вы, возможно, захотите, является внутренняя переадресация (описанная выше). Однако даже в этом случае вы, вероятно, закончите тем, что Flask обработает новый запрос, и поэтому Flask сделает все, что вам нужно.

Однако есть некоторые случаи, когда вы хотите сами манипулировать стеком.

Запуск кода за пределами запроса

Одна типичная проблема, с которой люди сталкиваются, заключается в том, что они используют расширение Flask-SQLAlchemy для настройки базы данных SQL и определения модели с использованием кода, похожего на то, что показано ниже...

app = Flask(__name__)
db = SQLAlchemy() # Initialize the Flask-SQLAlchemy extension object
db.init_app(app)

Затем они используют значения app и db в script, которые должны запускаться из оболочки. Например, "setup_tables.py" script...

from myapp import app, db

# Set up models
db.create_all()

В этом случае расширение Flask-SQLAlchemy знает о приложении app, но во время create_all() оно будет вызывать ошибку, жалуясь на отсутствие контекста приложения. Эта ошибка оправдана; вы никогда не говорили Flask, с каким приложением он должен работать при запуске метода create_all.

Возможно, вам интересно, почему вы не нуждаетесь в этом вызове with app.app_context(), когда вы запускаете подобные функции в своих представлениях. Причина в том, что Flask уже обрабатывает управление контекстом приложения для вас, когда он обрабатывает фактические веб-запросы. Проблема действительно возникает только вне этих функций просмотра (или других таких обратных вызовов), например, при использовании ваших моделей в одноразовой script.

Резолюция заключается в том, чтобы самостоятельно нажать контекст приложения, что можно сделать, выполнив...

from myapp import app, db

# Set up models
with app.app_context():
    db.create_all()

Это вызовет новый контекст приложения (с помощью приложения app, помните, что может быть более одного приложения).

Тестирование

Другой случай, когда вы хотите манипулировать стеком, - это тестирование. Вы можете создать unit test, который обрабатывает запрос, и вы проверяете результаты:

import unittest
from flask import request

class MyTest(unittest.TestCase):
    def test_thing(self):
        with app.test_request_context('/?next=http://example.com/') as ctx:
            # You can now view attributes on request context stack by using `request`.

        # Now the request context stack is empty

Ответ 2

Предыдущие ответы уже дают хороший обзор того, что происходит на фоне Flask во время запроса. Если вы еще не прочитали это, я рекомендую ответить @MarkHildreth перед тем, как прочитать это. Короче говоря, для каждого HTTP-запроса создается новый контекст (поток), поэтому для него необходимо иметь средство потока Local, которое позволяет доступным объектам, таким как request и g, глобально через потоки, при сохранении их запрос конкретного контекста. Кроме того, при обработке запроса HTTP Flask может эмулировать дополнительные запросы изнутри, следовательно, необходимо сохранить их соответствующий контекст в стеке. Кроме того, Flask позволяет нескольким приложениям wsgi работать друг с другом в рамках одного процесса, и более чем один может быть вызван к действию во время запроса (каждый запрос создает новый контекст приложения), следовательно, потребность в стеке контекста для приложений. Это резюме того, что было рассмотрено в предыдущих ответах.

Моя цель состоит в том, чтобы дополнить наше нынешнее понимание, объяснив, как Flask и Werkzeug делают то, что они делают с этими местными жителями контекста. Я упростил код, чтобы улучшить понимание его логики, но если вы его получите, вы сможете легко понять, что из реального источника (werkzeug.local и flask.globals).

Позвольте сначала понять, как Werkzeug реализует локаторы потоков.

Местные

Когда приходит HTTP-запрос, он обрабатывается в контексте одного потока. В качестве альтернативного средства для создания нового контекста во время HTTP-запроса Werkzeug также позволяет использовать зелья (своего рода более легкие "микропотоки" ) вместо обычных потоков. Если у вас нет установленных зеленых, вместо этого они вернутся к использованию потоков. Каждый из этих потоков (или зеленых) идентифицируется уникальным идентификатором, который вы можете получить с помощью функции модуля get_ident(). Эта функция является отправной точкой для магии за request, current_app, url_for, g и другими такими глобальными объектами, связанными с контекстом.

try:
    from greenlet import get_ident
except ImportError:
    from thread import get_ident

Теперь, когда у нас есть наша функция идентификации, мы можем знать, к какому потоку мы подключаемся в любой момент времени, и мы можем создать то, что называется потоком Local, контекстным объектом, к которому можно получить доступ во всем мире, но когда вы получаете доступ к его атрибутам они решают их значение для этого конкретного потока. например.

# globally
local = Local()

# ...

# on thread 1
local.first_name = 'John'

# ...

# on thread 2
local.first_name = 'Debbie'

Оба значения присутствуют на глобально доступном объекте Local в то же время, но доступ к local.first_name в контексте потока 1 даст вам 'John', тогда как он вернет 'Debbie' в поток 2.

Как это возможно? Посмотрим на некоторый (упрощенный) код:

class Local(object)
    def __init__(self):
        self.storage = {}

    def __getattr__(self, name):
        context_id = get_ident() # we get the current thread or greenlet id
        contextual_storage = self.storage.setdefault(context_id, {})
        try:
            return contextual_storage[name]
        except KeyError:
            raise AttributeError(name)

    def __setattr__(self, name, value):
        context_id = get_ident()
        contextual_storage = self.storage.setdefault(context_id, {})
        contextual_storage[name] = value

    def __release_local__(self):
        context_id = get_ident()
        self.storage.pop(context_id, None)

local = Local()

Из приведенного выше кода видно, что магия сводится к get_ident(), которая идентифицирует текущую зеленую или нить. Хранилище Local затем просто использует это как ключ для хранения любых данных, контекстуальных для текущего потока.

У вас может быть несколько объектов Local для каждого процесса и request, g, current_app, а другие могут быть просто созданы таким образом. Но это не то, как это делается в Flask, в котором они не являются технически Local объектами, а точнее LocalProxy объектами. Что a LocalProxy?

LocalProxy

LocalProxy - это объект, который запрашивает Local, чтобы найти другой объект, представляющий интерес (т.е. объект, к которому он относится). Давайте взглянем, чтобы понять:

class LocalProxy(object):
    def __init__(self, local, name):
        # `local` here is either an actual `Local` object, that can be used
        # to find the object of interest, here identified by `name`, or it's
        # a callable that can resolve to that proxied object
        self.local = local
        # `name` is an identifier that will be passed to the local to find the
        # object of interest.
        self.name = name

    def _get_current_object(self):
        # if `self.local` is truly a `Local` it means that it implements
        # the `__release_local__()` method which, as its name implies, is
        # normally used to release the local. We simply look for it here
        # to identify which is actually a Local and which is rather just
        # a callable:
        if hasattr(self.local, '__release_local__'):
            try:
                return getattr(self.local, self.name)
            except AttributeError:
                raise RuntimeError('no object bound to %s' % self.name)

        # if self.local is not actually a Local it must be a callable that 
        # would resolve to the object of interest.
        return self.local(self.name)

    # Now for the LocalProxy to perform its intended duties i.e. proxying 
    # to an underlying object located somewhere in a Local, we turn all magic
    # methods into proxies for the same methods in the object of interest.
    @property
    def __dict__(self):
        try:
            return self._get_current_object().__dict__
        except RuntimeError:
            raise AttributeError('__dict__')

    def __repr__(self):
        try:
            return repr(self._get_current_object())
        except RuntimeError:
            return '<%s unbound>' % self.__class__.__name__

    def __bool__(self):
        try:
            return bool(self._get_current_object())
        except RuntimeError:
            return False

    # ... etc etc ... 

    def __getattr__(self, name):
        if name == '__members__':
            return dir(self._get_current_object())
        return getattr(self._get_current_object(), name)

    def __setitem__(self, key, value):
        self._get_current_object()[key] = value

    def __delitem__(self, key):
        del self._get_current_object()[key]

    # ... and so on ...

    __setattr__ = lambda x, n, v: setattr(x._get_current_object(), n, v)
    __delattr__ = lambda x, n: delattr(x._get_current_object(), n)
    __str__ = lambda x: str(x._get_current_object())
    __lt__ = lambda x, o: x._get_current_object() < o
    __le__ = lambda x, o: x._get_current_object() <= o
    __eq__ = lambda x, o: x._get_current_object() == o

    # ... and so forth ...

Теперь, чтобы создать глобально доступные прокси, вы бы сделали

# this would happen some time near application start-up
local = Local()
request = LocalProxy(local, 'request')
g = LocalProxy(local, 'g')

и теперь через некоторое время в течение некоторого времени запроса вы будете хранить некоторые объекты внутри локального объекта, к которым могут иметь доступ ранее созданные прокси, независимо от того, в каком потоке мы находимся

# this would happen early during processing of an http request
local.request = RequestContext(http_environment)
local.g = SomeGeneralPurposeContainer()

Преимущество использования LocalProxy в качестве объектов, доступных по всему миру, а не для их самих Locals заключается в том, что он упрощает их управление. Вам просто нужен только один объект Local для создания многих доступных в глобальном масштабе прокси. В конце запроса во время очистки вы просто освобождаете один Local (т.е. Вы выталкиваете контекст_ид из своего хранилища) и не беспокоитесь о прокси-серверах, они по-прежнему доступны по всему миру и все еще откладывают на один Local, чтобы найти объект, представляющий интерес для последующих HTTP-запросов.

# this would happen some time near the end of request processing
release(local) # aka local.__release_local__()

Чтобы упростить создание LocalProxy, когда мы уже имеем Local, Werkzeug реализует магический метод Local.__call__() следующим образом:

class Local(object):
    # ... 
    # ... all same stuff as before go here ...
    # ... 

    def __call__(self, name):
        return LocalProxy(self, name)

# now you can do
local = Local()
request = local('request')
g = local('g')

Однако, если вы посмотрите в источнике Flask (flask.globals), который все еще не работает, как request, g, current_app и session. Как мы установили, Flask может порождать несколько "поддельных" запросов (из одного истинного HTTP-запроса) и в этом процессе также нажимать несколько контекстов приложения. Это не обычный случай использования, но это возможность структуры. Поскольку эти "параллельные" запросы и приложения по-прежнему ограничены для запуска только с одним "фокусом" в любое время, имеет смысл использовать стек для соответствующего контекста. Всякий раз, когда генерируется новый запрос или вызывается одно из приложений, они нажимают свой контекст в верхней части своего соответствующего стека. Для этой цели Flask использует объекты LocalStack. Когда они завершают свой бизнес, они выталкивают контекст из стека.

LocalStack

Это выглядит как LocalStack (опять-таки код упрощен, чтобы облегчить понимание его логики).

class LocalStack(object):

    def __init__(self):
        self.local = Local()

    def push(self, obj):
        """Pushes a new item to the stack"""
        rv = getattr(self.local, 'stack', None)
        if rv is None:
            self.local.stack = rv = []
        rv.append(obj)
        return rv

    def pop(self):
        """Removes the topmost item from the stack, will return the
        old value or `None` if the stack was already empty.
        """
        stack = getattr(self.local, 'stack', None)
        if stack is None:
            return None
        elif len(stack) == 1:
            release_local(self.local) # this simply releases the local
            return stack[-1]
        else:
            return stack.pop()

    @property
    def top(self):
        """The topmost item on the stack.  If the stack is empty,
        `None` is returned.
        """
        try:
            return self.local.stack[-1]
        except (AttributeError, IndexError):
            return None

Обратите внимание, что a LocalStack - это стек, хранящийся в локальной, а не в кучке локалей, хранящихся в стеке. Это означает, что, хотя стек глобально доступен, он представляет собой другой стек в каждом потоке.

Флажок не имеет объектов request, current_app, g и session, разрешающих непосредственно к LocalStack, он скорее использует объекты LocalProxy, которые обертывают функцию поиска (вместо Local), который найдет базовый объект из LocalStack:

_request_ctx_stack = LocalStack()
def _find_request():
    top = _request_ctx_stack.top
    if top is None:
        raise RuntimeError('working outside of request context')
    return top.request
request = LocalProxy(_find_request)

def _find_session():
    top = _request_ctx_stack.top
    if top is None:
        raise RuntimeError('working outside of request context')
    return top.session
session = LocalProxy(_find_session)

_app_ctx_stack = LocalStack()
def _find_g():
    top = _app_ctx_stack.top
    if top is None:
        raise RuntimeError('working outside of application context')
    return top.g
g = LocalProxy(_find_g)

def _find_app():
    top = _app_ctx_stack.top
    if top is None:
        raise RuntimeError('working outside of application context')
    return top.app
current_app = LocalProxy(_find_app)

Все они объявляются при запуске приложения, но фактически не разрешаются ничем, пока контекст запроса или контекст приложения не будут перенесены в их соответствующий стек.

Если вам интересно посмотреть, как контекст фактически вставлен в стек (а затем выскочил), посмотрите flask.app.Flask.wsgi_app(), который является точкой входа в приложение wsgi (то есть, что веб-сервер вызывает и передает среда http до того момента, когда приходит запрос), и следуйте за созданием объекта RequestContext через все последующие push() в _request_ctx_stack. После нажатия на верхнюю часть стека он доступен через _request_ctx_stack.top. Вот некоторый сокращенный код, демонстрирующий поток:

Итак, вы запускаете приложение и делаете его доступным для сервера WSGI...

app = Flask(*config, **kwconfig)

# ...

Позже приходит HTTP-запрос, и сервер WSGI вызывает приложение с обычными параметрами...

app(environ, start_response) # aka app.__call__(environ, start_response)

Это примерно то, что происходит в приложении...

def Flask(object):

    # ...

    def __call__(self, environ, start_response):
        return self.wsgi_app(environ, start_response)

    def wsgi_app(self, environ, start_response):
        ctx = RequestContext(self, environ)
        ctx.push()
        try:
            # process the request here
            # raise error if any
            # return Response
        finally:
            ctx.pop()

    # ...

и это примерно то, что происходит с RequestContext...

class RequestContext(object):

    def __init__(self, app, environ, request=None):
        self.app = app
        if request is None:
            request = app.request_class(environ)
        self.request = request
        self.url_adapter = app.create_url_adapter(self.request)
        self.session = self.app.open_session(self.request)
        if self.session is None:
            self.session = self.app.make_null_session()
        self.flashes = None

    def push(self):
        _request_ctx_stack.push(self)

    def pop(self):
        _request_ctx_stack.pop()

Скажем, что запрос завершил инициализацию, поиск request.path из одной из ваших функций просмотра будет следующим:

  • начать с глобального объекта LocalProxy request.
  • чтобы найти свой основной объект интереса (объект, который он проксирует), он вызывает свою функцию поиска _find_request() (функция, зарегистрированная как self.local).
  • эта функция запрашивает объект LocalStack _request_ctx_stack для верхнего контекста в стеке.
  • чтобы найти верхний контекст, объект LocalStack сначала запрашивает свой внутренний атрибут Local (self.local) для свойства stack, которое ранее было там сохранено.
  • из stack он получает верхний контекст
  • и top.request, таким образом, разрешены как основной объект, представляющий интерес.
  • из этого объекта мы получаем атрибут path

Итак, мы видели, как работают Local, LocalProxy и LocalStack, теперь задумайтесь на мгновение о последствиях и нюансах при извлечении path из:

  • a request объект, который будет простым глобально доступным объектом.
  • a request объект, который будет локальным.
  • a request объект, сохраненный как атрибут локального.
  • a request объект, который является прокси-сервером для объекта, хранящегося в локальном.
  • a request объект, хранящийся в стеке, который, в свою очередь, хранится в локальной сети.
  • a request объект, который является прокси-объектом для объекта в стеке, хранящегося в локальном. < - это то, что делает Flask.

Ответ 3

Маленькое дополнение @Mark Hildreth.

Контекстный стек выглядит как {thread.get_ident(): []}, где [] называется "стек", потому что используется только операции append (push), pop и [-1] (__getitem__(-1)). Таким образом, стек контекста будет сохранять фактические данные для потока нити или зеленой строки.

current_app, g, request, session и т.д. - это объект LocalProxy, который просто переопределяет специальные методы __getattr__, __getitem__, __call__, __eq__ и т.д. и возвращает значение из верхнего стека контекста ([-1]) по имени аргумента (например, current_app, request). LocalProxy необходимо импортировать эти объекты один раз, и они не будут пропускать актуальность. Так что лучше просто импортировать request, где бы вы ни находились в коде, вместо этого играйте с отправкой аргумента запроса до ваших функций и методов. Вы можете легко писать собственные расширения с ним, но не забывайте, что легкомысленное использование может затруднить понимание кода.

Проведите время, чтобы понять https://github.com/mitsuhiko/werkzeug/blob/master/werkzeug/local.py.

Итак, как заполнены оба стека? По запросу Flask:

  • создать request_context по средам (init map_adapter, путь соответствия)
  • введите или нажмите этот запрос:
    • удалить предыдущий request_context
    • создать app_context, если он пропущен и перенесен в стек контекста приложения
    • этот запрос отправлен для запроса стека контекста
    • сеанс init, если он пропустил
  • запрос на отправку
  • очистить запрос и поместить его из стека

Ответ 4

Давайте возьмем один пример, предположим, что вы хотите установить usercontext (используя конструкцию фляг Local и LocalProxy).

Определите один класс пользователя:

class User(object):
    def __init__(self):
        self.userid = None

определить функцию для извлечения объекта пользователя внутри текущего потока или зеленой строки

def get_user(_local):
    try:
        # get user object in current thread or greenlet
        return _local.user
    except AttributeError:
        # if user object is not set in current thread ,set empty user object 
       _local.user = User()
    return _local.user

Теперь определите LocalProxy

usercontext = LocalProxy(partial(get_user, Local()))

Теперь, чтобы получить идентификатор пользователя в текущей теме   usercontext.userid

объяснение:

1.Local имеет идентификатор и объект, идентичность - это идентификатор threadid или greenlet, в этом примере _local.user = User() является эквивалентным для _local.___ storage __ [current thread id] [ "user" ] = User ( )

  1. Локальная программа делегирует операцию для завершения локального объекта или вы можете предоставить функцию, возвращающую целевой объект. В приведенном выше примере функция get_user предоставляет текущий пользовательский объект LocalProxy, и когда вы запрашиваете текущего пользователя userid usercontext.userid, функция LocalProxy __getattr__ сначала вызывает get_user для получения объекта пользователя (пользователя), а затем вызывает getattr (user, "userid" ). для установки userid на User (в текущем потоке или зеленой строке) вы просто выполните: usercontext.userid = "user_123"