Poprzednie odpowiedzi już dają ładny przegląd tego, co dzieje się w tle Flaska podczas żądania. Jeśli jeszcze tego nie czytałeś, polecam odpowiedź @ MarkHildreth przed przeczytaniem tego. Krótko mówiąc, dla każdego żądania http tworzony jest nowy kontekst (wątek), dlatego konieczne jest posiadanie funkcji wątku Local
, która zezwala na obiekty takie jak request
ig
aby były dostępne globalnie we wszystkich wątkach, przy jednoczesnym zachowaniu określonego kontekstu żądań. Co więcej, podczas przetwarzania żądania http Flask może emulować dodatkowe żądania od wewnątrz, stąd konieczność przechowywania ich kontekstu na stosie. Ponadto Flask umożliwia uruchamianie wielu aplikacji wsgi w ramach jednego procesu, a więcej niż jedną można wywołać podczas żądania (każde żądanie tworzy nowy kontekst aplikacji), stąd potrzeba stosu kontekstów dla aplikacji. To podsumowanie tego, co zostało omówione w poprzednich odpowiedziach.
Moim celem jest teraz uzupełnienie naszej obecnej wiedzy, wyjaśniając, w jaki sposób Flask i Werkzeug robią to, co robią z lokalnymi kontekstami. Uprościłem kod, aby lepiej zrozumieć jego logikę, ale jeśli to zrozumiesz, powinieneś być w stanie łatwo uchwycić większość tego, co jest w rzeczywistym źródle ( werkzeug.local
i flask.globals
).
Najpierw zrozumiemy, jak Werkzeug implementuje Thread Locals.
Lokalny
Kiedy przychodzi żądanie http, jest przetwarzane w kontekście pojedynczego wątku. Jako alternatywny sposób tworzenia nowego kontekstu podczas żądania http, Werkzeug pozwala również na użycie zielonych wątków (rodzaj lżejszych „mikro-wątków”) zamiast zwykłych wątków. Jeśli nie masz zainstalowanych greenlets, powróci do używania wątków. Każdy z tych wątków (lub zielonych ulotek) można zidentyfikować za pomocą unikalnego identyfikatora, który można pobrać za pomocą get_ident()
funkcji modułu . Funkcja ta jest punktem wyjścia do magii za posiadających request
, current_app
, url_for
, g
, i innych tego typu obiektów globalnych kontekst związany.
try:
from greenlet import get_ident
except ImportError:
from thread import get_ident
Teraz, gdy mamy naszą funkcję tożsamości, możemy wiedzieć, w którym wątku jesteśmy w danym momencie i możemy stworzyć tak zwany wątek Local
, obiekt kontekstowy, do którego można uzyskać dostęp globalnie, ale kiedy uzyskujesz dostęp do jego atrybutów, rozwiązują one ich wartość dla ten konkretny wątek. na przykład
# globally
local = Local()
# ...
# on thread 1
local.first_name = 'John'
# ...
# on thread 2
local.first_name = 'Debbie'
Obie wartości są obecne w globalnie dostępnym Local
obiekcie w tym samym czasie, ale dostęp local.first_name
w kontekście wątku 1 da ci 'John'
, podczas gdy powróci 'Debbie'
w wątku 2.
Jak to możliwe? Spójrzmy na jakiś (uproszczony) kod:
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()
Z powyższego kodu możemy zobaczyć, że magia sprowadza się do get_ident()
tego, że identyfikuje aktualną greenlet lub wątek. Local
Przechowywania wtedy właśnie używa tego jako klucz do przechowywać żadnych danych kontekstowych do bieżącego wątku.
Można mieć wiele Local
obiektów na proces i request
, g
, current_app
a inni mogą po prostu zostały stworzone w taki sposób. Ale nie tak to się robi w Flasku, w którym nie są to technicznie Local
obiekty, ale dokładniej LocalProxy
obiekty. Co to jest LocalProxy
?
LocalProxy
LocalProxy to obiekt, który pyta a, Local
aby znaleźć inny interesujący obiekt (tj. Obiekt, do którego pośredniczy). Spójrzmy, aby zrozumieć:
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 ...
Teraz, aby utworzyć globalnie dostępne serwery proxy, należy zrobić
# this would happen some time near application start-up
local = Local()
request = LocalProxy(local, 'request')
g = LocalProxy(local, 'g')
a teraz jakiś czas wcześniej w trakcie żądania możesz przechowywać niektóre obiekty wewnątrz lokalnego, do którego mają dostęp wcześniej utworzone proxy, bez względu na to, w którym wątku jesteśmy
# this would happen early during processing of an http request
local.request = RequestContext(http_environment)
local.g = SomeGeneralPurposeContainer()
Zaletą używania LocalProxy
obiektów globalnie dostępnych, zamiast tworzenia ich Locals
samodzielnie, jest to, że upraszcza to zarządzanie nimi. Wystarczy jeden Local
obiekt, aby utworzyć wiele globalnie dostępnych serwerów proxy. Na końcu żądania, podczas czyszczenia, po prostu zwalniasz ten Local
(tj. Usuwasz context_id z jego pamięci) i nie przejmujesz się serwerami proxy, są one nadal dostępne globalnie i nadal odkładają na ten, kto Local
znajdzie ich obiekt zainteresowania dla kolejnych żądań http.
# this would happen some time near the end of request processing
release(local) # aka local.__release_local__()
Aby uprościć tworzenie, LocalProxy
gdy już mamy Local
, Werkzeug implementuje Local.__call__()
magiczną metodę w następujący sposób:
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')
Jednak, jeśli spojrzeć w źródle kolbę (flask.globals), które nie jest jeszcze jak request
, g
, current_app
i session
są tworzone. Jak ustaliliśmy, Flask może generować wiele „fałszywych” żądań (z jednego prawdziwego żądania http), a w trakcie tego procesu także przekazywać wiele kontekstów aplikacji. To nie jest typowy przypadek użycia, ale jest to zdolność platformy. Ponieważ te „współbieżne” żądania i aplikacje są nadal ograniczone do działania, a tylko jedno ma „fokus” w dowolnym momencie, sensowne jest użycie stosu dla ich odpowiedniego kontekstu. Za każdym razem, gdy pojawia się nowe żądanie lub wywoływana jest jedna z aplikacji, umieszczają kontekst na górze odpowiedniego stosu. Flask używa LocalStack
do tego obiektów. Kiedy kończą swoją działalność, wyrywają kontekst ze stosu.
LocalStack
Tak LocalStack
wygląda a (ponownie kod jest uproszczony, aby ułatwić zrozumienie jego logiki).
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
Zauważ z powyższego, że a LocalStack
to stos przechowywany w lokalnym, a nie zbiór lokalnych przechowywanych na stosie. Oznacza to, że chociaż stos jest dostępny globalnie, w każdym wątku jest inny stos.
Kolba nie posiada request
, current_app
, g
, i session
obiektów bezpośrednio do rozstrzygania LocalStack
, to raczej wykorzystuje LocalProxy
obiekty, które zawinąć funkcja wyszukiwania (zamiast Local
obiektu), która znajdzie bazowego obiektu z 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)
Wszystkie te są deklarowane podczas uruchamiania aplikacji, ale w rzeczywistości nie są rozwiązywane do niczego, dopóki kontekst żądania lub kontekst aplikacji nie zostanie przesłany do odpowiedniego stosu.
Jeśli jesteś ciekawy, w jaki sposób kontekst jest faktycznie wstawiany do stosu (a następnie wyskakiwany), spójrz, w flask.app.Flask.wsgi_app()
którym miejscu znajduje się punkt wejścia aplikacji wsgi (tj. Co wywołuje serwer sieciowy i przekazuje środowisko http do request przychodzi) i śledź tworzenie RequestContext
obiektu przez wszystkie jego kolejne push()
etapy _request_ctx_stack
. Po umieszczeniu na szczycie stosu jest dostępny za pośrednictwem _request_ctx_stack.top
. Oto skrócony kod, aby zademonstrować przepływ:
Więc uruchamiasz aplikację i udostępniasz ją na serwerze WSGI ...
app = Flask(*config, **kwconfig)
# ...
Później przychodzi żądanie http i serwer WSGI wywołuje aplikację ze zwykłymi parametrami ...
app(environ, start_response) # aka app.__call__(environ, start_response)
Tak z grubsza dzieje się w aplikacji ...
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()
# ...
i tak z grubsza dzieje się z 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()
Powiedzmy, że żądanie zostało zainicjowane, wyszukiwanie request.path
z jednej z funkcji widoku wyglądałoby zatem następująco:
- zacznij od
LocalProxy
obiektu dostępnego globalnie request
.
- aby znaleźć podstawowy obiekt zainteresowania (obiekt, do którego jest skierowany), wywołuje swoją funkcję wyszukiwania
_find_request()
(funkcję, którą zarejestrował jako swoją self.local
).
- ta funkcja wysyła zapytanie do
LocalStack
obiektu _request_ctx_stack
o najwyższy kontekst na stosie.
- aby znaleźć najwyższy kontekst,
LocalStack
obiekt najpierw wysyła zapytanie do swojego wewnętrznego Local
atrybutu ( self.local
) o stack
właściwość, która była tam wcześniej przechowywana.
- z
stack
tego uzyskuje główny kontekst
- i
top.request
tym samym jest rozpatrywany jako podstawowy przedmiot zainteresowania.
- z tego obiektu otrzymujemy
path
atrybut
Tak jak widzieliśmy Local
, LocalProxy
i LocalStack
pracę, teraz pomyśleć przez chwilę o konsekwencjach i niuansów przy pobieraniu path
z:
request
obiekt, który byłby prosty globalnie dostępny obiekt.
request
obiekt, który byłby lokalny.
request
przedmiot przechowywany jako atrybut miejscowy.
request
obiekt, który jest pełnomocnikiem do obiektu przechowywane w lokalnej.
request
obiekt przechowywane na stosie, który z kolei jest przechowywany w lokalnym.
request
obiekt, który jest pełnomocnikiem do obiektu na stosie przechowywane w lokalnej. <- to właśnie robi Flask.