Quel est le but des piles de contexte de Flask?

158

J'utilise le contexte de demande / application depuis un certain temps sans comprendre pleinement comment cela fonctionne ou pourquoi il a été conçu tel quel. Quel est le but de la "pile" en ce qui concerne la demande ou le contexte d'application? Ces deux piles sont-elles séparées ou font-elles toutes deux partie d'une pile? Le contexte de requête est-il poussé sur une pile ou s'agit-il d'une pile elle-même? Suis-je capable de pousser / faire apparaître plusieurs contextes les uns sur les autres? Si oui, pourquoi voudrais-je faire ça?

Désolé pour toutes les questions, mais je suis toujours confus après avoir lu la documentation pour le contexte de demande et le contexte d'application.

Ben Davis
la source
5
kronosapiens.github.io/blog/2014/08/14/… IMO, ce billet de blog me donne la description la plus compréhensible du contexte du flacon.
mission.liao

Réponses:

243

Applications multiples

Le contexte de l'application (et son objectif) est en effet déroutant jusqu'à ce que vous vous rendiez compte que Flask peut avoir plusieurs applications. Imaginez la situation où vous souhaitez qu'un seul interpréteur WSGI Python exécute plusieurs applications Flask. Nous ne parlons pas de Blueprints ici, nous parlons d'applications Flask entièrement différentes.

Vous pouvez configurer cela de la même manière que dans la section de documentation de Flask sur l' exemple "Distribution d'application" :

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

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

Notez que deux applications Flask complètement différentes sont créées "frontend" et "backend". En d'autres termes, le Flask(...)constructeur d'application a été appelé deux fois, créant deux instances d'une application Flask.

Contextes

Lorsque vous travaillez avec Flask, vous finissez souvent par utiliser des variables globales pour accéder à diverses fonctionnalités. Par exemple, vous avez probablement du code qui lit ...

from flask import request

Ensuite, lors d'une vue, vous pouvez utiliser requestpour accéder aux informations de la demande en cours. De toute évidence, ce requestn'est pas une variable globale normale; en réalité, c'est une valeur locale de contexte . En d'autres termes, il y a une certaine magie dans les coulisses qui dit "quand j'appelle request.path, récupère l' pathattribut de l' requestobjet de la requête CURRENT." Deux demandes différentes auront des résultats différents pour request.path.

En fait, même si vous exécutez Flask avec plusieurs threads, Flask est suffisamment intelligent pour garder les objets de requête isolés. Ce faisant, il devient possible pour deux threads, chacun traitant une requête différente, d'appeler request.pathet d'obtenir simultanément les informations correctes pour leurs requêtes respectives.

Mettre ensemble

Nous avons donc déjà vu que Flask peut gérer plusieurs applications dans le même interpréteur, et aussi qu'en raison de la façon dont Flask vous permet d'utiliser les globaux "context local", il doit y avoir un mécanisme pour déterminer ce qu'est la requête "actuelle" ( afin de faire des choses telles que request.path).

En rassemblant ces idées, il devrait également être logique que Flask ait un moyen de déterminer quelle est l'application "actuelle"!

Vous avez probablement également un code similaire au suivant:

from flask import url_for

Comme notre requestexemple, la url_forfonction a une logique qui dépend de l'environnement actuel. Dans ce cas, cependant, il est clair que la logique dépend fortement de quelle application est considérée comme l'application "actuelle". Dans l'exemple frontend / backend illustré ci-dessus, les applications "frontend" et "backend" peuvent avoir une route "/ login" et url_for('/login')doivent donc renvoyer quelque chose de différent selon que la vue traite la demande de l'application frontend ou backend.

Pour répondre à tes questions...

Quel est le but de la "pile" en ce qui concerne la demande ou le contexte d'application?

À partir de la documentation sur le contexte de la demande:

Étant donné que le contexte de la demande est géré en interne sous forme de pile, vous pouvez pousser et afficher plusieurs fois. C'est très pratique pour implémenter des choses comme les redirections internes.

