Попередні відповіді вже дають хороший огляд того, що відбувається у фоновому режимі під час запиту. Якщо ви ще цього не прочитали, рекомендую відповідь @ MarkHildreth перед її читанням. Коротше кажучи, для кожного запиту http створюється новий контекст (потік), тому необхідно мати об'єкт потоку Local
, який дозволяє об'єкти, такі як request
іg
бути доступними в усьому світі через потоки, зберігаючи конкретний контекст їх запиту. Крім того, обробляючи http-запит, Flask може імітувати додаткові запити зсередини, отже, необхідність зберігання відповідного контексту у стеку. Крім того, Flask дозволяє декільком програмам wsgi запускати один одного в межах одного процесу, і більше одного можна викликати до дії під час запиту (кожен запит створює новий контекст програми), отже, необхідність контекстного стеку для додатків. Це підсумок того, що було висвітлено у попередніх відповідях.
Моя мета зараз - доповнити наше сучасне розуміння, пояснивши, як Фляск і Веркгейг роблять те, що роблять з цими місцевими жителями. Я спростив код, щоб покращити розуміння його логіки, але якщо ви це отримаєте, ви зможете легко зрозуміти більшість того, що знаходиться у фактичному джерелі ( werkzeug.local
і flask.globals
).
Давайте спочатку розберемося, як Werkzeug реалізує локальні потоки.
Місцеві
Коли надходить http-запит, він обробляється в контексті одного потоку. Як альтернативне засіб для породження нового контексту під час http-запиту, Werkzeug також дозволяє використовувати зелені (на зразок легших "мікропотоків") замість звичайних ниток. Якщо у вас немає встановлених зелених грінок, він замість цього повернеться до використання тем. Кожен з цих потоків (або greenlets) ідентифікується за допомогою унікального ідентифікатора, який ви можете отримати за допомогою функції модуля get_ident()
. Ця функція є відправною точкою до магії за мають request
, current_app
, url_for
, g
, та інші подібні контекстний палітуркою глобальних об'єктів.
try:
from greenlet import get_ident
except ImportError:
from thread import get_ident
Тепер, коли ми маємо свою функцію ідентичності, ми можемо знати, на якому потоці ми перебуваємо в будь-який момент часу, і ми можемо створити те, що називається потоком Local
, контекстним об'єктом, до якого можна отримати доступ у всьому світі, але коли ви отримаєте доступ до його атрибутів, вони визначають їх значення для що конкретна нитка. напр
# globally
local = Local()
# ...
# on thread 1
local.first_name = 'John'
# ...
# on thread 2
local.first_name = 'Debbie'
Обидва значення присутні на глобально доступному Local
об'єкті одночасно, але доступ local.first_name
у контексті потоку 1 надасть вам 'John'
, тоді як він повернеться 'Debbie'
у потоці 2.
Як це можливо? Давайте розглянемо деякий (спрощений) код:
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()
З наведеного вище коду ми бачимо, що магія зводиться до того, get_ident()
який ідентифікує поточну зелену або нитку. Потім Local
сховище просто використовує це як ключ для зберігання будь-яких даних, контекстуальних до поточного потоку.
Ви можете мати кілька Local
об'єктів для кожного процесу і request
, g
, current_app
та інші могли б просто були створені таким чином. Але це не так, як це робиться в колбі, в якій це не технічно Local
об'єкти, а точніше LocalProxy
об'єкти. Що таке LocalProxy
?
LocalProxy
LocalProxy - це об'єкт, який запитує a, Local
щоб знайти інший об'єкт, що цікавить (тобто об'єкт, якому він надає). Давайте подивимось, щоб зрозуміти:
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 ...
Тепер для створення глобально доступних проксі-серверів ви б це зробили
# this would happen some time near application start-up
local = Local()
request = LocalProxy(local, 'request')
g = LocalProxy(local, 'g')
і тепер деякий час на початку запиту ви зберігатимете деякі об’єкти всередині локальних, до яких раніше можуть створюватися проксі-сервери, незалежно від того, на якій ниті ми знаходимося
# this would happen early during processing of an http request
local.request = RequestContext(http_environment)
local.g = SomeGeneralPurposeContainer()
Перевага використання LocalProxy
як глобально доступних об'єктів, а не виготовлення їх Locals
самих, полягає в тому, що це спрощує управління ними. Вам потрібно лише один Local
об'єкт, щоб створити багато глобально доступних проксі. Наприкінці запиту під час очищення ви просто відпустите його Local
(тобто ви завантажуєте контекст_id із його сховища) і не турбуєтесь про проксі, вони все ще доступні в усьому світі і все одно відхиляються від того, Local
щоб знайти свій об’єкт представляє інтерес для подальших http-запитів.
# this would happen some time near the end of request processing
release(local) # aka local.__release_local__()
Щоб спростити створення, LocalProxy
коли у нас вже є Local
, Werkzeug реалізує Local.__call__()
магічний метод наступним чином:
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')
Тим НЕ менше, якщо ви дивитеся в джерелі Колба (flask.globals) , яка все ще не так, як request
, g
, current_app
і session
створені. Як ми встановили, Flask може породжувати декілька "підроблених" запитів (з одного справжнього запиту http), і в процесі також висувати декілька контекстів додатків. Це не звичайний випадок використання, але це здатність фреймворку. Оскільки ці "паралельні" запити та додатки все ще обмежені для запуску лише з одним, який має "фокус" у будь-який час, має сенс використовувати стек для відповідного контексту. Щоразу, коли створюється новий запит або викликається одне з програм, вони висувають свій контекст у верхній частині відповідного стеку. Колба використовує LocalStack
для цього предмети. Коли вони закінчують свій бізнес, вони вискакують контекст із стека.
LocalStack
Так LocalStack
виглядає (знову код спрощений, щоб полегшити розуміння його логіки).
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
Зверніть увагу на вищесказане, що a LocalStack
- це стек, що зберігається в локальній, а не купі місцевих жителів, що зберігаються в стеці. Це означає, що хоча стек є загальнодоступним, він є різним стеком у кожному потоці.
Колба не має його request
, current_app
, g
і session
об'єкти рішення безпосередньо до LocalStack
, він замість цього використовує LocalProxy
об'єкти , які обгортають функцію пошуку (замість Local
об'єкта) , який буде знайти базовий об'єкт з 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)
Усі вони декларуються при запуску програми, але насправді ні до чого не вирішуються, поки контекст запиту чи контекст програми не буде висунуто на відповідний стек.
Якщо вам цікаво побачити, як контекст насправді вставляється в стек (і згодом вискакує), подивіться, в flask.app.Flask.wsgi_app()
якій точці входить додаток wsgi (тобто, до чого звертається веб-сервер і передайте середовище http, коли приходить запит), а також стежити за створенням RequestContext
об'єкта через весь його подальший push()
INTO _request_ctx_stack
. Після натискання на верхню частину стека це доступно через _request_ctx_stack.top
. Ось короткий код для демонстрації потоку:
Отже, ви запускаєте додаток і робите його доступним для сервера WSGI ...
app = Flask(*config, **kwconfig)
# ...
Пізніше надходить запит http, і сервер WSGI викликає додаток за допомогою звичайних парам ...
app(environ, start_response) # aka app.__call__(environ, start_response)
Це приблизно те, що відбувається в додатку ...
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()
# ...
і це приблизно те, що відбувається з 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()
Скажімо, запит ініціалізується, пошук для request.path
однієї з функцій перегляду буде таким чином:
- починати з глобально доступного
LocalProxy
об’єкта request
.
- щоб знайти його основний об'єкт, що цікавить (об'єкт, якому він знаходиться поруч), він викликає свою функцію пошуку
_find_request()
(функцію, яку він зареєстрував як свою self.local
).
- ця функція запитує
LocalStack
об'єкт _request_ctx_stack
у верхньому контексті стека.
- щоб знайти верхній контекст,
LocalStack
об’єкт спочатку запитує свій внутрішній Local
атрибут ( self.local
) для stack
властивості, яке раніше зберігалося там.
- з цього
stack
він отримує верхній контекст
- і
top.request
таким чином вирішується як основний об'єкт інтересу.
- від цього об’єкта ми отримуємо
path
атрибут
Тож ми бачили, як Local
, LocalProxy
і LocalStack
працюємо, зараз на мить подумайте про наслідки та нюанси у виведенні path
:
request
об'єкт , який був би простий глобально доступний об'єкт.
request
об'єкт , який буде локальним.
request
об'єкт , що зберігається в якості атрибута локальних.
request
об'єкт , який є проксі об'єкта , що зберігається в локальний.
request
об'єкт , що зберігається в стеці, що в свою чергу , зберігається в місцевому.
request
об'єкт , який є проксі об'єкта на стеку зберігається в локальній. <- це те, що робить колба.