Was ist der Zweck von Flask 'Kontextstapeln?

157

Ich verwende den Anforderungs- / Anwendungskontext seit einiger Zeit, ohne vollständig zu verstehen, wie er funktioniert oder warum er so entworfen wurde, wie er war. Was ist der Zweck des "Stacks", wenn es um den Anforderungs- oder Anwendungskontext geht? Sind diese beiden getrennten Stapel oder sind sie beide Teil eines Stapels? Wird der Anforderungskontext auf einen Stapel verschoben oder handelt es sich um einen Stapel selbst? Kann ich mehrere Kontexte übereinander schieben / platzieren? Wenn ja, warum sollte ich das tun wollen?

Entschuldigung für alle Fragen, aber ich bin immer noch verwirrt, nachdem ich die Dokumentation für Anforderungskontext und Anwendungskontext gelesen habe.

Ben Davis
quelle
5
kronosapiens.github.io/blog/2014/08/14/… IMO, dieser Blog-Beitrag gibt mir die verständlichste Beschreibung des Kolbenkontexts.
Mission.liao

Antworten:

242

Mehrere Apps

Der Anwendungskontext (und sein Zweck) ist in der Tat verwirrend, bis Sie erkennen, dass Flask mehrere Apps haben kann. Stellen Sie sich die Situation vor, in der ein einzelner WSGI Python-Interpreter mehrere Flask-Anwendungen ausführen soll. Wir sprechen hier nicht von Blaupausen, sondern von ganz anderen Flask-Anwendungen.

Sie können dies ähnlich wie im Abschnitt zur Kolbendokumentation im Beispiel "Application Dispatching" einrichten :

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

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

Beachten Sie, dass zwei völlig unterschiedliche Flask-Anwendungen "Frontend" und "Backend" erstellt werden. Mit anderen Worten, der Flask(...)Anwendungskonstruktor wurde zweimal aufgerufen, wodurch zwei Instanzen einer Flask-Anwendung erstellt wurden.

Kontexte

Wenn Sie mit Flask arbeiten, verwenden Sie häufig globale Variablen, um auf verschiedene Funktionen zuzugreifen. Zum Beispiel haben Sie wahrscheinlich Code, der lautet ...

from flask import request

Während einer Ansicht können Sie requestdann auf die Informationen der aktuellen Anforderung zugreifen. Offensichtlich requestist keine normale globale Variable; In Wirklichkeit ist es ein lokaler Kontextwert . Mit anderen Worten, hinter den Kulissen steckt etwas Magisches, das besagt: "Wenn ich anrufe request.path, erhalte ich das pathAttribut vom requestObjekt der CURRENT-Anforderung." Zwei verschiedene Anfragen haben unterschiedliche Ergebnisse für request.path.

Selbst wenn Sie Flask mit mehreren Threads ausführen, ist Flask intelligent genug, um die Anforderungsobjekte isoliert zu halten. Auf diese Weise können zwei Threads, die jeweils eine andere Anforderung bearbeiten, gleichzeitig request.pathdie richtigen Informationen für ihre jeweiligen Anforderungen aufrufen und abrufen .

Etwas zusammensetzen

Wir haben also bereits gesehen, dass Flask mehrere Anwendungen in demselben Interpreter verarbeiten kann und dass aufgrund der Art und Weise, wie Flask die Verwendung von "kontextlokalen" Globals ermöglicht, ein Mechanismus vorhanden sein muss, um zu bestimmen, wie die "aktuelle" Anforderung lautet ( um Dinge zu tun wie request.path).

Wenn man diese Ideen zusammenfasst, sollte es auch Sinn machen, dass Flask eine Möglichkeit haben muss, um zu bestimmen, was die "aktuelle" Anwendung ist!

Sie haben wahrscheinlich auch Code ähnlich dem folgenden:

from flask import url_for