En d'autres termes, même si vous avez généralement 0 ou 1 élément sur cette pile de requêtes "actuelles" ou d'applications "actuelles", il est possible que vous en ayez plus.

L'exemple donné est celui où votre requête renvoie les résultats d'une "redirection interne". Disons qu'un utilisateur demande A, mais que vous souhaitez revenir à l'utilisateur B.Dans la plupart des cas, vous émettez une redirection vers l'utilisateur et pointez l'utilisateur vers la ressource B, ce qui signifie que l'utilisateur exécutera une deuxième demande pour récupérer B. A Une manière légèrement différente de gérer cela serait de faire une redirection interne, ce qui signifie que lors du traitement de A, Flask se fera une nouvelle demande pour la ressource B et utilisera les résultats de cette deuxième demande comme résultats de la demande d'origine de l'utilisateur.

Ces deux piles sont-elles séparées ou font-elles toutes deux partie d'une pile?

Ce sont deux piles distinctes . Cependant, ceci est un détail de mise en œuvre. Le plus important n'est pas tant qu'il existe une pile, mais le fait qu'à tout moment, vous pouvez obtenir l'application ou la requête "actuelle" (en haut de la pile).

Le contexte de requête est-il poussé sur une pile ou s'agit-il d'une pile elle-même?

Un «contexte de demande» est un élément de la «pile de contexte de demande». De même avec le "contexte d'application" et la "pile de contexte d'application".

Suis-je capable de pousser / faire apparaître plusieurs contextes les uns sur les autres? Si oui, pourquoi voudrais-je faire ça?

Dans une application Flask, vous ne le feriez généralement pas. Un exemple où vous pourriez vouloir est pour une redirection interne (décrite ci-dessus). Même dans ce cas, cependant, vous finiriez probablement par avoir Flask gérer une nouvelle requête, et donc Flask ferait tout le push / popping pour vous.

Cependant, dans certains cas, vous souhaitez manipuler vous-même la pile.

Exécution de code en dehors d'une requête

Un problème typique que les gens ont est qu'ils utilisent l'extension Flask-SQLAlchemy pour configurer une base de données SQL et une définition de modèle en utilisant un code semblable à ce qui est montré ci-dessous ...

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

Ensuite, ils utilisent les valeurs appet dbdans un script qui doit être exécuté à partir du shell. Par exemple, un script "setup_tables.py" ...

from myapp import app, db

# Set up models
db.create_all()

Dans ce cas, l'extension Flask-SQLAlchemy connaît l' appapplication, mais au cours de create_all()celle-ci, une erreur se plaint de l'absence de contexte d'application. Cette erreur est justifiée; vous n'avez jamais dit à Flask à quelle application il devait traiter lors de l'exécution de la create_allméthode.

Vous vous demandez peut-être pourquoi vous n'avez pas besoin de cet with app.app_context()appel lorsque vous exécutez des fonctions similaires dans vos vues. La raison en est que Flask gère déjà la gestion du contexte de l'application pour vous lorsqu'il traite des requêtes Web réelles. Le problème ne survient vraiment qu'en dehors de ces fonctions de vue (ou d'autres rappels de ce type), comme lors de l'utilisation de vos modèles dans un script ponctuel.

La résolution est de pousser vous-même le contexte de l'application, ce qui peut être fait en faisant ...

from myapp import app, db

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

Cela poussera un nouveau contexte d'application (en utilisant l'application de app, rappelez-vous qu'il peut y avoir plus d'une application).

Essai

