จุดประสงค์ของบริบทของ Flask คืออะไร


158

ฉันใช้บริบทคำขอ / แอปพลิเคชันมาระยะหนึ่งโดยไม่เข้าใจว่ามันทำงานอย่างไรหรือทำไมจึงถูกออกแบบมาในแบบที่มันเป็น วัตถุประสงค์ของ "สแต็ค" เมื่อมาถึงคำขอหรือบริบทแอปพลิเคชันคืออะไร? สแต็กทั้งสองนี้แยกกันหรือเป็นทั้งสองสแต็กหรือไม่ บริบทคำร้องขอถูกพุชลงบนสแต็กหรือเป็นสแต็กเอง? ฉันสามารถผลักดัน / เปิดคอนเท็กซ์หลายรายการที่ด้านบนของแต่ละอื่นได้หรือไม่ ถ้าเป็นเช่นนั้นทำไมฉันถึงต้องการทำเช่นนั้น?

ขออภัยสำหรับคำถามทั้งหมด แต่ฉันยังคงสับสนหลังจากอ่านเอกสารสำหรับบริบทของคำขอและบริบทของแอปพลิเคชัน


5
kronosapiens.github.io/blog/2014/08/14/… IMO โพสต์บล็อกนี้ให้คำอธิบายที่เข้าใจได้มากที่สุดเกี่ยวกับบริบทขวด
mission.liao

คำตอบ:


243

แอพหลายตัว

บริบทของแอปพลิเคชั่น (และวัตถุประสงค์) ทำให้เกิดความสับสนจนกว่าคุณจะทราบว่า Flask สามารถมีหลายแอพได้ ลองนึกภาพสถานการณ์ที่คุณต้องการให้ล่าม WSGI Python เดียวรันแอพพลิเคชั่น Flask หลาย ๆ ตัว เราไม่ได้พูดถึงพิมพ์เขียวที่นี่เรากำลังพูดถึงแอพพลิเคชั่นขวดที่แตกต่างกันอย่างสิ้นเชิง

คุณอาจตั้งค่านี้คล้ายกับส่วนเอกสารประกอบขวดในตัวอย่าง "Application Dispatching" :

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

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

โปรดสังเกตว่ามีสองแอพพลิเคชั่นขวดที่แตกต่างอย่างสิ้นเชิงที่กำลังสร้าง "ส่วนหน้า" และ "ส่วนหลัง" กล่าวอีกนัยหนึ่งตัวFlask(...)สร้างแอปพลิเคชันได้รับการเรียกสองครั้งโดยสร้างสองอินสแตนซ์ของแอปพลิเคชัน Flask

บริบท

เมื่อคุณทำงานกับ Flask คุณมักจะลงเอยด้วยการใช้ตัวแปรส่วนกลางเพื่อเข้าถึงฟังก์ชั่นต่าง ๆ ตัวอย่างเช่นคุณอาจมีรหัสที่อ่าน ...

from flask import request

จากนั้นในระหว่างการดูคุณอาจใช้requestเพื่อเข้าถึงข้อมูลของคำขอปัจจุบัน เห็นได้ชัดว่าrequestไม่ใช่ตัวแปรทั่วโลกปกติ ในความเป็นจริงมันเป็นบริบทค่าท้องถิ่น กล่าวอีกอย่างหนึ่งก็คือมีเวทมนตร์อยู่ด้านหลังของฉากที่กล่าวว่า "เมื่อฉันโทรrequest.pathมารับpathคุณสมบัติจากrequestวัตถุของคำขอปัจจุบัน" request.pathคำขอสองที่แตกต่างกันจะมีผลแตกต่างกันสำหรับ

ในความเป็นจริงแม้ว่าคุณจะรัน Flask ด้วยหลายเธรด Flask ก็ฉลาดพอที่จะแยกวัตถุคำขอได้ ในการทำเช่นนั้นมันเป็นไปได้สำหรับสองเธรดแต่ละการจัดการคำร้องขอที่แตกต่างกันเพื่อเรียกrequest.pathและรับข้อมูลที่ถูกต้องสำหรับคำร้องขอที่เกี่ยวข้อง

วางไว้ด้วยกัน

ดังนั้นเราจึงได้เห็นแล้วว่า Flask สามารถจัดการหลายแอพพลิเคชั่นในล่ามเดียวกันและเพราะวิธีที่ Flask อนุญาตให้คุณใช้ globals "บริบทท้องถิ่น" จะต้องมีกลไกบางอย่างเพื่อพิจารณาว่าคำขอ "ปัจจุบัน" คืออะไร ( เพื่อที่จะทำสิ่งต่าง ๆ เช่นrequest.path)