Wie in unserem requestBeispiel verfügt die url_forFunktion über eine Logik, die von der aktuellen Umgebung abhängt. In diesem Fall ist jedoch klar, dass die Logik stark davon abhängt, welche App als "aktuelle" App betrachtet wird. In dem oben gezeigten Frontend / Backend-Beispiel können sowohl die "Frontend" - als auch die "Backend" -App eine "/ login" -Route haben und url_for('/login')sollten daher etwas anderes zurückgeben, je nachdem, ob die Ansicht die Anforderung für die Frontend- oder Backend-App verarbeitet.

Um Ihre Fragen zu beantworten ...

Was ist der Zweck des "Stacks", wenn es um den Anforderungs- oder Anwendungskontext geht?

Aus den Anforderungskontextdokumenten:

Da der Anforderungskontext intern als Stapel verwaltet wird, können Sie mehrere Male Push- und Pop-Vorgänge ausführen. Dies ist sehr praktisch, um Dinge wie interne Weiterleitungen zu implementieren.

Mit anderen Worten, obwohl Sie normalerweise 0 oder 1 Elemente auf diesem Stapel von "aktuellen" Anforderungen oder "aktuellen" Anwendungen haben, ist es möglich, dass Sie mehr haben.

In dem angegebenen Beispiel würde Ihre Anfrage die Ergebnisse einer "internen Umleitung" zurückgeben. Angenommen, ein Benutzer fordert A an, aber Sie möchten zu Benutzer B zurückkehren. In den meisten Fällen geben Sie eine Umleitung an den Benutzer aus und verweisen den Benutzer auf Ressource B, was bedeutet, dass der Benutzer eine zweite Anforderung zum Abrufen von B ausführt. A. Eine etwas andere Art, dies zu handhaben, wäre eine interne Umleitung. Dies bedeutet, dass Flask während der Verarbeitung von A eine neue Anforderung für Ressource B an sich selbst stellt und die Ergebnisse dieser zweiten Anforderung als Ergebnisse der ursprünglichen Anforderung des Benutzers verwendet.

Sind diese beiden getrennten Stapel oder sind sie beide Teil eines Stapels?

Sie sind zwei separate Stapel . Dies ist jedoch ein Implementierungsdetail. Was wichtiger ist, ist nicht so sehr, dass es einen Stapel gibt, sondern die Tatsache, dass Sie jederzeit die "aktuelle" App oder Anfrage (oben auf dem Stapel) erhalten können.

Wird der Anforderungskontext auf einen Stapel verschoben oder handelt es sich um einen Stapel selbst?

Ein "Anforderungskontext" ist ein Element des "Anforderungskontextstapels". Ähnliches gilt für den "App-Kontext" und den "App-Kontext-Stack".

Kann ich mehrere Kontexte übereinander schieben / platzieren? Wenn ja, warum sollte ich das tun wollen?

In einer Flask-Anwendung würden Sie dies normalerweise nicht tun. Ein Beispiel dafür, wo Sie möchten, ist eine interne Umleitung (siehe oben). Selbst in diesem Fall würde Flask wahrscheinlich eine neue Anfrage bearbeiten, und Flask würde das ganze Pushing / Popping für Sie erledigen.

Es gibt jedoch einige Fälle, in denen Sie den Stapel selbst bearbeiten möchten.

Ausführen von Code außerhalb einer Anforderung

Ein typisches Problem besteht darin, dass sie die Flask-SQLAlchemy-Erweiterung verwenden, um eine SQL-Datenbank und eine Modelldefinition mithilfe von Code wie dem unten gezeigten einzurichten.

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

Dann verwenden sie die Werte appund dbin einem Skript, das von der Shell ausgeführt werden soll. Zum Beispiel ein "setup_tables.py" -Skript ...

from myapp import app, db

# Set up models
db.create_all()

In diesem Fall kennt die Flask-SQLAlchemy-Erweiterung die appAnwendung, gibt jedoch create_all()einen Fehler aus, der sich darüber beschwert, dass kein Anwendungskontext vorhanden ist. Dieser Fehler ist gerechtfertigt; Sie haben Flask nie gesagt, mit welcher Anwendung es sich beim Ausführen der create_allMethode befassen soll .

