Предыдущие ответы уже дают хороший обзор того, что происходит на фоне 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's or greenlet's 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
объекты. Что LocalProxy
?
LocalProxy
LocalProxy - это объект, который запрашивает a, 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
(то есть извлекаете context_id из его хранилища) и не беспокоитесь о прокси-серверах, они по-прежнему доступны глобально и по-прежнему передаются тому, кто Local
ищет свой объект. представляет интерес для последующих запросов http.
# this would happen some time near the end of request processing
release(local) # aka local.__release_local__()
Чтобы упростить создание a, 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.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
из:
request
объект , который был бы простой глобально доступный объект.
request
объект , который будет локальным.
request
объект , хранящийся в качестве атрибута локальных.
request
объект , который является прокси объекта , хранящегося в локальный.
request
объект , хранящийся в стеке, что в свою очередь , хранится в местном.
request
объект , который является прокси объекта на стеке хранится в локальной. <- это то, что делает Flask.