الانتظار دون التوقف عن العمل
في معظم الوقت لا يحسب برنامج البيانات بل ينتظر: ردّ خادم الأسعار أو تأكيد أمر أو انتهاء تنزيل. وإذا طلب عشرة أسعار واحدًا تلو الآخر انتظر عشر مرات متتالية. والشيفرة غير المتزامنة تتيح للبرنامج أن يبدأ طلبًا ويضعه جانبًا أثناء انتظاره ويمضي إلى غيره، كل ذلك في خيط واحد. وأداة بايثون لذلك هي asyncio المبنية على كلمتين مفتاحيتين: async def وawait.
# A coroutine: calling it does nothing until it is awaited
import asyncio
async def get_price(symbol):
await asyncio.sleep(0.01) # stands in for waiting on a network answer
return {"AAPL": 184.25, "MSFT": 410.5}[symbol]
coro = get_price("AAPL")
print(type(coro).__name__) # coroutine nothing has run yet
print(asyncio.run(coro)) # 184.25 the event loop runs itasync def و await وحلقة الأحداث
الدالة المعرَّفة بـ async def هي دالة كوروتين: استدعاؤها يعيد كائن كوروتين ولا ينفذ شيئًا من جسمها. وداخلها يوقف await شيئًا الكوروتين حتى يجهز ذلك الشيء ويعيد التحكم إلى حلقة الأحداث التي تستطيع تشغيل كوروتينات أخرى في الأثناء. وasyncio.run(coro) تبدأ حلقة وتشغّل كوروتينًا حتى النهاية. ولا يُستعمل await إلا داخل async def؛ ونسيانه هو الخطأ الكلاسيكي لأن النداء يعيد حينها كوروتينًا بدل الجواب.
# Awaiting ten prices one after another waits ten times; gather overlaps the waits
import asyncio
import time
async def get_price(symbol):
await asyncio.sleep(0.05)
return len(symbol) * 10.0
async def one_by_one(symbols):
return [await get_price(s) for s in symbols]
async def together(symbols):
return await asyncio.gather(*(get_price(s) for s in symbols))
symbols = ["AAPL", "MSFT", "NVDA", "TSLA", "AMZN", "META", "GOOG", "NFLX", "ORCL", "INTC"]
t0 = time.perf_counter()
asyncio.run(one_by_one(symbols))
slow = time.perf_counter() - t0
t0 = time.perf_counter()
asyncio.run(together(symbols))
fast = time.perf_counter() - t0
print(f"one by one: {slow:.2f} s, gathered: {fast:.2f} s") # about 0.50 s against 0.05 s
print(slow > 4 * fast) # Trueالأخطاء والمهلات والتنفيذ اليدوي
قد يفشل الانتظار. والطلب الذي لا يجيب أبدًا يجب قطعه بـ asyncio.wait_for(coro, timeout) التي ترفع TimeoutError؛ وawait داخل try / except يعالج الأخطاء كالشيفرة العادية تمامًا، فإعادة المحاولة حلقة حول نداء منتظَر. وفي الباطن الكوروتين شيء يمكن تقديمه خطوة خطوة: coro.send(None) يشغّله حتى توقفه التالي وعند انتهائه يرفع StopIteration حاملًا النتيجة. وحلقة الأحداث هي الشيفرة التي تكرر ذلك لكثير من الكوروتينات معًا، وهكذا تشغّل اختبارات هذا المختبر كوروتيناتك.
# Stepping a coroutine by hand: this is what an event loop does for many of them
class Pause:
def __await__(self):
yield
async def job():
await Pause()
await Pause()
return 184.25
coro = job()
steps = 0
while True:
try:
coro.send(None)
except StopIteration as stop:
print("finished with", stop.value) # finished with 184.25
break
steps += 1
print("paused", steps, "times") # paused 2 times# A timeout turns a slow answer into an error you can handle, then retry
import asyncio
async def slow_feed():
await asyncio.sleep(1.0)
return 184.25
async def main():
for attempt in range(1, 4):
try:
return await asyncio.wait_for(slow_feed(), timeout=0.01)
except TimeoutError:
print(f"attempt {attempt} timed out")
return None
print(asyncio.run(main())) # three timeouts, then Noneasyncio كوروتينًا واحدًا في كل مرة ولا يبدّل إلا عند await. وهو يسرّع انتظار الشبكة لا الحساب الثقيل: فالحساب الطويل داخل كوروتين يحجب الحلقة وكل الكوروتينات معه. أجرِ الحساب في دوال عادية واحتفظ بالكوروتينات للانتظار.async defتعرّف دالة كوروتين؛ واستدعاؤها ينشئ كوروتينًا فقط ولا يشغّل شيئًا بعد.awaitيتوقف حتى تجهز النتيجة ويدع حلقة الأحداث تشغّل غيره؛ ولا يعمل إلا داخلasync def.asyncio.runتبدأ الحلقة وgatherتداخل الانتظارات وwait_forتضيف مهلة؛ وتُعالج الأخطاء بـtry/exceptعادية.
التالي: بث البيانات عبر websockets، استقبال تدفق حي من التيكات لحظة حدوثها.
المختبر: ما الذي يُقيَّم
- انتظر سعرًا واحدًا باستخدام
get_price25 نقطة - اجلب قائمة المراقبة باستخدام
get_prices25 نقطة - تجاوز انتهاء المهلة باستخدام
retry_price25 نقطة - كن حلقة الأحداث باستخدام
drive25 نقطة