Sie fragen sich möglicherweise, warum Sie diesen with app.app_context()Aufruf nicht benötigen , wenn Sie ähnliche Funktionen in Ihren Ansichten ausführen. Der Grund dafür ist, dass Flask die Verwaltung des Anwendungskontexts bereits für Sie übernimmt, wenn es um tatsächliche Webanforderungen geht. Das Problem tritt wirklich nur außerhalb dieser Ansichtsfunktionen (oder anderer solcher Rückrufe) auf, z. B. wenn Sie Ihre Modelle in einem einmaligen Skript verwenden.

Die Lösung besteht darin, den Anwendungskontext selbst zu verschieben, was durch ...

from myapp import app, db

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

Dadurch wird ein neuer Anwendungskontext erstellt (bei Verwendung der Anwendung von appdenken Sie daran, dass möglicherweise mehr als eine Anwendung vorhanden ist).

Testen

Ein anderer Fall, in dem Sie den Stapel manipulieren möchten, ist das Testen. Sie können einen Komponententest erstellen, der eine Anforderung verarbeitet, und die Ergebnisse überprüfen:

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
quelle
3
Das verwirrt mich immer noch! Warum nicht einen einzigen Anforderungskontext haben und ihn ersetzen, wenn Sie eine interne Umleitung durchführen möchten? Scheint mir ein klares Design zu sein.
Maarten
@Maarten Wenn Sie während der Bearbeitung von Anforderung A Anforderung B stellen und Anforderung B Anforderung A auf dem Stapel ersetzt, kann die Verarbeitung für Anforderung A nicht abgeschlossen werden. Selbst wenn Sie die von Ihnen vorgeschlagene Ersetzungsstrategie durchgeführt haben und keinen Stapel hatten (was bedeutet, dass interne Weiterleitungen schwieriger wären), ändert dies nichts an der Tatsache, dass App- und Anforderungskontexte erforderlich sind, um die Bearbeitung von Anforderungen zu isolieren.
Mark Hildreth
Gute Erklärung! Aber ich bin immer noch etwas verwirrend in Bezug auf: "Der Anwendungskontext wird nach Bedarf erstellt und zerstört. Er bewegt sich nie zwischen Threads und wird nicht zwischen Anforderungen geteilt." Im Dokument der Flasche. Warum bleibt ein "Anwendungskontext" nicht zusammen mit der App bestehen?
Jayven
1
Ein Beispiel für eine interne Weiterleitung in Flask wäre hilfreich, da das Googeln nicht viel ergibt. Wenn nicht, würde ein einfacheres request = Local()Design für global.py nicht ausreichen? Es gibt wahrscheinlich Anwendungsfälle, an die ich nicht denke.
QuadrupleA
Ist es in Ordnung, den App-Kontext beim Importieren der Ansichten in die Factory-Methode zu verschieben? Da Ansichten Routen enthalten, die auf current_app verweisen, benötige ich den Kontext.
Variable
48

Frühere Antworten geben bereits einen schönen Überblick darüber, was während einer Anfrage im Hintergrund von Flask vor sich geht. Wenn Sie es noch nicht gelesen haben, empfehle ich die Antwort von @ MarkHildreth, bevor Sie dies lesen. Kurz gesagt, für jede http-Anforderung wird ein neuer Kontext (Thread) erstellt, weshalb eine Thread-Funktion erforderlich ist Local, die Objekte wie requestund zulässtgglobaler Zugriff über Threads hinweg unter Beibehaltung des anforderungsspezifischen Kontexts. Darüber hinaus kann Flask während der Verarbeitung einer http-Anfrage zusätzliche Anfragen von innen emulieren, weshalb der jeweilige Kontext auf einem Stapel gespeichert werden muss. Außerdem ermöglicht Flask, dass mehrere wsgi-Anwendungen innerhalb eines einzelnen Prozesses nebeneinander ausgeführt werden, und während einer Anforderung können mehrere aufgerufen werden (jede Anforderung erstellt einen neuen Anwendungskontext), sodass ein Kontextstapel für Anwendungen erforderlich ist. Das ist eine Zusammenfassung dessen, was in früheren Antworten behandelt wurde.

