Jaki jest cel stosów kontekstów Flask?

158

Od jakiegoś czasu korzystam z kontekstu żądania / aplikacji, nie w pełni rozumiejąc, jak to działa i dlaczego zostało zaprojektowane tak, jak było. Jaki jest cel „stosu”, jeśli chodzi o żądanie lub kontekst aplikacji? Czy są to dwa oddzielne stosy, czy też oba są częścią jednego stosu? Czy kontekst żądania jest wypychany na stos, czy też sam jest stosem? Czy mogę przesuwać / przesuwać wiele kontekstów jeden na drugim? Jeśli tak, dlaczego miałbym to robić?

Przepraszam za wszystkie pytania, ale nadal jestem zdezorientowany po przeczytaniu dokumentacji dotyczącej kontekstu żądania i kontekstu aplikacji.

Ben Davis
źródło
5
kronosapiens.github.io/blog/2014/08/14/ ... IMO, ten post na blogu daje mi najbardziej zrozumiały opis kontekstu kolb.
mission.liao

Odpowiedzi:

243

Wiele aplikacji

Kontekst aplikacji (i jej cel) jest rzeczywiście mylący, dopóki nie zdasz sobie sprawy, że Flask może mieć wiele aplikacji. Wyobraź sobie sytuację, w której chcesz, aby jeden interpreter WSGI Python uruchamiał wiele aplikacji Flask. Nie mówimy tutaj o schematach, mówimy o zupełnie różnych aplikacjach Flask.

Możesz ustawić to podobnie do sekcji dokumentacji Flask w przykładzie „Wysyłanie aplikacji” :

from werkzeug.wsgi import DispatcherMiddleware
from frontend_app import application as frontend
from backend_app import application as backend

application = DispatcherMiddleware(frontend, {
    '/backend':     backend
})

Zauważ, że są tworzone dwie zupełnie różne aplikacje Flask „frontend” i „backend”. Innymi słowy, Flask(...)konstruktor aplikacji został wywołany dwukrotnie, tworząc dwie instancje aplikacji Flask.

Konteksty

Kiedy pracujesz z Flaskiem, często używasz zmiennych globalnych, aby uzyskać dostęp do różnych funkcji. Na przykład prawdopodobnie masz kod, który czyta ...

from flask import request

Następnie podczas przeglądania możesz użyć, requestaby uzyskać dostęp do informacji o bieżącym żądaniu. Oczywiście requestnie jest to normalna zmienna globalna; w rzeczywistości jest to kontekstowa wartość lokalna . Innymi słowy, za kulisami kryje się jakaś magia, która mówi: „kiedy dzwonię request.path, pobierz pathatrybut z requestobiektu żądania CURRENT”. Dwa różne żądania będą miały różne wyniki dla request.path.

W rzeczywistości, nawet jeśli uruchomisz Flask z wieloma wątkami, Flask jest wystarczająco inteligentny, aby izolować obiekty żądań. W ten sposób staje się możliwe, że dwa wątki, każdy obsługujący inne żądanie, jednocześnie wywołują request.pathi uzyskują prawidłowe informacje dla swoich żądań.

Łącząc to razem

Widzieliśmy już, że Flask może obsługiwać wiele aplikacji w tym samym interprecie, a także że ze względu na sposób, w jaki Flask pozwala na używanie globalnych "kontekstowo lokalnych", musi istnieć jakiś mechanizm określający, jakie jest "bieżące" żądanie ( w celu robienia rzeczy takich jak request.path).

Łącząc te pomysły razem, powinno również mieć sens, że Flask musi mieć jakiś sposób na określenie, jaka jest „bieżąca” aplikacja!

Prawdopodobnie masz również kod podobny do następującego:

from flask import url_for

Podobnie jak w naszym requestprzykładzie, url_forfunkcja ma logikę zależną od bieżącego środowiska. W tym przypadku jednak widać wyraźnie, że logika w dużym stopniu zależy od tego, która aplikacja jest uważana za „bieżącą” aplikację. W przykładzie frontend / backend pokazanym powyżej, zarówno aplikacje „frontend”, jak i „backend” mogą mieć trasę „/ login”, a więc url_for('/login')powinny zwracać coś innego w zależności od tego, czy widok obsługuje żądanie aplikacji frontend czy backend.