Un autre cas où vous voudriez manipuler la pile est pour les tests. Vous pouvez créer un test unitaire qui gère une demande et vous vérifiez les résultats:

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
la source
3
C'est encore déroutant pour moi! Pourquoi ne pas avoir un seul contexte de requête et le remplacer si vous souhaitez effectuer une redirection interne. Cela me semble être un design clair.
Maarten
@Maarten Si, lors du traitement de la requête A, vous faites la requête B et que la requête B remplace la requête A sur la pile, la gestion de la requête A ne peut pas se terminer. Cependant, même si vous avez appliqué la stratégie de remplacement comme vous l'avez suggéré et que vous n'avez pas de pile (ce qui signifie que les redirections internes seraient plus difficiles), cela ne change pas vraiment le fait que les contextes d'application et de demande sont nécessaires pour isoler la gestion des demandes.
Mark Hildreth
Bonne explication! Mais je suis encore un peu confus à propos de: "Le contexte de l'application est créé et détruit si nécessaire. Il ne se déplace jamais entre les threads et il ne sera pas partagé entre les requêtes." Dans le document de Flask. Pourquoi un "contexte d'application" n'est pas persistant avec l'application?
jayven
1
Un exemple de redirection interne dans Flask serait utile, googler cela ne se révèle pas beaucoup. Sinon, une request = Local()conception plus simple ne suffirait-elle pas pour global.py? Il y a probablement des cas d'utilisation auxquels je ne pense pas.
QuadrupleA
Est-il correct de pousser le contexte de l'application dans la méthode d'usine lors de l'importation des vues? Parce que les vues contiennent des routes faisant référence à current_app, j'ai besoin du contexte.
variable
48