Mein Ziel ist es nun, unser derzeitiges Verständnis zu ergänzen, indem ich erkläre, wie Flask und Werkzeug das tun, was sie mit diesen Kontext-Einheimischen tun. Ich habe den Code vereinfacht, um das Verständnis seiner Logik zu verbessern, aber wenn Sie dies erhalten, sollten Sie in der Lage sein, die meisten Inhalte der tatsächlichen Quelle ( werkzeug.localund flask.globals) leicht zu erfassen .

Lassen Sie uns zunächst verstehen, wie Werkzeug Thread-Locals implementiert.

Lokal

Wenn eine http-Anfrage eingeht, wird sie im Kontext eines einzelnen Threads verarbeitet. Als alternatives Mittel, um während einer http-Anfrage einen neuen Kontext zu erzeugen, erlaubt Werkzeug auch die Verwendung von Greenlets (eine Art leichterer "Mikro-Threads") anstelle von normalen Threads. Wenn Sie keine Greenlets installiert haben, werden stattdessen wieder Threads verwendet. Jeder dieser Threads (oder Greenlets) ist durch eine eindeutige ID erkennbar, die Sie mit der get_ident()Funktion des Moduls abrufen können. Diese Funktion ist der Ausgangspunkt , um die Magie hinter mit request, current_app, url_for, gund andere solcher kontextgebundene globalen Objekten.

try:
    from greenlet import get_ident
except ImportError:
    from thread import get_ident

Nachdem wir nun unsere Identitätsfunktion haben, können wir wissen, in welchem ​​Thread wir uns zu einem bestimmten Zeitpunkt befinden, und wir können einen sogenannten Thread erstellen Local, ein Kontextobjekt, auf das global zugegriffen werden kann. Wenn Sie jedoch auf seine Attribute zugreifen, werden sie in ihren Wert für aufgelöst dieser spezifische Thread. z.B

# globally
local = Local()

# ...

# on thread 1
local.first_name = 'John'

# ...

# on thread 2
local.first_name = 'Debbie'

Beide Werte sind gleichzeitig auf dem global zugänglichen LocalObjekt vorhanden, aber der Zugriff local.first_nameim Kontext von Thread 1 gibt Ihnen die Möglichkeit 'John', während er 'Debbie'auf Thread 2 zurückkehrt.

Wie ist das möglich? Schauen wir uns einen (vereinfachten) Code an:

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()

Aus dem obigen Code können wir erkennen, dass die Magie darauf get_ident()hinausläuft, das aktuelle Greenlet oder den aktuellen Thread zu identifizieren. Der LocalSpeicher verwendet dies dann nur als Schlüssel zum Speichern von Daten, die für den aktuellen Thread kontextbezogen sind.

Sie können mehrere LocalObjekte pro Prozess und requesthaben g, current_appund andere könnten einfach so erstellt worden sein. Aber so wird es in Flask nicht gemacht , wo es sich nicht um technische Local Objekte handelt, sondern um genauere LocalProxyObjekte. Was ist ein LocalProxy?

LocalProxy

Ein LocalProxy ist ein Objekt, das a abfragt Local, um ein anderes interessierendes Objekt zu finden (dh das Objekt, für das es einen Proxy erstellt). Lassen Sie uns einen Blick darauf werfen, um zu verstehen:

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 ...

Nun würden Sie global zugängliche Proxys erstellen

# this would happen some time near application start-up
local = Local()
request = LocalProxy(local, 'request')
g = LocalProxy(local, 'g')

und jetzt, einige Zeit früher im Verlauf einer Anfrage, würden Sie einige Objekte in der lokalen speichern, auf die die zuvor erstellten Proxys zugreifen können, unabhängig davon, auf welchem ​​Thread wir uns befinden

# this would happen early during processing of an http request
local.request = RequestContext(http_environment)
local.g = SomeGeneralPurposeContainer()

