Le risposte precedenti offrono già una buona panoramica di ciò che accade sullo sfondo di Flask durante una richiesta. Se non l'hai ancora letto, raccomando la risposta di @ MarkHildreth prima di leggere questo. In breve, viene creato un nuovo contesto (thread) per ogni richiesta http, motivo per cui è necessario disporre di una Local
funzione thread che consenta oggetti come request
eg
essere accessibili a livello globale tra i thread, pur mantenendo il contesto specifico della loro richiesta. Inoltre, durante l'elaborazione di una richiesta http Flask può emulare richieste aggiuntive dall'interno, quindi la necessità di memorizzare il rispettivo contesto su uno stack. Inoltre, Flask consente a più applicazioni wsgi di essere eseguite l'una accanto all'altra all'interno di un singolo processo e più di una può essere chiamata all'azione durante una richiesta (ogni richiesta crea un nuovo contesto dell'applicazione), quindi la necessità di uno stack di contesto per le applicazioni. Questo è un riassunto di ciò che è stato trattato nelle risposte precedenti.
Il mio obiettivo ora è quello di integrare la nostra attuale comprensione spiegando come Flask e Werkzeug fanno quello che fanno con queste persone del contesto. Ho semplificato il codice per migliorare la comprensione della sua logica, ma se ottieni questo, dovresti essere in grado di cogliere facilmente la maggior parte di ciò che è nella fonte reale ( werkzeug.local
e flask.globals
).
Per prima cosa capiamo come Werkzeug implementa Thread Locals.
Locale
Quando arriva una richiesta http, viene elaborata nel contesto di un singolo thread. Come mezzo alternativo per generare un nuovo contesto durante una richiesta http, Werkzeug consente anche l'uso di greenlet (una sorta di "micro-thread" più leggeri) anziché normali thread. Se non hai installato greenlet, tornerà a utilizzare i thread. Ognuno di questi thread (o greenlet) è identificabile da un ID univoco, che è possibile recuperare con la get_ident()
funzione del modulo . Tale funzione è il punto di partenza per la magia dietro con request
, current_app
, url_for
, g
, e altre oggetti globali legato al contesto.
try:
from greenlet import get_ident
except ImportError:
from thread import get_ident
Ora che abbiamo la nostra funzione di identità, possiamo sapere su quale thread ci troviamo in un dato momento e possiamo creare quello che viene chiamato thread Local
, un oggetto contestuale a cui è possibile accedere a livello globale, ma quando accedi ai suoi attributi si risolvono al loro valore per quel filo specifico. per esempio
# globally
local = Local()
# ...
# on thread 1
local.first_name = 'John'
# ...
# on thread 2
local.first_name = 'Debbie'
Entrambi i valori sono presenti Local
contemporaneamente sull'oggetto accessibile globalmente , ma l'accesso local.first_name
nel contesto del thread 1 ti darà 'John'
, mentre tornerà 'Debbie'
sul thread 2.
Come è possibile? Diamo un'occhiata ad alcuni codici (semplificati):
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()
Dal codice sopra possiamo vedere che la magia si riduce a get_ident()
cui identifica il greenlet o il thread corrente. L' Local
archiviazione quindi lo utilizza come chiave per archiviare qualsiasi dato contestuale al thread corrente.
È possibile avere più Local
oggetti per ogni processo e request
, g
, current_app
e altri potrebbe semplicemente essere stato creato in quel modo. Ma non è così che si fa in Flask in cui questi non sono tecnicamente Local
oggetti, ma LocalProxy
oggetti più accurati . Che cos'è un LocalProxy
?
LocalProxy
Un LocalProxy è un oggetto che richiede a Local
di trovare un altro oggetto di interesse (ovvero l'oggetto a cui viene inoltrato). Diamo un'occhiata per capire:
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 ...
Ora, per creare proxy accessibili a livello globale, lo faresti
# this would happen some time near application start-up
local = Local()
request = LocalProxy(local, 'request')
g = LocalProxy(local, 'g')
e ora qualche tempo prima nel corso di una richiesta, memorizzereste alcuni oggetti all'interno del locale a cui i proxy precedentemente creati possono accedere, indipendentemente dal thread su cui ci troviamo
# this would happen early during processing of an http request
local.request = RequestContext(http_environment)
local.g = SomeGeneralPurposeContainer()
Il vantaggio di utilizzare LocalProxy
come oggetti accessibili a livello globale piuttosto che crearli da Locals
soli è che semplifica la loro gestione. Hai solo bisogno di un singolo Local
oggetto per creare molti proxy accessibili a livello globale. Alla fine della richiesta, durante la pulizia, è sufficiente rilasciare quello Local
(ovvero estrarre il context_id dalla sua memoria) e non preoccuparsi dei proxy, sono ancora accessibili a livello globale e continuano a rinviare a quello Local
per trovare il loro oggetto di interesse per successive richieste http.
# this would happen some time near the end of request processing
release(local) # aka local.__release_local__()
Per semplificare la creazione di un LocalProxy
quando ne abbiamo già uno Local
, Werkzeug implementa il Local.__call__()
metodo magico come segue:
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')
Tuttavia, se si guarda nella fonte Flask (flask.globals), che non è ancora come request
, g
, current_app
e session
vengono creati. Come abbiamo stabilito, Flask può generare più richieste "false" (da una singola richiesta http reale) e nel processo anche spingere più contesti applicativi. Questo non è un caso d'uso comune, ma è una capacità del framework. Dal momento che queste richieste e app "simultanee" sono ancora limitate per essere eseguite con solo uno che ha il "focus" in qualsiasi momento, ha senso usare uno stack per il rispettivo contesto. Ogni volta che viene generata una nuova richiesta o viene chiamata una delle applicazioni, spingono il loro contesto in cima al rispettivo stack. Flask utilizza LocalStack
oggetti per questo scopo. Quando concludono la loro attività, espellono il contesto dallo stack.
LocalStack
Ecco LocalStack
come appare (di nuovo il codice è semplificato per facilitare la comprensione della sua logica).
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
Si noti da quanto sopra che a LocalStack
è uno stack archiviato in un locale, non un gruppo di locali memorizzati in uno stack. Ciò implica che sebbene lo stack sia accessibile a livello globale è uno stack diverso in ogni thread.
Flask non dispone request
, current_app
, g
, e session
gli oggetti per risolvere direttamente ad una LocalStack
, piuttosto utilizza LocalProxy
oggetti che avvolgono una funzione di ricerca (invece di un Local
oggetto) che troverà l'oggetto sottostante dal 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)
Tutti questi sono dichiarati all'avvio dell'applicazione, ma in realtà non risolvono nulla fino a quando un contesto di richiesta o contesto di applicazione non viene inserito nel rispettivo stack.
Se sei curioso di vedere come un contesto è effettivamente inserito nello stack (e successivamente spuntato), guarda in flask.app.Flask.wsgi_app()
quale punto di entrata dell'app wsgi (ovvero ciò che chiama il web server e passa l'ambiente http a quando un richiesta entra) e segui la creazione RequestContext
dell'oggetto per tutto il suo successivo push()
in _request_ctx_stack
. Una volta spinto nella parte superiore dello stack, è accessibile tramite _request_ctx_stack.top
. Ecco un codice abbreviato per dimostrare il flusso:
Quindi avvii un'app e la rendi disponibile al server WSGI ...
app = Flask(*config, **kwconfig)
# ...
Successivamente arriva una richiesta http e il server WSGI chiama l'app con i soliti parametri ...
app(environ, start_response) # aka app.__call__(environ, start_response)
Questo è all'incirca quello che succede nell'app ...
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 questo è più o meno quello che succede 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()
Supponi che una richiesta abbia terminato l'inizializzazione, la ricerca request.path
da una delle tue funzioni di visualizzazione dovrebbe quindi procedere come segue:
- partire dalla globalmente accessibile
LocalProxy
oggetto request
.
- per trovare il suo oggetto di interesse sottostante (l'oggetto a cui sta eseguendo il proxy) chiama la sua funzione di ricerca
_find_request()
(la funzione che ha registrato come suo self.local
).
- quella funzione interroga l'
LocalStack
oggetto _request_ctx_stack
per il contesto principale nello stack.
- per trovare il contesto principale, l'
LocalStack
oggetto prima interroga il suo Local
attributo interno ( self.local
) per la stack
proprietà precedentemente memorizzata lì.
- da
stack
esso ottiene il contesto superiore
- e
top.request
viene quindi risolto come oggetto di interesse sottostante.
- da quell'oggetto otteniamo l'
path
attributo
Quindi abbiamo visto come Local
, LocalProxy
e LocalStack
funziona, ora pensiamo per un momento alle implicazioni e alle sfumature nel recuperare il path
da:
- un
request
oggetto che sarebbe un semplice oggetto accessibile a livello globale.
- un
request
oggetto che sarebbe un locale.
- un
request
oggetto memorizzato come attributo di un locale.
- un
request
oggetto che è un proxy per un oggetto archiviato in un locale.
- un
request
oggetto memorizzato su uno stack, che a sua volta è archiviato in un locale.
- un
request
oggetto che è un proxy per un oggetto su uno stack archiviato in un locale. <- questo è ciò che fa Flask.