Les réponses précédentes donnent déjà un bon aperçu de ce qui se passe en arrière-plan de Flask lors d'une requête. Si vous ne l'avez pas encore lu, je recommande la réponse de @ MarkHildreth avant de lire ceci. En bref, un nouveau contexte (thread) est créé pour chaque requête http, c'est pourquoi il est nécessaire d'avoir une fonction de thread Localqui autorise des objets tels que requestetgpour être accessible globalement à travers les threads, tout en conservant leur contexte spécifique à la demande. De plus, lors du traitement d'une requête http, Flask peut émuler des requêtes supplémentaires de l'intérieur, d'où la nécessité de stocker leur contexte respectif sur une pile. De plus, Flask permet à plusieurs applications wsgi de fonctionner les unes avec les autres dans un même processus, et plusieurs peuvent être appelées à l'action pendant une requête (chaque requête crée un nouveau contexte d'application), d'où la nécessité d'une pile de contexte pour les applications. C'est un résumé de ce qui a été traité dans les réponses précédentes.

Mon objectif est maintenant de compléter notre compréhension actuelle en expliquant comment Flask et Werkzeug font ce qu'ils font avec ces locaux contextuels. J'ai simplifié le code pour améliorer la compréhension de sa logique, mais si vous obtenez cela, vous devriez être en mesure de saisir facilement la plupart de ce qui se trouve dans la source réelle ( werkzeug.localet flask.globals).

Voyons d'abord comment Werkzeug implémente les threads locaux.

Local

Lorsqu'une requête http arrive, elle est traitée dans le contexte d'un seul thread. Comme moyen alternatif de générer un nouveau contexte lors d'une requête http, Werkzeug permet également l'utilisation de greenlets (une sorte de «micro-threads» plus légers) au lieu de threads normaux. Si vous n'avez pas installé de greenlets, il reviendra à l'utilisation des threads à la place. Chacun de ces threads (ou greenlets) est identifiable par un identifiant unique, que vous pouvez récupérer avec la get_ident()fonction du module . Cette fonction est le point de départ de la magie derrière ayant request, current_app, url_for, g, et d' autres objets globaux liés au contexte.

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

Maintenant que nous avons notre fonction d'identité, nous pouvons savoir sur quel thread nous sommes à un moment donné et nous pouvons créer ce qu'on appelle un thread Local, un objet contextuel accessible globalement, mais lorsque vous accédez à ses attributs, ils se résolvent à leur valeur pour ce fil spécifique. par exemple

# globally
local = Local()

# ...

# on thread 1
local.first_name = 'John'

# ...

# on thread 2
local.first_name = 'Debbie'

Les deux valeurs sont présentes sur l' Localobjet globalement accessible en même temps, mais accéder local.first_namedans le contexte du thread 1 vous le donnera 'John', alors qu'il reviendra 'Debbie'sur le thread 2.

Comment est-ce possible? Regardons du code (simplifié):

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

À partir du code ci-dessus, nous pouvons voir que la magie se résume à get_ident()qui identifie le greenlet ou le thread actuel. Le Localstockage l'utilise ensuite comme clé pour stocker toutes les données contextuelles du thread actuel.

Vous pouvez avoir plusieurs Localobjets par processus et request, g, current_appet d' autres pourraient simplement avoir été créé comme ça. Mais ce n'est pas ainsi que cela se fait dans Flask dans lequel ce ne sont pas techniquement des Local objets, mais plus précisément des LocalProxyobjets. Qu'est-ce qu'un LocalProxy?

LocalProxy

Un LocalProxy est un objet qui interroge a Localpour trouver un autre objet d'intérêt (c'est-à-dire l'objet auquel il se rapporte ). Jetons un œil pour comprendre:

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

Maintenant, pour créer des proxys accessibles à l'échelle mondiale, vous

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

et maintenant quelque temps plus tôt au cours d'une demande, vous stockeriez des objets dans le local auxquels les proxys précédemment créés peuvent accéder, quel que soit le thread sur lequel nous sommes

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

L'avantage d'utiliser des LocalProxyobjets globalement accessibles plutôt que de les fabriquer eux- Localsmêmes est que cela simplifie leur gestion. Vous n'avez besoin que d'un seul Localobjet pour créer de nombreux proxys globalement accessibles. A la fin de la requête, lors du nettoyage, vous libérez simplement celui Local(c'est-à-dire que vous sortez le context_id de son stockage) et ne vous embêtez pas avec les proxies, ils sont toujours accessibles globalement et se reportent toujours à celui Localpour trouver leur objet d'intérêt pour les requêtes http suivantes.

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

Pour simplifier la création d'un LocalProxylorsque nous avons déjà un Local, Werkzeug implémente la Local.__call__()méthode magique comme suit:

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

Toutefois, si vous regardez dans la source Flask (de flask.globals) qui est toujours pas comment request, g, current_appet sessionsont créés. Comme nous l'avons établi, Flask peut générer plusieurs "fausses" requêtes (à partir d'une seule vraie requête http) et, dans le processus, également pousser plusieurs contextes d'application. Ce n'est pas un cas d'utilisation courant, mais c'est une capacité du framework. Étant donné que ces demandes et applications "simultanées" sont encore limitées à s'exécuter avec une seule ayant le "focus" à tout moment, il est logique d'utiliser une pile pour leur contexte respectif. À chaque fois qu'une nouvelle demande est générée ou qu'une des applications est appelée, ils poussent leur contexte en haut de leur pile respective. Flask utilise des LocalStackobjets à cet effet. Quand ils concluent leurs affaires, ils sortent le contexte de la pile.

LocalStack

Voici à quoi LocalStackressemble un (encore une fois, le code est simplifié pour faciliter la compréhension de sa logique).

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

Notez à partir de ce qui précède que a LocalStackest une pile stockée dans un local, pas un groupe de locaux stockés sur une pile. Cela implique que bien que la pile soit globalement accessible, c'est une pile différente dans chaque thread.

Flask ne dispose pas de son request, current_app, get projet d' sessionobjets résoudre directement à un LocalStack, il utilise plutôt des LocalProxyobjets qui enveloppent une fonction de recherche ( au lieu d'un Localobjet) qui trouve l'objet sous - jacent de la 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)

Tous ces éléments sont déclarés au démarrage de l'application, mais ne se résolvent réellement en rien tant qu'un contexte de demande ou un contexte d'application n'est pas poussé vers leur pile respective.