การรวบรวมแนวคิดเหล่านี้เข้าด้วยกันควรทำให้รู้สึกว่า Flask ต้องมีวิธีในการพิจารณาว่าแอปพลิเคชั่น "ปัจจุบัน" คืออะไร!

คุณอาจมีรหัสคล้ายกับที่แสดงต่อไปนี้:

from flask import url_for

เช่นเดียวกับrequestตัวอย่างของเราurl_forฟังก์ชั่นมีตรรกะที่ขึ้นอยู่กับสภาพแวดล้อมปัจจุบัน อย่างไรก็ตามในกรณีนี้จะเห็นได้ชัดว่าตรรกะนั้นขึ้นอยู่กับแอพใดที่ถือว่าเป็นแอพ "ปัจจุบัน" ในตัวอย่างส่วนหน้า / ส่วนหลังที่แสดงด้านบนทั้งแอป "ส่วนหน้า" และ "ส่วนหลัง" อาจมีเส้นทาง "/ เข้าสู่ระบบ" ดังนั้นurl_for('/login')ควรกลับบางสิ่งที่แตกต่างกันไปขึ้นอยู่กับว่ามุมมองจัดการคำขอแอปส่วนหน้าหรือส่วนหลัง

เพื่อตอบคำถามของคุณ ...

วัตถุประสงค์ของ "สแต็ค" เมื่อมาถึงคำขอหรือบริบทแอปพลิเคชันคืออะไร?

จากเอกสารบริบทการร้องขอ:

เนื่องจากบริบทคำร้องขอได้รับการดูแลภายในเป็นสแต็กคุณสามารถพุชและป็อปได้หลายครั้ง สิ่งนี้มีประโยชน์มากในการใช้สิ่งต่าง ๆ เช่นการเปลี่ยนเส้นทางภายใน

กล่าวอีกนัยหนึ่งถึงแม้ว่าโดยทั่วไปคุณจะมี 0 หรือ 1 รายการในสแต็กของคำขอ "ปัจจุบัน" หรือแอปพลิเคชัน "ปัจจุบัน" เหล่านี้เป็นไปได้ว่าคุณอาจมีมากกว่านี้

ตัวอย่างที่ให้ไว้คือที่ที่คุณต้องการให้คำขอของคุณส่งคืนผลลัพธ์ของ "การเปลี่ยนเส้นทางภายใน" สมมติว่าผู้ใช้ร้องขอ A แต่คุณต้องการกลับไปที่ผู้ใช้ B ในกรณีส่วนใหญ่คุณออกการเปลี่ยนเส้นทางไปยังผู้ใช้และชี้ผู้ใช้ไปยังทรัพยากร B หมายความว่าผู้ใช้จะเรียกใช้คำขอที่สองเพื่อดึงข้อมูล B. A วิธีที่แตกต่างกันเล็กน้อยในการจัดการสิ่งนี้คือทำการเปลี่ยนเส้นทางภายในซึ่งหมายความว่าในขณะที่ทำการประมวลผล A ขวดจะทำการร้องขอใหม่สำหรับทรัพยากร B และใช้ผลลัพธ์ของคำขอที่สองนี้เป็นผลลัพธ์ของคำขอต้นฉบับของผู้ใช้

สแต็กทั้งสองนี้แยกกันหรือเป็นทั้งสองสแต็กหรือไม่

พวกเขาเป็นสองกองแยกต่างหาก อย่างไรก็ตามนี่เป็นรายละเอียดการใช้งาน สิ่งที่สำคัญกว่านั้นไม่มากนักที่มีสแต็ก แต่ความจริงที่ว่าเมื่อใดก็ตามที่คุณสามารถรับแอพหรือคำขอ "ปัจจุบัน" (ด้านบนของสแต็ก)

บริบทคำร้องขอถูกพุชลงบนสแต็กหรือเป็นสแต็กเอง?

"บริบทคำขอ" คือหนึ่งรายการของ "คำขอบริบทสแต็ก" ในทำนองเดียวกันกับ "บริบทของแอป" และ "บริบทของแอป"

ฉันสามารถผลักดัน / เปิดคอนเท็กซ์หลายรายการที่ด้านบนของแต่ละอื่นได้หรือไม่ ถ้าเป็นเช่นนั้นทำไมฉันถึงต้องการทำเช่นนั้น?

ในแอปพลิเคชั่นขวดโดยทั่วไปคุณจะไม่ทำเช่นนี้ ตัวอย่างหนึ่งที่คุณอาจต้องการใช้สำหรับการเปลี่ยนเส้นทางภายใน (อธิบายไว้ข้างต้น) อย่างไรก็ตามในกรณีดังกล่าวคุณอาจจบลงด้วยการที่ Flask จัดการคำขอใหม่ดังนั้น Flask จะทำการผลัก / popping ทั้งหมดให้คุณ

