คำตอบก่อนหน้านี้ให้ภาพรวมที่ดีของสิ่งที่เกิดขึ้นในพื้นหลังของขวดระหว่างการร้องขอ หากคุณยังไม่ได้อ่านฉันขอแนะนำคำตอบของ @ MarkHildreth ก่อนอ่าน กล่าวโดยย่อบริบทใหม่ (เธรด) ถูกสร้างขึ้นสำหรับแต่ละคำขอ http ซึ่งเป็นเหตุผลว่าทำไมจึงจำเป็นต้องมีLocal
สิ่งอำนวยความสะดวกของเธรดที่อนุญาตให้วัตถุเช่นrequest
และg
เพื่อให้สามารถเข้าถึงได้ทั่วโลกในหลาย ๆ เธรดในขณะที่ยังคงบริบทเฉพาะคำขอไว้ นอกจากนี้ในขณะที่การประมวลผลคำขอ http ขวดสามารถเลียนแบบการร้องขอเพิ่มเติมจากภายในดังนั้นความจำเป็นในการจัดเก็บบริบทของตนในกอง นอกจากนี้ Flask อนุญาตให้แอปพลิเคชัน wsgi หลายตัวสามารถทำงานร่วมกันภายในกระบวนการเดียวและสามารถเรียกใช้มากกว่าหนึ่งรายการในระหว่างการร้องขอ (แต่ละคำขอสร้างบริบทแอปพลิเคชันใหม่) ดังนั้นความต้องการบริบทสแต็กสำหรับแอปพลิเคชัน นั่นเป็นบทสรุปของสิ่งที่กล่าวถึงในคำตอบก่อนหน้า
เป้าหมายของฉันคือการเสริมความเข้าใจในปัจจุบันของเราโดยการอธิบายว่า Flask และ Werkzeug ทำสิ่งที่พวกเขาทำกับบริบทท้องถิ่นเหล่านี้ได้อย่างไร ฉันลดความซับซ้อนของรหัสเพื่อเพิ่มความเข้าใจในตรรกะของมัน แต่ถ้าคุณได้รับสิ่งนี้คุณควรจะสามารถเข้าใจสิ่งที่อยู่ในแหล่งข้อมูลจริง ( werkzeug.local
และflask.globals
) ได้อย่างง่ายดาย
ก่อนอื่นให้เรามาทำความเข้าใจว่า Werkzeug ได้ทำหน้าที่จัดการเธรดในตัวอย่างไร
ในประเทศ
เมื่อมีคำขอ HTTP เข้ามาจะมีการประมวลผลภายในบริบทของเธรดเดี่ยว ในฐานะที่เป็นทางเลือกหมายถึงการวางไข่บริบทใหม่ในระหว่างการร้องขอ http, Werkzeug ยังอนุญาตให้ใช้กรีนเล็ต (ประเภท "ไมโครเธรด" ที่เบากว่า) แทนเธรดปกติ หากคุณไม่ได้ติดตั้งกรีนเล็ตมันจะเปลี่ยนกลับไปใช้เธรดแทน แต่ละเธรด (หรือกรีนเล็ต) เหล่านี้สามารถระบุได้ด้วย id ที่ไม่ซ้ำกันซึ่งคุณสามารถดึงข้อมูลได้ด้วย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 เป็นวัตถุที่สืบค้น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
(เช่นคุณป๊อป context_id จากที่เก็บข้อมูล) และไม่ต้องกังวลกับพร็อกซีพวกเขายังสามารถเข้าถึงได้ทั่วโลกและยังคงรอการLocal
ค้นหาวัตถุของพวกเขา ที่น่าสนใจสำหรับคำขอ HTTP ที่ตามมา
# this would happen some time near the end of request processing
release(local) # aka local.__release_local__()
เพื่อทำให้การสร้าง a LocalProxy
เมื่อเรามี a ง่ายขึ้นแล้ว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 (flask.globals) ที่ยังคงไม่เป็นrequest
เช่นg
นั้นcurrent_app
และsession
ถูกสร้างขึ้น ตามที่เราได้สร้างไว้แล้วขวดสามารถวางไข่คำขอ "ปลอม" ได้หลายรายการ (จากคำขอ 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()
เมื่อผลักดันที่ด้านบนของสแต็คก็สามารถเข้าถึงได้ผ่านทาง_request_ctx_stack
_request_ctx_stack.top
นี่คือรหัสย่อบางส่วนที่แสดงให้เห็นถึงการไหล:
ดังนั้นคุณจึงเริ่มแอพและทำให้มันพร้อมใช้งานกับเซิร์ฟเวอร์ WSGI ...
app = Flask(*config, **kwconfig)
# ...
หลังจากนั้นคำขอ http มาในและเซิร์ฟเวอร์ WSGI เรียกแอปที่มี params ตามปกติ ...
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
วัตถุที่เป็นพร็อกซี่กับวัตถุบนกองเก็บไว้ในท้องถิ่น <- นี่คือสิ่งที่ Flask ทำ