Les réponses précédentes donnent déjà un bon aperçu de ce qui se passe en arrière-plan de Flask lors d'une requête. Si vous ne l'avez pas encore lu, je recommande la réponse de @ MarkHildreth avant de lire ceci. En bref, un nouveau contexte (thread) est créé pour chaque requête http, c'est pourquoi il est nécessaire d'avoir une fonction de thread Local
qui autorise des objets tels que request
etg
pour être accessible globalement à travers les threads, tout en conservant leur contexte spécifique à la demande. De plus, lors du traitement d'une requête http, Flask peut émuler des requêtes supplémentaires de l'intérieur, d'où la nécessité de stocker leur contexte respectif sur une pile. De plus, Flask permet à plusieurs applications wsgi de fonctionner les unes avec les autres dans un même processus, et plusieurs peuvent être appelées à l'action pendant une requête (chaque requête crée un nouveau contexte d'application), d'où la nécessité d'une pile de contexte pour les applications. C'est un résumé de ce qui a été traité dans les réponses précédentes.
Mon objectif est maintenant de compléter notre compréhension actuelle en expliquant comment Flask et Werkzeug font ce qu'ils font avec ces locaux contextuels. J'ai simplifié le code pour améliorer la compréhension de sa logique, mais si vous obtenez cela, vous devriez être en mesure de saisir facilement la plupart de ce qui se trouve dans la source réelle ( werkzeug.local
et flask.globals
).
Voyons d'abord comment Werkzeug implémente les threads locaux.
Local
Lorsqu'une requête http arrive, elle est traitée dans le contexte d'un seul thread. Comme moyen alternatif de générer un nouveau contexte lors d'une requête http, Werkzeug permet également l'utilisation de greenlets (une sorte de «micro-threads» plus légers) au lieu de threads normaux. Si vous n'avez pas installé de greenlets, il reviendra à l'utilisation des threads à la place. Chacun de ces threads (ou greenlets) est identifiable par un identifiant unique, que vous pouvez récupérer avec la get_ident()
fonction du module . Cette fonction est le point de départ de la magie derrière ayant request
, current_app
, url_for
, g
, et d' autres objets globaux liés au contexte.
try:
from greenlet import get_ident
except ImportError:
from thread import get_ident
Maintenant que nous avons notre fonction d'identité, nous pouvons savoir sur quel thread nous sommes à un moment donné et nous pouvons créer ce qu'on appelle un thread Local
, un objet contextuel accessible globalement, mais lorsque vous accédez à ses attributs, ils se résolvent à leur valeur pour ce fil spécifique. par exemple
# globally
local = Local()
# ...
# on thread 1
local.first_name = 'John'
# ...
# on thread 2
local.first_name = 'Debbie'
Les deux valeurs sont présentes sur l' Local
objet globalement accessible en même temps, mais accéder local.first_name
dans le contexte du thread 1 vous le donnera 'John'
, alors qu'il reviendra 'Debbie'
sur le thread 2.
Comment est-ce possible? Regardons du code (simplifié):
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()
À partir du code ci-dessus, nous pouvons voir que la magie se résume à get_ident()
qui identifie le greenlet ou le thread actuel. Le Local
stockage l'utilise ensuite comme clé pour stocker toutes les données contextuelles du thread actuel.
Vous pouvez avoir plusieurs Local
objets par processus et request
, g
, current_app
et d' autres pourraient simplement avoir été créé comme ça. Mais ce n'est pas ainsi que cela se fait dans Flask dans lequel ce ne sont pas techniquement des Local
objets, mais plus précisément des LocalProxy
objets. Qu'est-ce qu'un LocalProxy
?
LocalProxy
Un LocalProxy est un objet qui interroge a Local
pour trouver un autre objet d'intérêt (c'est-à-dire l'objet auquel il se rapporte ). Jetons un œil pour comprendre:
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 ...
Maintenant, pour créer des proxys accessibles à l'échelle mondiale, vous
# this would happen some time near application start-up
local = Local()
request = LocalProxy(local, 'request')
g = LocalProxy(local, 'g')
et maintenant quelque temps plus tôt au cours d'une demande, vous stockeriez des objets dans le local auxquels les proxys précédemment créés peuvent accéder, quel que soit le thread sur lequel nous sommes
# this would happen early during processing of an http request
local.request = RequestContext(http_environment)
local.g = SomeGeneralPurposeContainer()
L'avantage d'utiliser des LocalProxy
objets globalement accessibles plutôt que de les fabriquer eux- Locals
mêmes est que cela simplifie leur gestion. Vous n'avez besoin que d'un seul Local
objet pour créer de nombreux proxys globalement accessibles. A la fin de la requête, lors du nettoyage, vous libérez simplement celui Local
(c'est-à-dire que vous sortez le context_id de son stockage) et ne vous embêtez pas avec les proxies, ils sont toujours accessibles globalement et se reportent toujours à celui Local
pour trouver leur objet d'intérêt pour les requêtes http suivantes.
# this would happen some time near the end of request processing
release(local) # aka local.__release_local__()
Pour simplifier la création d'un LocalProxy
lorsque nous avons déjà un Local
, Werkzeug implémente la Local.__call__()
méthode magique comme suit:
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')
Toutefois, si vous regardez dans la source Flask (de flask.globals) qui est toujours pas comment request
, g
, current_app
et session
sont créés. Comme nous l'avons établi, Flask peut générer plusieurs "fausses" requêtes (à partir d'une seule vraie requête http) et, dans le processus, également pousser plusieurs contextes d'application. Ce n'est pas un cas d'utilisation courant, mais c'est une capacité du framework. Étant donné que ces demandes et applications "simultanées" sont encore limitées à s'exécuter avec une seule ayant le "focus" à tout moment, il est logique d'utiliser une pile pour leur contexte respectif. À chaque fois qu'une nouvelle demande est générée ou qu'une des applications est appelée, ils poussent leur contexte en haut de leur pile respective. Flask utilise des LocalStack
objets à cet effet. Quand ils concluent leurs affaires, ils sortent le contexte de la pile.
LocalStack
Voici à quoi LocalStack
ressemble un (encore une fois, le code est simplifié pour faciliter la compréhension de sa logique).
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
Notez à partir de ce qui précède que a LocalStack
est une pile stockée dans un local, pas un groupe de locaux stockés sur une pile. Cela implique que bien que la pile soit globalement accessible, c'est une pile différente dans chaque thread.
Flask ne dispose pas de son request
, current_app
, g
et projet d' session
objets résoudre directement à un LocalStack
, il utilise plutôt des LocalProxy
objets qui enveloppent une fonction de recherche ( au lieu d'un Local
objet) qui trouve l'objet sous - jacent 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)
Tous ces éléments sont déclarés au démarrage de l'application, mais ne se résolvent réellement en rien tant qu'un contexte de demande ou un contexte d'application n'est pas poussé vers leur pile respective.
Si vous êtes curieux de voir comment un contexte est réellement inséré dans la pile (et ensuite sorti), regardez dans flask.app.Flask.wsgi_app()
quel est le point d'entrée de l'application wsgi (c'est-à-dire ce que le serveur Web appelle et transmet l'environnement http quand un requête arrive), et suivez la création de l' RequestContext
objet tout au long de sa suite push()
dans _request_ctx_stack
. Une fois poussé en haut de la pile, il est accessible via _request_ctx_stack.top
. Voici un code abrégé pour illustrer le flux:
Vous démarrez donc une application et la mettez à disposition du serveur WSGI ...
app = Flask(*config, **kwconfig)
# ...
Plus tard, une requête http arrive et le serveur WSGI appelle l'application avec les paramètres habituels ...
app(environ, start_response) # aka app.__call__(environ, start_response)
C'est à peu près ce qui se passe dans l'application ...
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()
# ...
et c'est à peu près ce qui se passe avec 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()
Supposons qu'une requête ait terminé son initialisation, la recherche à request.path
partir de l'une de vos fonctions d'affichage se déroulerait donc comme suit:
- commencer à partir de l'
LocalProxy
objet globalement accessible request
.
- pour trouver son objet d'intérêt sous-jacent (l'objet auquel il est mandaté), il appelle sa fonction de recherche
_find_request()
(la fonction qu'il a enregistrée comme son self.local
).
- cette fonction interroge l'
LocalStack
objet _request_ctx_stack
pour le contexte supérieur sur la pile.
- pour trouver le contexte supérieur, l'
LocalStack
objet interroge d'abord son Local
attribut interne ( self.local
) pour la stack
propriété qui y était précédemment stockée.
- du
stack
il obtient le contexte supérieur
- et
top.request
est donc résolu comme l'objet d'intérêt sous-jacent.
- de cet objet, nous obtenons l'
path
attribut
Nous avons donc vu comment Local
, LocalProxy
et LocalStack
travailler, réfléchissons maintenant un instant aux implications et aux nuances de la récupération path
de:
- un
request
objet qui serait un simple objet accessible globalement.
- un
request
objet qui serait un local.
- un
request
objet stocké comme attribut d'un local.
- un
request
objet qui est un proxy vers un objet stocké dans un local.
- un
request
objet stocké sur une pile, qui est à son tour stocké dans un local.
- un
request
objet qui est un proxy vers un objet sur une pile stockée dans un local. <- c'est ce que fait Flask.