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 requestigaby 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.locali 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 Localobiekcie w tym samym czasie, ale dostęp local.first_namew 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. LocalPrzechowywania wtedy właśnie używa tego jako klucz do przechowywać żadnych danych kontekstowych do bieżącego wątku.
Można mieć wiele Localobiektów na proces i request, g, current_appa 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 LocalProxyobiekty. Co to jest LocalProxy?
LocalProxy
LocalProxy to obiekt, który pyta a, Localaby 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 LocalProxyobiektów globalnie dostępnych, zamiast tworzenia ich Localssamodzielnie, jest to, że upraszcza to zarządzanie nimi. Wystarczy jeden Localobiekt, 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 Localznajdzie 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, LocalProxygdy 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_appi sessionsą 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 LocalStackdo tego obiektów. Kiedy kończą swoją działalność, wyrywają kontekst ze stosu.
LocalStack
Tak LocalStackwyglą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 LocalStackto 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 sessionobiektów bezpośrednio do rozstrzygania LocalStack, to raczej wykorzystuje LocalProxyobiekty, które zawinąć funkcja wyszukiwania (zamiast Localobiektu), 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 RequestContextobiektu 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.pathz jednej z funkcji widoku wyglądałoby zatem następująco:
- zacznij od
LocalProxyobiektu 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
LocalStackobiektu _request_ctx_stacko najwyższy kontekst na stosie.
- aby znaleźć najwyższy kontekst,
LocalStackobiekt najpierw wysyła zapytanie do swojego wewnętrznego Localatrybutu ( self.local) o stackwłaściwość, która była tam wcześniej przechowywana.
- z
stacktego uzyskuje główny kontekst
- i
top.requesttym samym jest rozpatrywany jako podstawowy przedmiot zainteresowania.
- z tego obiektu otrzymujemy
pathatrybut
Tak jak widzieliśmy Local, LocalProxyi LocalStackpracę, teraz pomyśleć przez chwilę o konsekwencjach i niuansów przy pobieraniu pathz:
requestobiekt, który byłby prosty globalnie dostępny obiekt.
requestobiekt, który byłby lokalny.
requestprzedmiot przechowywany jako atrybut miejscowy.
requestobiekt, który jest pełnomocnikiem do obiektu przechowywane w lokalnej.
requestobiekt przechowywane na stosie, który z kolei jest przechowywany w lokalnym.
requestobiekt, który jest pełnomocnikiem do obiektu na stosie przechowywane w lokalnej. <- to właśnie robi Flask.