อย่างไรก็ตามมีบางกรณีที่คุณต้องการจัดการสแต็กด้วยตนเอง

ใช้รหัสนอกคำขอ

หนึ่งปัญหาที่คนทั่วไปมีคือพวกเขาใช้ส่วนขยาย Flask-SQLAlchemy เพื่อตั้งค่าฐานข้อมูล SQL และคำจำกัดความของโมเดลโดยใช้โค้ดอย่างที่แสดงด้านล่าง ...

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

จากนั้นพวกเขาใช้appและdbค่าในสคริปต์ที่ควรจะเรียกใช้จากเปลือก ตัวอย่างเช่นสคริปต์ "setup_tables.py" ...

from myapp import app, db

# Set up models
db.create_all()

ในกรณีนี้ส่วนขยาย Flask-SQLAlchemy รู้เกี่ยวกับappแอปพลิเคชัน แต่ในระหว่างcreate_all()นั้นจะเกิดข้อผิดพลาดที่บ่นว่าไม่มีบริบทแอปพลิเคชัน ข้อผิดพลาดนี้เป็นธรรม คุณไม่เคยบอก Flask ว่าควรใช้แอพพลิเคชั่นตัวใดเมื่อเรียกใช้create_allเมธอด

คุณอาจสงสัยว่าทำไมคุณถึงไม่ต้องการการwith app.app_context()โทรนี้เมื่อคุณใช้งานฟังก์ชั่นที่คล้ายกันในมุมมองของคุณ เหตุผลก็คือ Flask จัดการการจัดการบริบทแอปพลิเคชันสำหรับคุณอยู่แล้วเมื่อจัดการคำขอเว็บจริง ปัญหาเกิดขึ้นนอกฟังก์ชันมุมมองเหล่านี้เท่านั้น (หรือการเรียกกลับอื่น ๆ ) เช่นเมื่อใช้โมเดลของคุณในสคริปต์แบบใช้ครั้งเดียว

ความละเอียดคือการผลักดันบริบทของแอปพลิเคชันด้วยตนเองซึ่งสามารถทำได้โดยการ ...

from myapp import app, db

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

สิ่งนี้จะผลักดันบริบทแอปพลิเคชันใหม่ (โดยใช้แอปพลิเคชันappจำได้ว่าอาจมีแอปพลิเคชันมากกว่าหนึ่งรายการ)

การทดสอบ

อีกกรณีที่คุณต้องการจัดการสแต็กสำหรับการทดสอบ คุณสามารถสร้างการทดสอบหน่วยที่จัดการการร้องขอและคุณตรวจสอบผลลัพธ์:

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

3
นี่ยังคงทำให้ฉันสับสน! ทำไมไม่มีบริบทการร้องขอเดียวและแทนที่ถ้าคุณต้องการทำการเปลี่ยนเส้นทางภายใน ดูเหมือนว่าการออกแบบที่ชัดเจนกับฉัน
Maarten

@Maarten ถ้าในขณะที่จัดการคำขอ A คุณสร้างคำขอ B และคำขอ B แทนที่คำขอ A บนสแต็กการจัดการคำร้องขอ A ไม่สามารถเสร็จสิ้นได้ อย่างไรก็ตามแม้ว่าคุณจะใช้กลยุทธ์การแทนที่ตามที่คุณแนะนำและไม่มีสแต็ก (หมายถึงการเปลี่ยนเส้นทางภายในจะเป็นเรื่องยากมากขึ้น) สิ่งนี้ไม่ได้เปลี่ยนความจริงที่ว่าแอปและคำขอบริบทจำเป็นต้องแยกการจัดการคำขอ
Mark Hildreth

คำอธิบายที่ดี! แต่ฉันยังคงสับสนเล็กน้อยเกี่ยวกับ: "บริบทแอปพลิเคชันถูกสร้างและทำลายตามความจำเป็นมันจะไม่ย้ายระหว่างเธรดและจะไม่ถูกแชร์ระหว่างคำขอ" ในเอกสารของขวด ทำไม "แอปพลิเคชันบริบท" ไม่คงอยู่พร้อมกับแอพ
jayven

1
ตัวอย่างของการเปลี่ยนเส้นทางภายในใน Flask จะมีประโยชน์ googling มันไม่เปิดขึ้นมากนัก ถ้าไม่ใช่เพราะนั่นจะเป็นการrequest = Local()ออกแบบที่ง่ายกว่าสำหรับ global.py? อาจมีการใช้กรณีที่ฉันไม่ได้คิด
QuadrupleA

