Las respuestas anteriores ya ofrecen una buena descripción de lo que sucede en el fondo de Flask durante una solicitud. Si aún no lo ha leído, recomiendo la respuesta de @ MarkHildreth antes de leer esto. En resumen, se crea un nuevo contexto (hilo) para cada solicitud http, razón por la cual es necesario tener una función de hilo Local
que permita objetos como request
yg
para ser accesible globalmente a través de subprocesos, manteniendo su contexto específico de solicitud. Además, al procesar una solicitud http, Flask puede emular solicitudes adicionales desde dentro, de ahí la necesidad de almacenar su contexto respectivo en una pila. Además, Flask permite que múltiples aplicaciones wsgi se ejecuten juntas dentro de un solo proceso, y más de una puede ser llamada a la acción durante una solicitud (cada solicitud crea un nuevo contexto de aplicación), de ahí la necesidad de una pila de contexto para las aplicaciones. Ese es un resumen de lo que se cubrió en las respuestas anteriores.
Mi objetivo ahora es complementar nuestra comprensión actual explicando cómo Flask y Werkzeug hacen lo que hacen con estos locales de contexto. Simplifiqué el código para mejorar la comprensión de su lógica, pero si obtienes esto, deberías poder comprender fácilmente la mayoría de lo que hay en la fuente real ( werkzeug.local
y flask.globals
).
Primero comprendamos cómo Werkzeug implementa hilos locales.
Local
Cuando entra una solicitud http, se procesa dentro del contexto de un solo hilo. Como medio alternativo para generar un nuevo contexto durante una solicitud http, Werkzeug también permite el uso de greenlets (una especie de "microhilos" más ligeros) en lugar de hilos normales. Si no tiene instalados los greenlets, volverá a usar hilos en su lugar. Cada uno de estos hilos (o greenlets) son identificables por una identificación única, que puede recuperar con la get_ident()
función del módulo . Esa función es el punto de partida a la magia detrás de tener request
, current_app
, url_for
, g
, y otros objetos globales el contexto.
try:
from greenlet import get_ident
except ImportError:
from thread import get_ident
Ahora que tenemos nuestra función de identidad, podemos saber en qué subproceso estamos en un momento dado y podemos crear lo que se llama un subproceso Local
, un objeto contextual al que se puede acceder globalmente, pero cuando accede a sus atributos, resuelven su valor para ese hilo específico. p.ej
# globally
local = Local()
# ...
# on thread 1
local.first_name = 'John'
# ...
# on thread 2
local.first_name = 'Debbie'
Ambos valores están presentes en el Local
objeto accesible globalmente al mismo tiempo, pero acceder local.first_name
dentro del contexto del hilo 1 le dará 'John'
, mientras que volverá 'Debbie'
en el hilo 2.
¿Cómo es eso posible? Veamos un 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()
Del código anterior podemos ver que la magia se reduce a lo get_ident()
que identifica el greenlet o hilo actual. El Local
almacenamiento solo lo usa como clave para almacenar cualquier dato contextual al hilo actual.
Se pueden tener varios Local
objetos por proceso y request
, g
, current_app
y otros simplemente pudo haber sido creado por el estilo. Pero no es así como se hace en Flask en el que estos no son técnicamente Local
objetos, sino más exactamente LocalProxy
objetos. ¿Qué es un LocalProxy
?
LocalProxy
Un LocalProxy es un objeto que consulta a Local
para encontrar otro objeto de interés (es decir, el objeto al que se aproxima). Echemos un vistazo 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 ...
Ahora, para crear proxies accesibles globalmente, harías
# this would happen some time near application start-up
local = Local()
request = LocalProxy(local, 'request')
g = LocalProxy(local, 'g')
y ahora un poco antes en el transcurso de una solicitud, almacenaría algunos objetos dentro del local a los que pueden acceder los servidores proxy creados anteriormente, sin importar en qué hilo estemos
# this would happen early during processing of an http request
local.request = RequestContext(http_environment)
local.g = SomeGeneralPurposeContainer()
La ventaja de usar LocalProxy
objetos globalmente accesibles en lugar de hacerlos ellos Locals
mismos es que simplifica su administración. Solo necesita un solo Local
objeto para crear muchos servidores proxy accesibles globalmente. Al final de la solicitud, durante la limpieza, simplemente libera el uno Local
(es decir, saca el context_id de su almacenamiento) y no se molesta con los proxies, todavía son accesibles globalmente y aún difieren del Local
que busca su objeto. de interés para solicitudes http posteriores.
# this would happen some time near the end of request processing
release(local) # aka local.__release_local__()
Para simplificar la creación de un LocalProxy
cuando ya tenemos un Local
, Werkzeug implementa el Local.__call__()
método mágico de la siguiente manera:
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')
Sin embargo, si nos fijamos en la fuente Frasco (flask.globals) que todavía no es como request
, g
, current_app
y session
son creados. Como hemos establecido, Flask puede generar múltiples solicitudes "falsas" (desde una única solicitud http verdadera) y en el proceso también puede impulsar múltiples contextos de aplicación. Este no es un caso de uso común, pero es una capacidad del marco. Dado que estas solicitudes y aplicaciones "concurrentes" todavía se limitan a ejecutarse con solo una que tiene el "foco" en cualquier momento, tiene sentido usar una pila para su contexto respectivo. Cada vez que se genera una nueva solicitud o se llama a una de las aplicaciones, empujan su contexto en la parte superior de su pila respectiva. Flask usa LocalStack
objetos para este propósito. Cuando concluyen su negocio, sacan el contexto de la pila.
LocalStack
Así es LocalStack
como se ve (de nuevo, el código se simplifica para facilitar la comprensión de su 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
Tenga en cuenta de lo anterior que a LocalStack
es una pila almacenada en un local, no un montón de locales almacenados en una pila. Esto implica que aunque la pila es accesible globalmente, es una pila diferente en cada hilo.
Frasco no tiene su request
, current_app
, g
, y session
objetos resolver directamente a una LocalStack
, más bien utiliza LocalProxy
objetos que se colocan una función de búsqueda (en lugar de un Local
objeto) que se encuentra el objeto subyacente de la 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 estos se declaran al inicio de la aplicación, pero en realidad no resuelven nada hasta que un contexto de solicitud o contexto de aplicación se empuje a su respectiva pila.
Si tiene curiosidad por ver cómo se inserta realmente un contexto en la pila (y luego emerge), observe flask.app.Flask.wsgi_app()
cuál es el punto de entrada de la aplicación wsgi (es decir, a qué llama el servidor web y pasa el entorno http cuando petición llega), y sigue la creación del RequestContext
objeto a lo largo de su posterior push()
dentro _request_ctx_stack
. Una vez presionado en la parte superior de la pila, se puede acceder a través de _request_ctx_stack.top
. Aquí hay un código abreviado para demostrar el flujo:
Entonces inicia una aplicación y la pone a disposición del servidor WSGI ...
app = Flask(*config, **kwconfig)
# ...
Más tarde, llega una solicitud http y el servidor WSGI llama a la aplicación con los parámetros habituales ...
app(environ, start_response) # aka app.__call__(environ, start_response)
Esto es más o menos lo que sucede en la aplicación ...
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()
# ...
y esto es más o menos lo que sucede con 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 una solicitud ha terminado de inicializarse, por lo tanto, la búsqueda request.path
de una de sus funciones de vista sería la siguiente:
- comenzar desde el
LocalProxy
objeto accesible globalmente request
.
- para encontrar su objeto de interés subyacente (el objeto al que está representando) llama a su función de búsqueda
_find_request()
(la función que registró como su self.local
).
- esa función consulta el
LocalStack
objeto _request_ctx_stack
para el contexto superior en la pila.
- Para encontrar el contexto superior, el
LocalStack
objeto primero consulta su Local
atributo interno ( self.local
) para la stack
propiedad que estaba previamente almacenada allí.
- del
stack
obtiene el contexto superior
- y por
top.request
lo tanto se resuelve como el objeto de interés subyacente.
- de ese objeto obtenemos el
path
atributo
Así hemos visto cómo Local
, LocalProxy
y LocalStack
el trabajo, ahora piensa por un momento de las implicaciones y matices en la recuperación de la path
de:
- un
request
objeto que sería un simple objeto accesible globalmente.
- un
request
objeto que sería un local.
- un
request
objeto almacenado como un atributo de un local.
- un
request
objeto que es un proxy para un objeto almacenado en un local.
- un
request
objeto almacenado en una pila, que a su vez se almacena en un local.
- un
request
objeto que es un proxy para un objeto en una pila almacenada en un local. <- esto es lo que hace Flask.