Cela fait quelque temps que j'utilise le contexte requête/application sans bien comprendre son fonctionnement ni la raison pour laquelle il a été conçu. Quel est le but de la "pile" en ce qui concerne le contexte de la demande ou de l'application? S'agit-il de deux piles distinctes, ou s'agit-il d'une pile? Le contexte de la demande est-il placé sur une pile ou s'agit-il d'une pile? Suis-je capable de pousser/sauter plusieurs contextes les uns sur les autres? Si oui, pourquoi voudrais-je faire cela?
Désolé pour toutes les questions, mais je suis toujours perplexe après avoir lu la documentation de Request Context et Application Context.
Le contexte de l'application (et son objectif) prête effectivement à confusion jusqu'à ce que vous réalisiez que Flask peut avoir plusieurs applications. Imaginez la situation dans laquelle vous voulez avoir un seul WSGI Python = L’interprète exécute plusieurs Flask. Nous ne parlons pas de plans ici, nous parlons d’applications complètement différentes Flask Flask.
Vous pouvez configurer ceci de la même manière que exemple de la section de documentation de Flask sur "Distribution de l'application" :
from werkzeug.wsgi import DispatcherMiddleware
from frontend_app import application as frontend
from backend_app import application as backend
application = DispatcherMiddleware(frontend, {
'/backend': backend
})
Notez qu'il y a deux applications Flask complètement différentes en cours de création "frontend" et "backend". En d'autres termes, le constructeur de l'application Flask(...)
a été appelé deux fois, créant ainsi deux instances. d'une application Flask.
Lorsque vous travaillez avec Flask, vous finissez souvent par utiliser des variables globales pour accéder à diverses fonctionnalités. Par exemple, vous avez probablement un code qui lit ...
from flask import request
Ensuite, lors d’une vue, vous pouvez utiliser request
pour accéder aux informations de la requête en cours. De toute évidence, request
n'est pas une variable globale normale; en réalité, il s'agit d'une valeur de contexte local . En d'autres termes, il y a une certaine magie dans les coulisses qui dit "quand j'appelle request.path
, Obtenez l'attribut path
de l'objet request
de la demande CURRENT". Deux requêtes différentes auront des résultats différents pour request.path
.
En fait, même si vous exécutez Flask avec plusieurs threads, Flask est suffisamment intelligent pour garder les objets de requête isolés. Ce faisant, il devient possible pour deux les threads, chacun traitant une requête différente, à appeler simultanément request.path
et à obtenir les informations correctes pour leurs requêtes respectives.
Donc, nous avons déjà vu que Flask peut gérer plusieurs applications dans le même interpréteur, et aussi en raison de la façon dont Flask vous permet d'utiliser le "contexte" globals "locaux" il doit exister un mécanisme permettant de déterminer ce qu'est la demande "actuelle" (afin de faire des choses telles que request.path
) .
En réunissant ces idées, il devrait également être logique que Flask doit avoir un moyen de déterminer l’application "actuelle"!
Vous avez probablement aussi un code similaire au suivant:
from flask import url_for
Comme notre exemple request
, la fonction url_for
A une logique qui dépend de l'environnement actuel. Dans ce cas, toutefois, il est clair que la logique dépend fortement de l'application considérée comme "actuelle". Dans l'exemple frontend/backend illustré ci-dessus, les applications "frontend" et "backend" peuvent toutes les deux avoir une route "/ login", et donc url_for('/login')
devrait renvoyer quelque chose de différent selon que la vue traite la demande. pour l'application frontend ou backend.
Quel est le but de la "pile" en ce qui concerne le contexte de la demande ou de l'application?
À partir des documents de contexte de demande:
Étant donné que le contexte de la demande est géré en interne sous forme de pile, vous pouvez appuyer et décompresser plusieurs fois. C'est très pratique pour implémenter des choses comme les redirections internes.
En d'autres termes, même si vous avez généralement 0 ou 1 élément sur cette pile de requêtes "en cours" ou d'applications "en cours", il est possible que vous en ayez plus.
L'exemple donné est celui où votre demande renverrait les résultats d'une "redirection interne". Supposons qu'un utilisateur demande A, mais que vous souhaitiez le retourner à l'utilisateur B. Dans la plupart des cas, vous envoyez une redirection à l'utilisateur et pointez l'utilisateur sur la ressource B, ce qui signifie que l'utilisateur exécutera une seconde demande pour extraire B. A Une manière légèrement différente de gérer cela serait de faire une redirection interne, ce qui signifie que pendant le traitement de A, Flask se fera une nouvelle demande pour la ressource B et utilisera les résultats de cette seconde demande. en tant que résultat de la demande initiale de l'utilisateur.
S'agit-il de deux piles distinctes, ou s'agit-il d'une pile?
Ils sont deux piles séparées . Cependant, il s'agit d'un détail de mise en œuvre. Ce qui est plus important, ce n’est pas tant qu’il existe une pile, mais bien le fait qu’à tout moment, vous pouvez obtenir l’application ou la requête "actuelle" (en haut de la pile).
Le contexte de la demande est-il placé sur une pile ou s'agit-il d'une pile?
Un "contexte de demande" est l'un des éléments de la "pile de contextes de demande". De même avec le "contexte d'application" et la "pile de contexte d'application".
Suis-je capable de pousser/sauter plusieurs contextes les uns sur les autres? Si oui, pourquoi voudrais-je faire cela?
En règle générale, dans une application Flask, vous ne le feriez pas. Par exemple, vous souhaiterez peut-être utiliser une redirection interne (décrite ci-dessus). Même dans ce cas, cependant, vous termineriez probablement up ayant Flask gérer une nouvelle demande, et ainsi Flask ferait tout ce que vous souhaitiez.
Cependant, dans certains cas, vous voudriez manipuler vous-même la pile.
Un problème typique des gens est qu’ils utilisent l’extension Flask-SQLAlchemy pour configurer une base de données SQL et une définition de modèle à l’aide d’un code similaire à celui présenté ci-dessous ...
app = Flask(__name__)
db = SQLAlchemy() # Initialize the Flask-SQLAlchemy extension object
db.init_app(app)
Ensuite, ils utilisent les valeurs app
et db
dans un script devant être exécuté à partir du shell. Par exemple, un script "setup_tables.py" ...
from myapp import app, db
# Set up models
db.create_all()
Dans ce cas, l'extension Flask-SQLAlchemy est au courant de l'application app
, mais lors de create_all()
, une erreur se plaindre en se plaignant de l'absence de contexte d'application. Cette erreur est justifiée. vous n'avez jamais dit à Flask quelle application il devrait traiter lors de l'exécution de la méthode create_all
.
Vous vous demandez peut-être pourquoi vous n'avez pas besoin de cet appel with app.app_context()
lorsque vous exécutez des fonctions similaires dans vos vues. La raison en est que Flask gère déjà la gestion du contexte de l'application lorsque vous gérez des demandes Web réelles. Le problème ne survient vraiment qu'en dehors de ces fonctions d'affichage (ou de tels rappels), comme lorsque vous utilisez vos modèles dans un script unique.
La solution est de pousser le contexte de l'application vous-même, ce qui peut être fait en faisant ...
from myapp import app, db
# Set up models
with app.app_context():
db.create_all()
Cela poussera un nouveau contexte d'application (en utilisant l'application de app
, rappelez-vous qu'il pourrait y avoir plus d'une application).
Un autre cas où vous voudriez manipuler la pile est pour le test. Vous pouvez créer un test unitaire qui gère une demande et vérifier les résultats:
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
Les réponses précédentes donnent déjà un bon aperçu de ce qui se passe à l'arrière-plan de Flask lors d'une demande. 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, raison pour laquelle il est nécessaire de disposer d'une fonctionnalité thread Local
qui autorise des objets tels que request
et g
être accessible globalement à travers les threads, tout en conservant leur contexte spécifique à la requête.En outre, 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 s’exécuter au sein d’un même processus, et plusieurs peuvent être appelées à agir pendant une requête (chaque requête crée un nouveau contexte d’application), d’où la besoin d'une pile de contexte pour les applications: c'est un résumé de ce qui a été couvert 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 sections locales de contexte. J'ai simplifié le code pour améliorer la compréhension de ses logique, mais si vous obtenez cela, vous devriez pouvoir facilement saisir 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 sections locales de threads.
Lorsqu'une requête http arrive, elle est traitée dans le contexte d'un seul thread. Autre 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) à la place des threads normaux. Si vous n'avez pas installé de greenlets, il utilisera plutôt des threads. Chacun de ces threads (ou greenlets) est identifiable par un identifiant unique, que vous pouvez récupérer avec la fonction get_ident()
du module. Cette fonction est le point de départ de la magie derrière le fait d'avoir 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 quel fil nous sommes à tout moment et nous pouvons créer ce qu’on appelle un fil Local
, un objet contextuel accessible de manière globale, mais quand vous y accédez. les attributs qu'ils résolvent à leur valeur pour ce thread 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 simultanément sur l'objet Local
globalement accessible, mais l'accès à local.first_name
dans le contexte du fil 1 vous donnera 'John'
, alors qu'il retournera 'Debbie'
sur le fil 2.
Comment est-ce possible? Regardons un 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()
Dans le code ci-dessus, nous pouvons voir que la magie se résume à get_ident()
, qui identifie le green ou le fil actuel. Le stockage Local
l'utilise alors simplement 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 auraient simplement été créés de cette façon. Mais ce n'est pas comme cela que l'on fait Flask dans lequel ce ne sont pas des objets techniquement Local
, mais plus précisément LocalProxy
objets Qu'est-ce qu'un LocalProxy
?
Un LocalProxy est un objet qui interroge un Local
pour trouver un autre objet d’intérêt (c’est-à-dire l’objet auquel il est mandaté). Jetons un coup d'oeil 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 proxies accessibles globalement, vous feriez
# this would happen some time near application start-up
local = Local()
request = LocalProxy(local, 'request')
g = LocalProxy(local, 'g')
et maintenant un peu de temps au début d'une requête, vous stockez des objets à l'intérieur du local auxquels les proxys créés précédemment peuvent accéder, quel que soit le fil sur lequel nous sommes.
# this would happen early during processing of an http request
local.request = RequestContext(http_environment)
local.g = SomeGeneralPurposeContainer()
L’utilisation de LocalProxy
comme objets globalement accessibles au lieu de les rendre Locals
_ plus simples est que leur gestion est simplifiée. Vous n'avez besoin que d'un seul objet Local
pour créer de nombreux serveurs proxy globalement accessibles. À la fin de la demande, pendant le nettoyage, vous libérez simplement le fichier Local
(c’est-à-dire que vous extrayez le context_id de son stockage) sans vous soucier des proxy, ils restent globalement accessibles et diffèrent à celui Local
pour trouver son 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 méthode magique Local.__call__()
de la manière suivante:
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')
Cependant, si vous regardez dans la source Flask source (flask.globals)), vous ne trouvez toujours pas comment request
, g
, current_app
et session
sont créés. Comme nous l'avons établi, Flask peut générer plusieurs "fausses" demandes (à partir d'une seule demande http véritable) et, dans le processus, également appliquer plusieurs contextes d'application. Ce n'est pas un cas d'utilisation courant, mais c'est une fonctionnalité du framework: étant donné que ces demandes et applications "simultanées" sont toujours limitées pour s'exécuter avec une seule "cible" à tout moment, il est logique d'utiliser une pile. Chaque fois qu’une nouvelle demande est générée ou qu’une des applications est appelée, ils placent leur contexte en haut de leur pile respective. Flask utilise LocalStack
objets À la fin de leur activité, ils sortent le contexte de la pile.
Voici à quoi ressemble un LocalStack
(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 ci-dessus qu'un LocalStack
est une pile stockée dans un local, pas un groupe de locaux stockés dans une pile. Cela implique que, même si la pile est globalement accessible, elle est différente dans chaque thread.
Le flacon n'a pas ses objets request
, current_app
, g
, et session
résolus directement en LocalStack
, utilise plutôt les objets LocalProxy
qui encapsulent une fonction de recherche (au lieu d'un objet Local
) qui recherchent l'objet sous-jacent à partir de 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 sont résolus 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 par la suite sauté), regardez dans flask.app.Flask.wsgi_app()
qui est le point d'entrée de l'application wsgi (c'est-à-dire ce que le serveur Web appelle et passez l'environnement http à lorsqu'une requête arrive), et suivez la création de l'objet RequestContext
tout au long de sa suivante Push()
dans _request_ctx_stack
. Une fois placé en haut de la pile, il est accessible via _request_ctx_stack.top
. Voici un code abrégé pour illustrer le flux:
Donc, vous démarrez une application et la mettez à la 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 fini de s'initialiser, la recherche de request.path
à partir de l'une de vos fonctions d'affichage se présentera donc comme suit:
LocalProxy
globalement accessible request
._find_request()
(la fonction qu'il a enregistrée sous le nom self.local
).LocalStack
_request_ctx_stack
pour connaître le contexte supérieur de la pile.LocalStack
interroge d'abord son attribut Local
intérieur (self.local
) pour la propriété stack
qui y était précédemment stockée.stack
il obtient le contexte supérieurtop.request
est donc résolu en tant qu'objet d'intérêt sous-jacent.path
Nous avons donc vu comment Local
, LocalProxy
et LocalStack
travaillaient, réfléchissons maintenant un instant aux implications et aux nuances de la récupération du path
de:
request
qui serait un simple objet accessible globalement.request
qui serait un local.request
stocké comme attribut d'un local.request
qui est un proxy pour un objet stocké dans un local.request
stocké dans une pile, elle-même stockée dans un local.request
qui est un proxy pour un objet sur une pile stockée dans un local. <- c'est ce que fait Flask).Petit ajout @ La réponse de Mark Hildreth .
La pile de contexte ressemble à {thread.get_ident(): []}
, où []
appelé "pile" car utilisé uniquement append
(Push
), pop
et [-1]
(__getitem__(-1)
) opérations. Ainsi, la pile de contexte conservera les données réelles du fil ou du fil de la greenlet.
current_app
, g
, request
, session
et etc est LocalProxy
objet qui vient de remplacer les méthodes spéciales __getattr__
, __getitem__
, __call__
, __eq__
, etc., et renvoient une valeur depuis la pile de contexte ([-1]
) par nom d'argument (current_app
, request
par exemple). LocalProxy
devait importer ces objets une fois et ils ne manqueraient pas l'actualité. Il est donc préférable de simplement importer request
où que vous soyez dans le code, jouez avec l'envoi de l'argument de requête à vos fonctions et méthodes. Vous pouvez facilement écrire vos propres extensions avec cela, mais n'oubliez pas qu'un usage frivole peut rendre le code plus difficile à comprendre.
Prenez le temps de comprendre https://github.com/mitsuhiko/werkzeug/blob/master/werkzeug/local.py .
Alors, comment peuplé les deux piles? Sur demande Flask
:
request_context
par environnement (init map_adapter
, chemin de correspondance)request_context
app_context
s'il est manquant et poussé dans la pile de contexte d'applicationPrenons un exemple. Supposons que vous souhaitiez définir un contexte utilisateur (avec flask construct of Local et LocalProxy).
Définir une classe d'utilisateurs:
class User(object):
def __init__(self):
self.userid = None
définir une fonction pour récupérer l'objet utilisateur dans le thread ou la greenlet en cours
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
Définissez maintenant un LocalProxy
usercontext = LocalProxy(partial(get_user, Local()))
Maintenant, obtenir l'ID utilisateur de l'utilisateur dans le fil actuel usercontext.userid
explication:
1.Local a un dicton d'identité et d'objet, identity est un threadid ou un id de greenlet, dans cet exemple, _local.user = utilisateur () est équivalent à _local .___ stockage __ [id du thread actuel] ["utilisateur"] = utilisateur ()