มันผลักดันบริบทแอพภายในวิธีการของโรงงานเมื่อนำเข้ามุมมองหรือไม่? เนื่องจากมุมมองมีเส้นทางที่อ้างถึง current_app ฉันต้องการบริบท
ตัวแปร

48

คำตอบก่อนหน้านี้ให้ภาพรวมที่ดีของสิ่งที่เกิดขึ้นในพื้นหลังของขวดระหว่างการร้องขอ หากคุณยังไม่ได้อ่านฉันขอแนะนำคำตอบของ @ 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 ง่ายขึ้นแล้วLocalWerkzeug ได้ใช้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จากหนึ่งในฟังก์ชั่นมุมมองของคุณจะเป็นดังนี้:

  • เริ่มต้นจากทั่วโลกสามารถเข้าถึงวัตถุLocalProxyrequest
  • เพื่อค้นหาวัตถุต้นแบบที่น่าสนใจ (วัตถุที่เป็นพร็อกซีถึง) จะเรียกใช้ฟังก์ชันการค้นหา_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 ทำ

4
บทสรุปที่ยอดเยี่ยมฉันได้ศึกษาโค้ดใน flask / globals.py และ werkzeug / local.py และสิ่งนี้ช่วยอธิบายความเข้าใจของฉัน ความรู้สึกที่หยาบคายของฉันบอกฉันว่านี่เป็นวิธีการออกแบบที่ซับซ้อน แต่ฉันยอมรับว่าฉันไม่เข้าใจทุกกรณีการใช้งานที่ตั้งใจไว้ "การเปลี่ยนเส้นทางภายใน" เป็นเพียงเหตุผลเดียวที่ฉันได้เห็นในคำอธิบายข้างต้นและ googling "การเปลี่ยนเส้นทางภายในขวด" ไม่เปิดขึ้นมากนักดังนั้นฉันยังคงสูญเสียอยู่เล็กน้อย หนึ่งในสิ่งที่ฉันชอบเกี่ยวกับกระติกน้ำคือโดยทั่วไปแล้วมันไม่ใช่สิ่ง java ประเภทซุปวัตถุที่เต็มไปด้วย AbstractProviderContextBaseFactories และเช่น
QuadrupleA

1
@QuadrupleA เมื่อคุณเข้าใจวิธีการเหล่านี้Local, LocalStackและLocalProxyการทำงานผมขอแนะนำให้ไปทบทวนบทความเหล่านี้ของเอกสาร: flask.pocoo.org/docs/0.11/appcontext , flask.pocoo.org/docs/0.11/extensiondevและflask.pocoo .org ความเข้าใจที่สดใหม่ของคุณอาจช่วยให้คุณเห็นแสงสว่างใหม่และอาจให้ข้อมูลเชิงลึกมากขึ้น
Michael Ekoka

อ่านผ่านลิงก์เหล่านี้ - พวกมันดูสมเหตุสมผล แต่การออกแบบก็ยังทำให้ฉันสับสนจนเกินไปและอาจฉลาดเกินกว่าที่มันจะดีได้ แต่ฉันไม่ใช่แฟนตัวยงของ OOP โดยทั่วไปและสิ่งที่ควบคุมการไหลโดยนัย (แทนที่ __call __ (), __getattr __ (), เหตุการณ์แบบไดนามิกที่ส่งไปเมื่อเทียบกับการเรียกฟังก์ชั่นแบบง่าย ๆ , การห่อสิ่งต่าง ๆ ในอุปกรณ์เข้าถึงคุณสมบัติ ) ดังนั้นอาจเป็นเพียงความแตกต่างในปรัชญา ไม่ใช่ผู้ประกอบการ TDD เช่นกันซึ่งดูเหมือนว่ามีจุดประสงค์เพื่อสนับสนุนเครื่องจักรพิเศษ
QuadrupleA

1
ขอบคุณที่แบ่งปันสิ่งนี้ชื่นชม การทำเธรดเป็นจุดอ่อนของภาษาเช่นไพ ธ อนคุณมีรูปแบบเหมือนด้านบนซึ่งรั่วไหลไปสู่เฟรมเวิร์กแอปพลิเคชัน Java เป็นอีกตัวอย่างหนึ่งในสถานการณ์ที่คล้ายคลึงกันอีกครั้ง threadlocals, เซมาฟอร์ ฯลฯ ยากที่จะได้รับสิทธิหรือบำรุงรักษา นี่คือที่ภาษาเช่น Erlang / Elixir (ใช้ BEAM) หรือวิธีวนรอบเหตุการณ์ (เช่น nginx vs apache ฯลฯ ) โดยทั่วไปจะให้วิธีที่มีประสิทธิภาพปรับขนาดได้และซับซ้อนน้อยกว่า
arcseldon