Aby odpowiedzieć na Twoje pytania ...

Jaki jest cel „stosu”, jeśli chodzi o żądanie lub kontekst aplikacji?

Z dokumentów kontekstu żądania:

Ponieważ kontekst żądania jest wewnętrznie obsługiwany jako stos, możesz wielokrotnie wypychać i popychać. Jest to bardzo przydatne do implementacji takich rzeczy, jak przekierowania wewnętrzne.

Innymi słowy, nawet jeśli na stosie „bieżących” żądań lub „bieżących” aplikacji będzie zazwyczaj 0 lub 1 elementów, możliwe, że będzie ich więcej.

Podany przykład dotyczy sytuacji, w której żądanie zwraca wyniki „wewnętrznego przekierowania”. Powiedzmy, że użytkownik żąda A, ale chcesz wrócić do użytkownika B. W większości przypadków wydajesz przekierowanie do użytkownika i kierujesz go do zasobu B, co oznacza, że ​​użytkownik uruchomi drugie żądanie pobrania B. A Nieco innym sposobem obsługi tego byłoby wykonanie wewnętrznego przekierowania, co oznacza, że ​​podczas przetwarzania A Flask wyśle ​​do siebie nowe żądanie dotyczące zasobu B i użyje wyników tego drugiego żądania jako wyników pierwotnego żądania użytkownika.

Czy są to dwa oddzielne stosy, czy też oba są częścią jednego stosu?

Są to dwa oddzielne stosy . Jest to jednak szczegół implementacji. Co ważniejsze, nie tyle istnieje stos, ale fakt, że w każdej chwili możesz pobrać „bieżącą” aplikację lub żądanie (szczyt stosu).

Czy kontekst żądania jest wypychany na stos, czy też sam jest stosem?

„Kontekst żądania” jest jednym z elementów „stosu kontekstów żądania”. Podobnie jest z „kontekstem aplikacji” i „stosem kontekstów aplikacji”.

Czy mogę przesuwać / przesuwać wiele kontekstów jeden na drugim? Jeśli tak, dlaczego miałbym to robić?

W aplikacji Flask zazwyczaj tego nie robisz. Przykładem tego, gdzie możesz chcieć, jest wewnętrzne przekierowanie (opisane powyżej). Jednak nawet w takim przypadku prawdopodobnie skończysz, gdy Flask zajmie się nowym żądaniem, więc Flask wykona za Ciebie wszystkie popychanie / popping.

Istnieją jednak przypadki, w których chcesz samodzielnie manipulować stosem.

Uruchamianie kodu poza żądaniem

Jednym z typowych problemów jest to, że używają rozszerzenia Flask-SQLAlchemy do konfigurowania bazy danych SQL i definicji modelu przy użyciu kodu podobnego do tego, co pokazano poniżej ...

app = Flask(__name__)
db = SQLAlchemy() # Initialize the Flask-SQLAlchemy extension object
db.init_app(app)

Następnie używają wartości appi dbw skrypcie, który powinien zostać uruchomiony z powłoki. Na przykład skrypt „setup_tables.py” ...

from myapp import app, db

# Set up models
db.create_all()

W takim przypadku rozszerzenie Flask-SQLAlchemy wie o appaplikacji, ale w trakcie create_all()tego wyrzuci błąd informujący o braku kontekstu aplikacji. Ten błąd jest uzasadniony; nigdy nie powiedziałeś Flaskowi, z jaką aplikacją powinien mieć do czynienia podczas uruchamiania create_allmetody.

Możesz się zastanawiać, dlaczego nie potrzebujesz tego with app.app_context()wywołania, gdy uruchamiasz podobne funkcje w swoich widokach. Powodem jest to, że Flask już obsługuje zarządzanie kontekstem aplikacji za Ciebie, gdy obsługuje rzeczywiste żądania sieciowe. Problem pojawia się naprawdę tylko poza tymi funkcjami widoku (lub innymi takimi wywołaniami zwrotnymi), na przykład podczas używania modeli w jednorazowym skrypcie.