Der Vorteil der Verwendung LocalProxyals global zugängliche Objekte, anstatt sie Localsselbst zu erstellen, besteht darin, dass ihre Verwaltung vereinfacht wird. Sie benötigen nur ein einziges LocalObjekt, um viele global zugängliche Proxys zu erstellen. Am Ende der Anforderung geben Sie während der Bereinigung einfach die Freigabe frei Local(dh Sie löschen die context_id aus ihrem Speicher) und kümmern sich nicht um die Proxys. Sie sind weiterhin global zugänglich und verschieben sich immer noch auf diejenige Local, um ihr Objekt zu finden von Interesse für nachfolgende http-Anfragen.

# this would happen some time near the end of request processing
release(local) # aka local.__release_local__()

Um die Erstellung eines zu vereinfachen, LocalProxywenn wir bereits ein haben Local, implementiert Werkzeug die Local.__call__()magische Methode wie folgt:

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')

Doch wenn man sich in der Flasche Quelle (flask.globals) , das ist immer noch nicht , wie request, g, current_appund sessionerstellt werden. Wie wir festgestellt haben, kann Flask mehrere "gefälschte" Anforderungen (aus einer einzigen echten http-Anforderung) erzeugen und dabei auch mehrere Anwendungskontexte übertragen. Dies ist kein allgemeiner Anwendungsfall, aber es ist eine Fähigkeit des Frameworks. Da diese "gleichzeitigen" Anforderungen und Apps immer noch nur mit einem "Fokus" ausgeführt werden können, ist es sinnvoll, einen Stapel für den jeweiligen Kontext zu verwenden. Immer wenn eine neue Anforderung erzeugt oder eine der Anwendungen aufgerufen wird, verschieben sie ihren Kontext an die Spitze ihres jeweiligen Stapels. Flask verwendet LocalStackzu diesem Zweck Objekte. Wenn sie ihr Geschäft abschließen, entfernen sie den Kontext aus dem Stapel.

LocalStack

So LocalStacksieht ein aus (wieder wird der Code vereinfacht, um das Verständnis seiner Logik zu erleichtern).

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

Beachten Sie, dass a LocalStackein Stapel ist, der in einem lokalen Speicher gespeichert ist, nicht eine Gruppe von Einheimischen, die auf einem Stapel gespeichert sind. Dies bedeutet, dass der Stapel zwar global zugänglich ist, in jedem Thread jedoch ein anderer Stapel vorhanden ist.

Kolben haben nicht seine request, current_app, g, und sessiondirekt an einen Auflösungs Objekte LocalStack, es eher verwendet LocalProxyObjekte , welche eine Lookup - Funktion (anstelle einem LocalObjekt) , die das darunter liegende Objekt aus dem finden wird 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)

Alle diese werden beim Start der Anwendung deklariert, werden jedoch erst dann aufgelöst, wenn ein Anforderungs- oder Anwendungskontext auf den jeweiligen Stapel verschoben wird.

Wenn Sie neugierig sind, wie ein Kontext tatsächlich in den Stapel eingefügt wird (und anschließend herausspringt), schauen Sie, an flask.app.Flask.wsgi_app()welcher Stelle die wsgi-App eingegeben wird (dh was der Webserver aufruft, und übergeben Sie die http-Umgebung an wann a Anfrage kommt herein), und folgen Sie der Erstellung des RequestContextObjekts durch seine anschließende push()in _request_ctx_stack. Sobald es oben auf den Stapel geschoben wurde, ist es über zugänglich _request_ctx_stack.top. Hier ist ein abgekürzter Code, um den Ablauf zu demonstrieren:

Sie starten also eine App und stellen sie dem WSGI-Server zur Verfügung ...

app = Flask(*config, **kwconfig)

# ...

Später kommt eine http-Anfrage und der WSGI-Server ruft die App mit den üblichen Parametern auf ...

app(environ, start_response) # aka app.__call__(environ, start_response)

Dies ist ungefähr das, was in der App passiert ...

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()

    # ...

und genau das passiert mit 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()