Si vous êtes curieux de voir comment un contexte est réellement inséré dans la pile (et ensuite sorti), regardez dans flask.app.Flask.wsgi_app()quel est le point d'entrée de l'application wsgi (c'est-à-dire ce que le serveur Web appelle et transmet l'environnement http quand un requête arrive), et suivez la création de l' RequestContextobjet tout au long de sa suite push()dans _request_ctx_stack. Une fois poussé en haut de la pile, il est accessible via _request_ctx_stack.top. Voici un code abrégé pour illustrer le flux:

Vous démarrez donc une application et la mettez à disposition du serveur WSGI ...

app = Flask(*config, **kwconfig)

# ...

Plus tard, une requête http arrive et le serveur WSGI appelle l'application avec les paramètres habituels ...

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

C'est à peu près ce qui se passe dans l'application ...

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

    # ...

et c'est à peu près ce qui se passe avec 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()

Supposons qu'une requête ait terminé son initialisation, la recherche à request.pathpartir de l'une de vos fonctions d'affichage se déroulerait donc comme suit:

  • commencer à partir de l' LocalProxyobjet globalement accessible request.
  • pour trouver son objet d'intérêt sous-jacent (l'objet auquel il est mandaté), il appelle sa fonction de recherche _find_request()(la fonction qu'il a enregistrée comme son self.local).
  • cette fonction interroge l' LocalStackobjet _request_ctx_stackpour le contexte supérieur sur la pile.
  • pour trouver le contexte supérieur, l' LocalStackobjet interroge d'abord son Localattribut interne ( self.local) pour la stackpropriété qui y était précédemment stockée.
  • du stackil obtient le contexte supérieur
  • et top.requestest donc résolu comme l'objet d'intérêt sous-jacent.
  • de cet objet, nous obtenons l' pathattribut

Nous avons donc vu comment Local, LocalProxyet LocalStacktravailler, réfléchissons maintenant un instant aux implications et aux nuances de la récupération pathde:

  • un requestobjet qui serait un simple objet accessible globalement.
  • un requestobjet qui serait un local.
  • un requestobjet stocké comme attribut d'un local.
  • un requestobjet qui est un proxy vers un objet stocké dans un local.
  • un requestobjet stocké sur une pile, qui est à son tour stocké dans un local.
  • un requestobjet qui est un proxy vers un objet sur une pile stockée dans un local. <- c'est ce que fait Flask.
Michael Ekoka
la source
4
Excellent aperçu, j'ai étudié le code dans flask / globals.py et werkzeug / local.py et cela aide à clarifier ma compréhension de celui-ci. Mon sens de l'araignée me dit que c'est une conception trop compliquée, mais j'avoue que je ne comprends pas tous les cas d'utilisation auxquels elle est destinée. Les "redirections internes" sont la seule justification que j'ai vue dans les descriptions ci-dessus, et googler "flask internal redirect" ne se présente pas beaucoup, donc je suis toujours un peu perdu. Une des choses que j'aime à propos de flask est que ce n'est généralement pas une chose de type java object-soup pleine de AbstractProviderContextBaseFactories et autres.
QuadrupleA
1
@QuadrupleA Une fois que vous comprenez comment ces Local , LocalStacket le LocalProxytravail, je vous propose de revenir sur ces articles de la doc: flask.pocoo.org/docs/0.11/appcontext , flask.pocoo.org/docs/0.11/extensiondev et flask.pocoo .org / docs / 0.11 / reqcontext . Votre nouvelle prise en main peut vous permettre de les voir sous un nouveau jour et peut fournir plus d'informations.
Michael Ekoka du
Lisez ces liens - ils ont pour la plupart du sens, mais la conception me semble toujours trop compliquée et peut-être trop intelligente pour son propre bien. Mais je ne suis pas un grand fan de la POO en général et des contrôles de flux implicites (remplacement de __call __ (), __getattr __ (), répartition dynamique des événements par rapport aux simples appels de fonction, encapsulation des choses dans des accesseurs de propriété plutôt que simplement en utilisant un attribut régulier, etc. .) alors peut-être que c'est juste une différence de philosophie. Pas un praticien du TDD non plus, ce que beaucoup de ces machines supplémentaires semblent être destinés à soutenir.
QuadrupleA
1
Merci de partager ceci, apprécié. Le threading est la faiblesse des langages comme python - vous vous retrouvez avec des modèles comme ci-dessus qui fuient dans les frameworks d'application et qui ne sont pas vraiment mis à l'échelle non plus. Java est un autre exemple dans une situation similaire re. threadlocals, sémaphores, etc. Notoirement difficile à obtenir correctement ou à maintenir. C'est là que les langages comme Erlang / Elixir (utilisant BEAM) ou les approches de boucle d'événements (par exemple nginx vs apache, etc.) offrent généralement une approche plus puissante, évolutive et moins complexe.
arcseldon
13

Petit ajout à la réponse de @Mark Hildreth .

La pile de contexte ressemble à {thread.get_ident(): []}, là où elle est []appelée "pile" car utilisée uniquement pour les opérations append( push) popet [-1]( __getitem__(-1)). Ainsi, la pile de contexte conservera les données réelles pour le thread ou le thread greenlet.

current_app, g , request, sessionEt etc est l' LocalProxyobjet qui vient overrided méthodes spéciales __getattr__, __getitem__, __call__, __eq__etc et la valeur de retour du haut (contexte de pile [-1]par nom de l' argument) ( current_app, requestpar exemple). LocalProxynécessaire pour importer ces objets une fois et ils ne manqueront pas d'actualité. Donc, mieux vaut simplement importer requestoù que vous soyez dans le code au lieu de jouer avec l'envoi d'argument de requête vers vos fonctions et méthodes. Vous pouvez facilement écrire vos propres extensions avec lui, mais n'oubliez pas qu'une utilisation frivole peut rendre le code plus difficile à comprendre.