13

@Mark Hildreth เติมคำตอบเล็ก ๆ น้อย ๆ

สแต็กบริบทมีลักษณะคล้ายกับ{thread.get_ident(): []}ที่[]เรียกว่า "สแต็ก" เนื่องจากใช้การดำเนินการappend( push) popและ[-1]( __getitem__(-1)) เท่านั้น ดังนั้นสแต็กบริบทจะเก็บข้อมูลจริงสำหรับเธรดหรือเธรดกรีนเล็ต

current_app, g, request, sessionและอื่น ๆ เป็นLocalProxyวัตถุที่เพิ่ง overrided วิธีพิเศษ__getattr__, __getitem__, __call__, __eq__และอื่น ๆ และผลตอบแทนอย่างคุ้มค่าจากบริบทสแต็คบน ( [-1]) โดยใช้ชื่ออาร์กิวเมนต์ ( current_app, requestตัวอย่าง) LocalProxyจำเป็นต้องนำเข้าวัตถุนี้ครั้งเดียวและพวกเขาจะไม่พลาดจริง ดังนั้นดีกว่าเพียงนำเข้าrequestที่คุณเคยอยู่ในรหัสแทนที่จะเล่นกับการส่งอาร์กิวเมนต์คำขอไปยังฟังก์ชั่นและวิธีการของคุณ คุณสามารถเขียนส่วนขยายของตัวเองได้ง่าย ๆ แต่อย่าลืมว่าการใช้งานเล็ก ๆ น้อย ๆ อาจทำให้โค้ดยากขึ้นสำหรับการทำความเข้าใจ

ใช้เวลาในการทำความเข้าใจhttps://github.com/mitsuhiko/werkzeug/blob/master/werkzeug/local.py

ดังนั้นวิธีการบรรจุทั้งสองกอง? ตามคำขอFlask:

  1. สร้างrequest_contextโดยสภาพแวดล้อม (init map_adapter, match path)
  2. ป้อนหรือผลักดันคำขอนี้:
    1. ล้างหน้าที่แล้ว request_context
    2. สร้างapp_contextถ้าพลาดและผลักไปที่สแต็กบริบทแอปพลิเคชัน
    3. คำขอนี้ส่งไปยังขอสแต็กบริบท
    4. เซสชันเริ่มต้นถ้ามันพลาด
  3. ส่งคำขอ
  4. ยกเลิกการร้องขอและนำออกจากสแต็ก

2

ให้ยกตัวอย่างหนึ่งสมมติว่าคุณต้องการตั้งค่า usercontext (โดยใช้โครงสร้างขวดของ Local และ LocalProxy)

กำหนดหนึ่งคลาสผู้ใช้:

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

กำหนดฟังก์ชั่นเพื่อดึงข้อมูลวัตถุผู้ใช้ใหม่ภายในเธรดหรือกรีนเล็ตปัจจุบัน

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

ตอนนี้กำหนด LocalProxy

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

ตอนนี้จะได้รับหมายเลขผู้ใช้ของผู้ใช้ในปัจจุบันด้าย usercontext.userid

คำอธิบาย:

1.Local มี dict of identity และ objet เอกลักษณ์คือ threadid หรือ greenlet id ในตัวอย่างนี้ _local.user = User () เป็น eqivalent ถึง _local .___ storage __ [id ของเธรดปัจจุบัน] ["ผู้ใช้"] = ผู้ใช้ ()

  1. LocalProxy มอบหมาย การดำเนินการเพื่อห่อวัตถุท้องถิ่นหรือคุณสามารถให้ฟังก์ชั่นที่ส่งกลับวัตถุเป้าหมาย ในตัวอย่างข้างต้นฟังก์ชั่น get_user ให้วัตถุผู้ใช้ปัจจุบันเพื่อ LocalProxy และเมื่อคุณขอ userid ของผู้ใช้ปัจจุบันโดย usercontext.userid ฟังก์ชั่น __getattr__ ของ LocalProxy จะเรียกใช้ get_user เพื่อรับวัตถุผู้ใช้ (ผู้ใช้) จากนั้นเรียก getattr (ผู้ใช้ "userid") การตั้งค่าหมายเลขผู้ใช้บนผู้ใช้ (ในเธรดหรือกรีนเล็ตปัจจุบัน) คุณทำได้โดย: usercontext.userid = "user_123"
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.