Angenommen, eine Anfrage wurde initialisiert. Die Suche nach request.patheiner Ihrer Ansichtsfunktionen würde daher wie folgt aussehen:

  • Beginnen Sie mit dem global zugänglichen LocalProxyObjekt request.
  • Um das zugrunde liegende interessierende Objekt (das Objekt, auf das es sich bezieht) zu finden, ruft es seine Suchfunktion auf _find_request()(die Funktion, die es als seine registriert hat self.local).
  • Diese Funktion fragt das LocalStackObjekt _request_ctx_stacknach dem obersten Kontext auf dem Stapel ab.
  • Um den obersten Kontext zu finden, LocalStackfragt das Objekt zuerst sein inneres LocalAttribut ( self.local) nach der stackEigenschaft ab, die zuvor dort gespeichert wurde.
  • von dem stackbekommt es den obersten Kontext
  • und top.requestwird somit als das zugrunde liegende interessierende Objekt aufgelöst.
  • Von diesem Objekt erhalten wir das pathAttribut

So wir , wie gesehen haben Local, LocalProxyund LocalStackArbeit, denkt jetzt für einen Moment der Auswirkungen und Nuancen in dem Abrufen der pathvon:

  • Ein requestObjekt, das ein einfaches global zugängliches Objekt wäre.
  • ein requestObjekt, das ein lokales wäre.
  • Ein requestObjekt, das als Attribut eines lokalen Objekts gespeichert ist.
  • Ein requestObjekt, das ein Proxy für ein Objekt ist, das in einem lokalen Objekt gespeichert ist.
  • Ein requestObjekt, das auf einem Stapel gespeichert ist, der wiederum in einem lokalen Objekt gespeichert ist.
  • Ein requestObjekt, das ein Proxy für ein Objekt auf einem Stapel ist, der in einem lokalen Speicher gespeichert ist. <- das macht Flask.
Michael Ekoka
quelle
4
Ausgezeichneter Überblick, ich habe den Code in flask / globals.py und werkzeug / local.py studiert und dies hilft mir, mein Verständnis davon zu klären. Mein Spidey Sense sagt mir, dass dies ein viel zu kompliziertes Design ist, aber ich gebe zu, dass ich nicht alle Anwendungsfälle verstehe, für die es bestimmt ist. "Interne Weiterleitungen" sind die einzige Rechtfertigung, die ich in den obigen Beschreibungen gesehen habe, und das Googeln der "internen Weiterleitung von Flaschen" taucht nicht viel auf, so dass ich immer noch ein bisschen ratlos bin. Eines der Dinge, die ich an flask mag, ist, dass es im Allgemeinen keine Java-Objektsuppe ist, die voll von AbstractProviderContextBaseFactories und so ist.
QuadrupleA
1
@QuadrupleA Wenn Sie verstehen , wie diese Local, LocalStackund LocalProxyArbeit, schlage ich vor , diese Artikel des Dokuments zu überdenken: flask.pocoo.org/docs/0.11/appcontext , flask.pocoo.org/docs/0.11/extensiondev und flask.pocoo .org / docs / 0.11 / reqcontext . Ihr neuer Griff kann Sie sie mit einem neuen Licht sehen lassen und mehr Einsicht bieten.
Michael Ekoka
Lesen Sie diese Links durch - sie sind meistens sinnvoll, aber das Design scheint mir immer noch überkompliziert und vielleicht zu clever für sich. Aber ich bin kein großer Fan von OOP im Allgemeinen und impliziten Flusskontrollelementen (Überschreiben von __call __ (), __getattr __ (), dynamischem Ereignisversand im Vergleich zu einfachen Funktionsaufrufen, Umschließen von Dingen in Eigenschaftszugriffsfunktionen, anstatt nur ein reguläres Attribut usw. Zu verwenden .) Vielleicht ist es nur ein Unterschied in der Philosophie. Auch kein TDD-Praktiker, den viele dieser zusätzlichen Maschinen zu unterstützen scheinen.
QuadrupleA
1
Vielen Dank für das Teilen, geschätzt. Threading ist die Schwäche von Sprachen wie Python - Sie erhalten Muster wie oben, die in Anwendungsframeworks eindringen und auch nicht wirklich skaliert werden. Java ist ein weiteres Beispiel in einer ähnlichen Situation bezüglich. Threadlocals, Semaphoren usw. Notorisch schwer zu finden oder zu pflegen. Hier bieten Sprachen wie Erlang / Elixir (unter Verwendung von BEAM) oder Event-Loop-Ansätze (z. B. Nginx vs Apache usw.) normalerweise einen leistungsfähigeren, skalierbareren und weniger komplexen Ansatz.
Arcseldon
13