Passez du temps à comprendre https://github.com/mitsuhiko/werkzeug/blob/master/werkzeug/local.py .

Alors, comment peuplé les deux piles? Sur demande Flask:

  1. créer request_contextpar environnement (init map_adapter, match path)
  2. entrez ou envoyez cette demande:
    1. effacer précédent request_context
    2. créer app_contexts'il a manqué et poussé vers la pile de contexte d'application
    3. cette demande est poussée pour demander la pile de contexte
    4. init session si elle a manqué
  3. demande d'expédition
  4. Effacer la demande et la sortir de la pile
tbicr
la source
2

Prenons un exemple, supposons que vous souhaitiez définir un contexte utilisateur (en utilisant la construction flask de Local et LocalProxy).

Définissez une classe d'utilisateurs:

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

définir une fonction pour récupérer l'objet utilisateur dans le thread ou le greenlet courant

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

Maintenant, définissez un LocalProxy

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

Maintenant, pour obtenir l'ID utilisateur de l'utilisateur dans le thread actuel usercontext.userid

explication:

1.Local a un dict d'identité et d'objet, l'identité est threadid ou greenlet id, dans cet exemple _local.user = User () est équivalent à _local .___ storage __ [id du thread actuel] ["user"] = User ()

  1. LocalProxy délègue l' opération à l'objet Local encapsulé ou vous pouvez fournir une fonction qui renvoie l'objet cible. Dans l'exemple ci-dessus, la fonction get_user fournit l'objet utilisateur actuel à LocalProxy, et lorsque vous demandez l'ID utilisateur de l'utilisateur actuel par usercontext.userid, la fonction __getattr__ de LocalProxy appelle d'abord get_user pour obtenir l'objet utilisateur (utilisateur), puis appelle getattr (utilisateur, "userid"). pour définir l'ID utilisateur sur l'utilisateur (dans le thread actuel ou le greenlet), il vous suffit de faire: usercontext.userid = "user_123"
Ratn Deo - Dev
la source