As respostas anteriores já oferecem uma boa visão geral do que acontece no plano de fundo do Flask durante uma solicitação. Se você ainda não o leu, recomendo a resposta de @ MarkHildreth antes de ler isso. Em resumo, um novo contexto (thread) é criado para cada solicitação http, e é por isso que é necessário ter um threadLocal
recurso de que permita objetos como request
eg
seja acessível globalmente através de threads, mantendo o contexto específico da solicitação. Além disso, ao processar uma solicitação http, o Flask pode emular solicitações adicionais de dentro, daí a necessidade de armazenar seu respectivo contexto em uma pilha. Além disso, o Flask permite que vários aplicativos wsgi sejam executados entre si em um único processo, e mais de um pode ser chamado à ação durante uma solicitação (cada solicitação cria um novo contexto de aplicativo), daí a necessidade de uma pilha de contexto para aplicativos. Este é um resumo do que foi abordado nas respostas anteriores.
Meu objetivo agora é complementar nosso entendimento atual, explicando como Flask e Werkzeug fazem o que fazem com esses locais de contexto. Simplifiquei o código para aprimorar o entendimento de sua lógica, mas se você entender isso, poderá entender facilmente a maior parte do que está na fonte ( werkzeug.local
e flask.globals
) real .
Vamos primeiro entender como o Werkzeug implementa Thread Locals.
Local
Quando uma solicitação http é recebida, ela é processada no contexto de um único encadeamento. Como um meio alternativo para gerar um novo contexto durante uma solicitação de http, o Werkzeug também permite o uso de greenlets (uma espécie de "micro-threads" mais leves) em vez de threads normais. Se você não tiver greenlets instalados, ele voltará ao uso de threads. Cada um desses encadeamentos (ou greenlets) é identificável por um ID exclusivo, que você pode recuperar com a get_ident()
função do módulo . Essa função é o ponto de partida para a mágica por trás tendo request
, current_app
, url_for
, g
, e outros tais objetos globais ligados ao contexto.
try:
from greenlet import get_ident
except ImportError:
from thread import get_ident
Agora que temos nossa função de identidade, podemos saber em qual thread estamos em um determinado momento e podemos criar o que é chamado de thread Local
, um objeto contextual que pode ser acessado globalmente, mas quando você acessa seus atributos, eles resolvem seu valor para esse segmento específico. por exemplo
# globally
local = Local()
# ...
# on thread 1
local.first_name = 'John'
# ...
# on thread 2
local.first_name = 'Debbie'
Ambos os valores estão presentes no Local
objeto acessível globalmente ao mesmo tempo, mas o acesso local.first_name
no contexto do encadeamento 1 fornecerá a você 'John'
, enquanto retornará 'Debbie'
no encadeamento 2.
Como isso é possível? Vamos dar uma olhada em algum código (simplificado):
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()
A partir do código acima, podemos ver que a mágica se resume a get_ident()
qual identifica o greenlet ou segmento atual. O Local
armazenamento usa apenas isso como uma chave para armazenar quaisquer dados contextuais no encadeamento atual.
Você pode ter vários Local
objetos por processo e request
, g
, current_app
e outros poderiam simplesmente ter sido criado assim. Mas não é assim que é feito no Flask, no qual estes não são tecnicamente Local
objetos, mas LocalProxy
objetos com mais precisão . O que é um LocalProxy
?
LocalProxy
Um LocalProxy é um objeto que consulta a Local
para encontrar outro objeto de interesse (ou seja, o objeto ao qual ele procura proxy). Vamos dar uma olhada para entender:
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 ...
Agora, para criar proxies acessíveis globalmente, você faria
# this would happen some time near application start-up
local = Local()
request = LocalProxy(local, 'request')
g = LocalProxy(local, 'g')
e agora, com algum tempo de antecedência, no decorrer de uma solicitação, você armazenaria alguns objetos dentro do local que os proxies criados anteriormente podem acessar, independentemente do segmento em que estamos
# this would happen early during processing of an http request
local.request = RequestContext(http_environment)
local.g = SomeGeneralPurposeContainer()
A vantagem de usar LocalProxy
como objetos acessíveis globalmente, em vez de torná-los eles Locals
mesmos, é que simplifica seu gerenciamento. Você só precisa de um único Local
objeto para criar muitos proxies acessíveis globalmente. No final da solicitação, durante a limpeza, você simplesmente libera uma Local
(ou seja, extrai o context_id do armazenamento) e não se preocupa com os proxies, eles ainda são acessíveis globalmente e ainda diferem Local
para encontrar o objeto de interesse para solicitações http subsequentes.
# this would happen some time near the end of request processing
release(local) # aka local.__release_local__()
Para simplificar a criação de a LocalProxy
quando já temos a Local
, Werkzeug implementa o Local.__call__()
método mágico da seguinte maneira:
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')
No entanto, se você olhar na fonte Flask (flask.globals) que ainda não é como request
, g
, current_app
e session
são criados. Como estabelecemos, o Flask pode gerar várias solicitações "falsas" (a partir de uma única solicitação HTTP verdadeira) e, no processo, também envia vários contextos de aplicativos. Este não é um caso de uso comum, mas é um recurso da estrutura. Como essas solicitações e aplicativos "simultâneos" ainda estão limitados para serem executados, com apenas um tendo o "foco" a qualquer momento, faz sentido usar uma pilha para o respectivo contexto. Sempre que uma nova solicitação é gerada ou um dos aplicativos é chamado, eles colocam seu contexto no topo da respectiva pilha. O Flask usa LocalStack
objetos para essa finalidade. Quando concluem seus negócios, expõem o contexto da pilha.
LocalStack
É assim que LocalStack
parece (novamente o código é simplificado para facilitar a compreensão de sua lógica).
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
Observe acima que um LocalStack
é uma pilha armazenada em um local, não um monte de locais armazenados em uma pilha. Isso implica que, embora a pilha seja globalmente acessível, é uma pilha diferente em cada thread.
O frasco não tem o seu request
, current_app
, g
, e session
objetos resolver diretamente a um LocalStack
, que, em vez usa LocalProxy
objetos que envolvem uma função de pesquisa (em vez de um Local
objeto) que irá encontrar o objeto subjacente do 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)
Todos esses itens são declarados na inicialização do aplicativo, mas na verdade não resolvem nada até que um contexto de solicitação ou contexto de aplicativo seja enviado para a respectiva pilha.
Se você estiver curioso para ver como um contexto é realmente inserido na pilha (e posteriormente exibido), flask.app.Flask.wsgi_app()
verifique qual é o ponto de entrada do aplicativo wsgi (ou seja, o que o servidor da web chama e passe o ambiente http para quando um pedido vem em), e siga a criação do RequestContext
objeto durante toda a sua posterior push()
em _request_ctx_stack
. Uma vez pressionado no topo da pilha, é acessível via_request_ctx_stack.top
. Aqui está um código abreviado para demonstrar o fluxo:
Então, você inicia um aplicativo e o disponibiliza para o servidor WSGI ...
app = Flask(*config, **kwconfig)
# ...
Mais tarde, uma solicitação http é recebida e o servidor WSGI chama o aplicativo com os parâmetros comuns ...
app(environ, start_response) # aka app.__call__(environ, start_response)
Isso é mais ou menos o que acontece no aplicativo ...
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()
# ...
e isso é aproximadamente o que acontece com o 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()
Digamos que uma solicitação tenha terminado de inicializar, a pesquisa para request.path
uma de suas funções de visualização seria a seguinte:
- comece a partir do
LocalProxy
objeto acessível globalmente request
.
- para encontrar seu objeto de interesse subjacente (o objeto para o qual está fazendo proxy), ele chama sua função de pesquisa
_find_request()
(a função que ele registrou como seuself.local
).
- essa função consulta o
LocalStack
objeto_request_ctx_stack
para o contexto superior na pilha.
- Para encontrar o contexto principal, o
LocalStack
objeto primeiro consulta seu Local
atributo interno ( self.local
) para ostack
propriedade que foi armazenada anteriormente lá.
- de
stack
que obtém o contexto superior
- e
top.request
portanto, é resolvido como o objeto de interesse subjacente.
- desse objeto, obtemos o
path
atributo
Então, vimos como Local
, LocalProxy
e LocalStack
trabalhamos, agora pense por um momento nas implicações e nuances na recuperação path
de:
- um
request
objeto que seria um simples objeto acessível globalmente.
- um
request
objeto que seria um local.
- um
request
objeto armazenado como um atributo de um local.
- uma
request
objeto que é um proxy para um objeto armazenado em um local.
- uma
request
objeto armazenado em uma pilha, que por sua vez é armazenado em um local.
- um
request
objeto que é um proxy para um objeto em uma pilha armazenada em um local. <- é isso que o Flask faz.