Kleine Ergänzung @ Mark Hildreths Antwort.

Der Kontextstapel sieht so aus {thread.get_ident(): []}, []als würde er "Stapel" genannt, da nur die Operationen append( push) popund [-1]( __getitem__(-1)) verwendet werden. Der Kontextstapel speichert also die tatsächlichen Daten für den Thread oder den Greenlet-Thread.

current_app, g, request, sessionUnd ist etc LocalProxyObjekt , das nur spezielle Methoden overrided __getattr__, __getitem__, __call__, __eq__und etc. und Rückgabewert von Kontextstapel nach oben ( [-1]) durch das Argument name ( current_app, requestzum Beispiel). LocalProxymüssen diese Objekte einmal importieren und sie werden die Aktualität nicht verfehlen. Importieren requestSie also besser, wo immer Sie sich im Code befinden, und spielen Sie stattdessen mit dem Senden des Anforderungsarguments an Ihre Funktionen und Methoden. Sie können damit problemlos eigene Erweiterungen schreiben, aber vergessen Sie nicht, dass eine leichtfertige Verwendung das Verständnis von Code erschweren kann.

Nehmen Sie sich Zeit, um https://github.com/mitsuhiko/werkzeug/blob/master/werkzeug/local.py zu verstehen .

Wie bevölkert sind beide Stapel? Auf Anfrage Flask:

  1. Nach request_contextUmgebung erstellen (Init map_adapter, Übereinstimmungspfad)
  2. Geben Sie diese Anfrage ein oder drücken Sie sie:
    1. klar vorher request_context
    2. Erstellen, app_contextwenn es fehlte und in den Anwendungskontextstapel verschoben wurde
    3. Diese Anforderung wurde an den Anforderungskontextstapel gesendet
    4. Init-Sitzung, wenn es fehlte
  3. Versandanfrage
  4. Anfrage löschen und vom Stapel ablegen
tbicr
quelle
2

Nehmen wir ein Beispiel: Angenommen, Sie möchten einen Benutzerkontext festlegen (mithilfe des Flask-Konstrukts von Local und LocalProxy).

Definieren Sie eine Benutzerklasse:

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

Definieren Sie eine Funktion zum Abrufen des Benutzerobjekts innerhalb des aktuellen Threads oder Greenlets

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

Definieren Sie nun einen LocalProxy

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

Nun, um die Benutzer-ID des Benutzers im aktuellen Thread usercontext.userid abzurufen

Erklärung:

1.Local hat ein Diktat der Identität und des Objekts, Identität ist Thread-ID oder Greenlet-ID. In diesem Beispiel entspricht _local.user = User () _local .___ Speicher __ [ID des aktuellen Threads] ["Benutzer"] = Benutzer ()

  1. LocalProxy delegiert den Vorgang an das abgeschlossene lokale Objekt, oder Sie können eine Funktion bereitstellen, die das Zielobjekt zurückgibt. Im obigen Beispiel stellt die Funktion get_user LocalProxy das aktuelle Benutzerobjekt zur Verfügung. Wenn Sie die Benutzer-ID des aktuellen Benutzers über usercontext.userid anfordern, ruft die Funktion __getattr__ von LocalProxy zuerst get_user auf, um das Benutzerobjekt (Benutzer) abzurufen, und ruft dann getattr (Benutzer, "Benutzer-ID") auf. Um die Benutzer-ID auf Benutzer (im aktuellen Thread oder Greenlet) festzulegen, gehen Sie einfach wie folgt vor: usercontext.userid = "user_123"
Ratn Deo - Dev
quelle