Rozwiązaniem jest samodzielne wypchnięcie kontekstu aplikacji, co można zrobić, wykonując ...

from myapp import app, db

# Set up models
with app.app_context():
    db.create_all()

Spowoduje to wypchnięcie nowego kontekstu aplikacji (używając aplikacji apppamiętaj, że może istnieć więcej niż jedna aplikacja).

Testowanie

Innym przypadkiem, w którym chciałbyś manipulować stosem, jest testowanie. Możesz utworzyć test jednostkowy, który obsługuje żądanie i sprawdzić wyniki:

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
Mark Hildreth
źródło
3
To nadal jest dla mnie mylące! Dlaczego nie mieć jednego kontekstu żądania i zastąpić go, jeśli chcesz wykonać wewnętrzne przekierowanie. Wydaje mi się, że projekt jest przejrzysty.
Maarten
@Maarten Jeśli podczas obsługi żądania A wyślesz żądanie B i żądanie B zastąpi żądanie A na stosie, obsługa żądania A nie może się zakończyć. Jednak nawet jeśli zastosowałeś strategię zastępowania, jak sugerowałeś, i nie masz stosu (co oznacza, że ​​wewnętrzne przekierowania byłyby trudniejsze), nie zmienia to tak naprawdę faktu, że konteksty aplikacji i żądań są wymagane do odizolowania obsługi żądań.
Mark Hildreth
Dobre wytłumaczenie! Ale nadal jestem trochę zagubiony w kwestii: „Kontekst aplikacji jest tworzony i niszczony w razie potrzeby. Nigdy nie przenosi się między wątkami i nie będzie współdzielony między żądaniami”. W dokumencie kolby. Dlaczego „kontekst aplikacji” nie jest utrzymywany razem z aplikacją?
jayven
1
Przydałby się przykład wewnętrznego przekierowania w Flasku, ponieważ wyszukiwanie go w Google nie pojawia się zbyt często. Gdyby nie to, czy prostszy request = Local()projekt nie byłby wystarczający dla global.py? Prawdopodobnie są przypadki użycia, o których nie myślę.
QuadrupleA
Czy podczas importowania widoków można wypychać kontekst aplikacji do metody fabrycznej? Ponieważ widoki zawierają trasy odwołujące się do current_app, potrzebuję kontekstu.
zmienna
48

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.
Michael Ekoka
źródło
4
Doskonałe podsumowanie, studiowałem kod w flask / globals.py i werkzeug / local.py i to pomaga mi lepiej go zrozumieć. Mój pajęczy zmysł mówi mi, że jest to zbyt skomplikowany projekt, ale przyznaję, że nie rozumiem wszystkich zastosowań, do których jest przeznaczony. „Wewnętrzne przekierowania” to jedyne uzasadnienie, jakie widziałem w opisach powyżej, a wyszukiwanie w Google „wewnętrzne przekierowanie kolby” nie pojawia się zbyt często, więc nadal jestem trochę zagubiony. Jedną z rzeczy, które lubię w kolbie jest to, że generalnie nie jest to obiekt typu zupa java, pełen AbstractProviderContextBaseFactories i tym podobne.
QuadrupleA
1
@QuadrupleA Kiedy zrozumiesz, jak te Local, LocalStacki LocalProxypraca, sugeruję, aby ponownie Te artykuły Doc: flask.pocoo.org/docs/0.11/appcontext , flask.pocoo.org/docs/0.11/extensiondev i flask.pocoo .org / docs / 0.11 / reqcontext . Twój świeży chwyt może pozwolić ci zobaczyć je w nowym świetle i może zapewnić lepszy wgląd.
Michael Ekoka
Przeczytaj te linki - przeważnie mają sens, ale projekt wciąż wydaje mi się zbyt skomplikowany i być może zbyt sprytny dla własnego dobra. Ale ogólnie nie jestem wielkim fanem OOP i niejawnej kontroli przepływu (nadpisywanie __call __ (), __getattr __ (), dynamiczne wysyłanie zdarzeń w porównaniu z prostymi wywołaniami funkcji, zawijanie rzeczy w metody dostępu do właściwości zamiast zwykłego używania atrybutu itp. .), więc może to tylko różnica w filozofii. Nie jest też praktykiem TDD, co wydaje się być przeznaczone do wspierania wielu z tych dodatkowych mechanizmów.
QuadrupleA
1
Dziękuję za udostępnienie tego. Wątkowanie jest słabością języków takich jak Python - kończysz z wzorcami takimi jak powyżej, które przeciekają do struktur aplikacji i które również nie są w pełni skalowalne. Java jest kolejnym przykładem w podobnej sytuacji dot. Threadlocals, semaphors etc. Notorycznie trudne do poprawienia lub utrzymania. W tym przypadku języki takie jak Erlang / Elixir (przy użyciu BEAM) lub podejścia do pętli zdarzeń (np. Nginx vs apache itp.) Zazwyczaj oferują potężniejsze, skalowalne i mniej złożone podejście.
arcseldon
13

Mały dodatek @Mark Hildreth 's odpowiedź.

Kontekst stosu wyglądać {thread.get_ident(): []}, gdzie []nazywa się „stos”, ponieważ wykorzystywane tylko append( push) popi [-1]( __getitem__(-1)) operacje. Zatem stos kontekstowy zachowa aktualne dane dla wątku lub wątku Greenlet.

current_app, g, request, sessionI itp jest LocalProxyobiekt, który właśnie overrided specjalnych metod __getattr__, __getitem__, __call__, __eq__itp i wartość powrotna z kontekstu stosie górnej ( [-1]) o nazwą zmiennej ( current_app, requestna przykład). LocalProxytrzeba raz zaimportować te obiekty i nie przegapią one aktualności. Więc lepiej po prostu importuj requestgdziekolwiek jesteś w kodzie, zamiast bawić się wysyłaniem argumentu żądania do funkcji i metod. Możesz z łatwością napisać własne rozszerzenia, ale nie zapominaj, że niepoważne użycie może utrudnić zrozumienie kodu.

Poświęć czas na zrozumienie https://github.com/mitsuhiko/werkzeug/blob/master/werkzeug/local.py .

Jak więc zapełniono oba stosy? Na zapytanie Flask:

  1. utwórz request_contextwedług środowiska (init map_adapter, match path)
  2. wprowadź lub wypchnij to żądanie:
    1. wyczyść poprzednie request_context
    2. utworzyć, app_contextjeśli pominięto go i przekazano do stosu kontekstów aplikacji
    3. to żądanie przekazane do żądania stosu kontekstu
    4. init sesja, jeśli została pominięta
  3. żądanie wysyłki
  4. wyczyść żądanie i zdejmij je ze stosu
tbicr
źródło
2

Weźmy jeden przykład, załóżmy, że chcesz ustawić kontekst użytkownika (używając konstrukcji kolbowej Local i LocalProxy).

Zdefiniuj jedną klasę użytkownika:

class User(object):
    def __init__(self):
        self.userid = None

zdefiniuj funkcję do pobierania obiektu użytkownika w bieżącym wątku lub greenletu

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

Teraz zdefiniuj LocalProxy

usercontext = LocalProxy(partial(get_user, Local()))

Teraz, aby uzyskać identyfikator użytkownika w bieżącym wątku usercontext.userid

wyjaśnienie:

1. Lokalny ma dykt tożsamości i objet, tożsamość to identyfikator wątku lub zielony, w tym przykładzie _local.user = User () jest równoważne z _local .___ storage __ [identyfikator bieżącego wątku] ["user"] = User ()

  1. LocalProxy deleguje operację do opakowanego obiektu lokalnego lub można dostarczyć funkcję, która zwraca obiekt docelowy. W powyższym przykładzie funkcja get_user dostarcza obiekt bieżącego użytkownika do LocalProxy, a kiedy pytasz o identyfikator bieżącego użytkownika przez usercontext.userid, funkcja __getattr__ LocalProxy najpierw wywołuje get_user w celu pobrania obiektu użytkownika (użytkownika), a następnie wywołuje getattr (użytkownik, „identyfikator_użytkownika”). aby ustawić identyfikator użytkownika na User (w bieżącym wątku lub greenlet), po prostu wykonaj: usercontext.userid = "user_123"
Ratn Deo - Dev
źródło