Ачааллж байна...
Энэ сургалт iteration-ээр эхэлсэн. Бүлэг 1-д for loop-ийн ард юу байдгийг задалсан: iter(), next(), StopIteration. Бүлэг 2-т generator-ийн зогсоох товч yield-ийг эзэмшиж, Хичээл 2.3-т өгөгдөл үе шат дамжин урсдаг pipeline бүтээсэн. Хичээл 14.1-т тэр зогсоох товч asyncio-гийн үндэс болохыг харсан. Одоо, сургалтын сүүлчийн бүлэгт, хоёр урсгал нийлнэ: iteration ба asyncio.
Хичээл 2.3-ын pipeline-д нэг далд таамаглал байсан: дараагийн элемент шууд бэлэн. Файлаас мөр унших хурдан, жагсаалтаас элемент авах хурдан. Гэвч өгөгдөл сүлжээгээр ирдэг бол яах вэ? Дэлгүүрийн системээс захиалгууд нэг нэгээрээ ирнэ, хоёр захиалгын хооронд хагас секунд өнгөрч болно. Энгийн generator дараагийн захиалгыг хүлээхдээ thread-ийг блоклоно: Хичээл 14.1-ийн хамгийн чухал дүрмийг зөрчинө.
Энэ хичээлд iteration-ий asyncio хувилбаруудыг эзэмшинэ. async generator бол yield ба await хоёуланг нь агуулдаг функц: дараагийн элементийг хүлээж болох generator. async for бол түүнийг давтах давталт. Async iterator протокол бол Бүлэг 1-ийн протоколын хувилбар. Бид Хичээл 2.3-ын pipeline-ийг асинхрон хэлбэрээр дахин бүтээж, захиалга ирэх тусам боловсруулахын давуу талыг хэмжинэ. Дараа нь Хичээл 14.2-ын task-уудыг async generator-тэй хослуулж, ажлуудыг зэрэг ажиллуулаад үр дүнг бэлэн болох дарааллаар урсгана. Эцэст нь Хичээл 6.3-ын цэвэрлэгээний санааг энд ч хэрэглэнэ: давталтаас эрт гарахад нөөцийг хэрхэн суллах вэ.
Шуудангийн хоёр төрлийн хүргэлтийг төсөөлөөд үзээрэй. Эхнийх нь сарын төгсгөлд бүх захидлыг нэг боодол болгож авчирдаг. Та тэдгээрийг уншиж эхлэхийн тулд сарын төгсгөлийг хүлээх ёстой. Энэ бол жагсаалт: бүх өгөгдөл ирсний дараа л боловсруулалт эхэлнэ.
Хоёр дахь нь захидал ирэх бүрт хаалгаа тогшдог. Та нэг захидлыг уншиж дуусахад дараагийнх нь хараахан ирээгүй байж болно. Тэр үед та хаалганы өмнө хөшиж зогсохгүй: цайгаа ууж, бусад ажлаа хийж байгаад, тогшилт сонсогдмогц очно. Захидал бүрийг ирмэгц нь уншина: эхний захидлыг сарын эхэнд л мэднэ.
Энэ хоёр дахь хүргэлтэд хоёр тал бий. Захидлууд дараалсан ирдэг, тиймээс энэ бол iteration. Гэхдээ хоёр захидлын хооронд хүлээх хугацаа бий бөгөөд тэр хугацаанд та өөр ажил хийж чадна. Тиймээс энэ бол asyncio. Async generator бол яг энэ: ирэх тусам нэг нэгээр нь өгдөг, хүлээх хооронд loop-д ээлжээ өгдөг өгөгдлийн эх үүсвэр.
Сүлжээгээр ирж буй захиалгуудын урсгалыг загварчилъя. Захиалга бүрийн хооронд санамсаргүй хугацаа өнгөрнө:
python
import asyncio
import random
import time
async def order_stream(count, seed=7):
rng = random.Random(seed)
for i in range(count):
await asyncio.sleep(rng.uniform(0.05, 0.3))
yield {"id": i, "amount": rng.randint(-20_000, 200_000)}
async def main():
start = time.perf_counter()
async for order in order_stream(5):
print(f"{time.perf_counter() - start:.2f} с: #{order['id']} {order['amount']:,}₮")
asyncio.run(main())0.12 с: #0 143,507₮
0.39 с: #1 -8,912₮
0.47 с: #2 61,230₮
0.71 с: #3 188,044₮
0.83 с: #4 27,615₮Таны тоонууд ижил seed-тэй тул тогтвортой, миний харуулснаас ялгаатай байж болно.
order_stream бол async generator: async def доторх yield. Хичээл 2.1-ийн generator-ийн бүх санаа энд хадгалагдана: функцийг дуудахад биеийнх нь код ажиллахгүй, объект буцна. Тэр объектоос элемент бүрийг авахад код дараагийн yield хүртэл ажиллаж зогсоно. Ганц ялгаа: yield-ийн хооронд await байж болно. Захиалга бүрийн өмнөх await asyncio.sleep(...) сүлжээнээс дараагийн мессеж ирэхийг хүлээж байна.
async for бол түүнийг давтах давталт. Энгийн for-оос ялгаатай нь дараагийн элементийг авахдаа await хийдэг: хэрэв элемент хараахан бэлэн биш бол loop-д ээлжээ өгнө. Хугацааны багана үүнийг харуулна: захиалга ирэх бүрт нэг мөр хэвлэгдсэн, бүгд ирэхийг хүлээгээгүй. Хэрэв энэ програм өөр coroutine-уудтай зэрэг ажиллаж байсан бол захиалга хүлээх бүх хугацаанд тэдгээр ажилласаар байх байсан.
Хичээл 1.3-т for loop-ийг задалж, iter() ба next()-ийн давталт гэдгийг харсан. async for бол ижил бүтэц, гэхдээ асинхрон хувилбаруудтай. Хичээл 1.4-ийн Countdown-ийг санаарай. Түүний асинхрон хувилбар:
python
class AsyncCountdown:
def __init__(self, start):
self.current = start
def __aiter__(self):
return self
async def __anext__(self):
if self.current <= 0:
raise StopAsyncIteration
await asyncio.sleep(0.1)
self.current -= 1
return self.current + 1
async def main():
async for number in AsyncCountdown(3):
print(number)
asyncio.run(main())3
2
1Хичээл 1.4-тэй мөр мөрөөр нь харьцуул. __iter__ нь __aiter__ болсон: async for эхлэхдээ үүнийг дуудна. __next__ нь async def __anext__ болсон: дараагийн элемент бүрийг авахад await хийгдэнэ, тиймээс түүний дотор хүлээж болно. StopIteration нь StopAsyncIteration болсон: "элемент дууслаа" гэсэн дохио. Үүнийг сольсны шалтгаан нь Хичээл 2.1-ийн RuntimeError-ийн түүхтэй адил: энгийн StopIteration coroutine-ийн дотор өөр утгатай тул төөрөгдөл үүсгэнэ.
Хичээл 1.4-т бид class-аар iterator бичиж, дараа нь generator хэр хялбар болохыг харсан. Энд ч ижил: async generator ихэнх тохиолдолд class-аас хамаагүй товч. Протоколыг мэдэх нь гурван шалтгаантай. Гуравдагч талын сангууд ихэвчлэн class-аар бичигддэг тул алдааны мессежийг ойлгох. Өөрийн class-д __aiter__ нэмэх, Хичээл 8.4-ийн Playlist-д __iter__ нэмсэн шиг. Мөн async for "ид шид" биш, ердөө хоёр method гэдгийг мэдэх.
Хичээл 2.3-ын pipeline-ийг асинхрон хэлбэрээр бүтээе. Захиалгын урсгалаас буцаалтыг, сөрөг дүнг, шүүж, үлдсэнд нь НӨАТ тооцоолно:
python
async def only_sales(orders):
async for order in orders:
if order["amount"] > 0:
yield order
async def with_tax(orders, rate=0.10):
async for order in orders:
yield {**order, "tax": round(order["amount"] * rate)}
async def main():
start = time.perf_counter()
pipeline = with_tax(only_sales(order_stream(10)))
async for order in pipeline:
print(f"{time.perf_counter() - start:.2f} с: #{order['id']} "
f"{order['amount']:,}₮, НӨАТ {order['tax']:,}₮")
asyncio.run(main())0.12 с: #0 143,507₮, НӨАТ 14,351₮
0.47 с: #2 61,230₮, НӨАТ 6,123₮
0.71 с: #3 188,044₮, НӨАТ 18,804₮
...Үе шат бүр async generator: өмнөх үе шатаас async for-оор авч, өөрийн боловсруулалтыг хийж, yield-ээр дараагийнх руу өгнө. #1 захиалга, буцаалт, only_sales-д шүүгдсэн. {**order, "tax": ...} нь Хичээл 7.4-ийн replace-ийн dict хувилбар: анхны dict-ийг өөрчлөхгүй, шинэ түлхүүртэй хуулбар үүсгэнэ. Монголд НӨАТ 10 хувь тул rate-ийн анхдагч утга 0.10.
Хичээл 2.3-ын гол санаа энд бүрэн хадгалагдсан: pipeline pull-based, татах зарчимтай. Давталт with_tax-аас нэг захиалга хүсэхэд with_tax only_sales-аас хүсэж, only_sales order_stream-ээс хүснэ. order_stream сүлжээг хүлээж байх хооронд гинж бүхэлдээ loop-д ээлжээ өгнө. Санах ойд нэг агшинд нэг л захиалга байна: Хичээл 3.4-ийн 30 килобайт ба 131 мегабайтын хэмжилтийн санаа энд ч хүчинтэй.
Хэмжилт: эхний үр дүн хэзээ ирэх вэ
Асинхрон урсгалын бас нэг давуу тал бий: хоцрогдол. Эхлээд бүх захиалгыг жагсаалтад цуглуулаад дараа нь боловсруулах аргатай харьцуулъя. Async comprehension нь list comprehension-ий асинхрон хувилбар: [x async for x in ...]:
python
async def first_result_streaming():
start = time.perf_counter()
async for order in with_tax(only_sales(order_stream(10))):
return time.perf_counter() - start
async def first_result_collected():
start = time.perf_counter()
orders = [order async for order in order_stream(10)]
sales = [o for o in orders if o["amount"] > 0]
taxed = [{**o, "tax": round(o["amount"] * 0.10)} for o in sales]
if taxed:
return time.perf_counter() - start
async def main():
print(f"Урсгал: эхний үр дүн {await first_result_streaming():.2f} с")
print(f"Цуглуулга: эхний үр дүн {await first_result_collected():.2f} с")
asyncio.run(main())Урсгал: эхний үр дүн 0.12 с
Цуглуулга: эхний үр дүн 1.71 сХоёр арга бүх захиалгыг боловсруулахад ижил хугацаа зарцуулна: сүлжээ хурдыг хязгаарлаж байна. Гэвч урсгал эхний үр дүнг 0.12 секундэд, цуглуулга 1.71 секундэд өгсөн. Арван захиалгад ялгаа нэг хагас секунд. Мянган захиалгатай бол хэдэн минут. Хэрэглэгчид захиалга бүрийг ирмэгц харуулах dashboard, эсвэл сэжигтэй гүйлгээг ирмэгц илрүүлэх систем: ийм програмд хоцрогдол бол гол шаардлага. Шуудангийн зүйрлэлээр: захидал бүрийг ирмэгц уншина, сарын төгсгөлийг хүлээхгүй.
first_result_streaming-д async for-ийн дундаас return хийсэн нь шинэ асуулт үүсгэнэ: эхний захиалгын дараа pipeline юу болсон бэ? Хичээлийн төгсгөлд энэ асуултад буцаж ирнэ.
async for өөрөө дараалсан: элемент бүрийг ар араас нь татна. order_stream-д энэ зөв, учир нь сүлжээ захиалгуудыг дарааллаар илгээдэг. Гэвч хэрэв өгөгдлийн эх үүсвэрүүд бие даасан бол, жишээлбэл гурван өөр серверээс татах, тэдгээрийг зэрэг ажиллуулж, үр дүнг бэлэн болох дарааллаар урсгахыг хүснэ. Хичээл 14.2-ын task ба Хичээл 13.2-ын as_completed-ийн санааг async generator-тэй хослуулъя:
python
async def fetch(name, seconds):
try:
await asyncio.sleep(seconds)
except asyncio.CancelledError:
print(f"{name}: цуцлагдлаа")
raise
return f"{name}: татагдлаа"
async def fetch_as_ready(jobs):
tasks = [asyncio.create_task(fetch(name, seconds)) for name, seconds in jobs.items()]
try:
for next_done in asyncio.as_completed(tasks):
yield await next_done
finally:
for task in tasks:
task.cancel()
async def main():
jobs = {"том": 3.0, "жижиг": 1.0, "дунд": 2.0}
start = time.perf_counter()
async for result in fetch_as_ready(jobs):
print(f"{time.perf_counter() - start:.1f} с: {result}")
asyncio.run(main())1.0 с: жижиг: татагдлаа
2.0 с: дунд: татагдлаа
3.0 с: том: татагдлааfetch_as_ready гурван task-ийг нэг дор эхлүүлнэ: тэд зэрэг ажиллана. asyncio.as_completed() бол Хичээл 13.2-ын as_completed-ийн asyncio хувилбар: давтах бүрт дараагийн дуусах ажлыг await хийж болох объект өгнө. Бид түүнийг await хийгээд үр дүнг yield хийнэ. Хэрэглэгч талаас энэ бол энгийн async for: тэр task, as_completed-ийн талаар юу ч мэдэх шаардлагагүй. Зэрэгцээ байдал generator-ийн дотор нуугдсан, Хичээл 9.2-ын Strategy-ийн адил нарийн ширийн зүйл нэг газар.
Python 3.13-аас хойш asyncio.as_completed-ийг шууд async for-оор давтаж болно. Энэ сургалт 3.12-оос хойшхи хувилбаруудыг дэмждэг тул бид хоёр хувилбарт ажиллах энгийн for хэлбэрийг хэрэглэсэн.
Generator-ийн finally блокийг анзаараарай: тэр бүх task-ийг цуцална. Бүх task аль хэдийн дууссан бол cancel() юу ч хийхгүй. Гэвч хэрэглэгч давталтаас эрт гарвал энэ блок хамгийн чухал болно.
Хэрэглэгч зөвхөн эхний үр дүнг хүсэж, түүний дараа давталтаас гарвал:
python
async def main():
jobs = {"том": 3.0, "жижиг": 1.0, "дунд": 2.0}
async for result in fetch_as_ready(jobs):
print("Эхнийх нь хангалттай:", result)
break
print("main цааш үргэлжиллээ")
await asyncio.sleep(3.0)Бүлэг 2-т энгийн generator-ээс break хийхэд юу болдгийг санаарай: generator yield дээр зогссон хэвээр үлдэж, Python түүнийг хэрэглэгдэхгүй болоход нь хаадаг. Энгийн generator-т тэр хаалт ихэвчлэн шууд явагддаг. Async generator-т асуудал бий: түүний finally блок дотор await байж болно, энгийн санах ойн цэвэрлэгч await хийж чадахгүй. Тиймээс asyncio async generator-ийн хаалтыг дараа нь, loop-ийн тохиромжтой агшинд хийхээр хойшлуулдаг: заримдаа зөвхөн asyncio.run дуусах үед. Тэр хүртэл "том" ба "дунд" task-ууд цаана нь ажилласаар, Хичээл 14.2-ын өнчин ажлууд.
Шийдэл бол Хичээл 6.3-ын хэв маяг: цэвэрлэгээг тодорхой хугацаанд баталгаажуулах context manager. contextlib модулийн aclosing async generator-ийг with блок дуусахад шууд хаана:
python
from contextlib import aclosing
async def main():
jobs = {"том": 3.0, "жижиг": 1.0, "дунд": 2.0}
async with aclosing(fetch_as_ready(jobs)) as results:
async for result in results:
print("Эхнийх нь хангалттай:", result)
break
print("main цааш үргэлжиллээ")
await asyncio.sleep(3.0)
asyncio.run(main())Эхнийх нь хангалттай: жижиг: татагдлаа
том: цуцлагдлаа
дунд: цуцлагдлаа
main цааш үргэлжиллээbreak-ийн дараа async with блок дуусч, aclosing generator-ийн aclose() method-ийг дуудна: Хичээл 6.3-ын throw-той төстэй механизм, generator-ийн зогссон yield-д GeneratorExit шидэж, finally-г ажиллуулна. finally үлдсэн хоёр task-ийг цуцалж, тэд "цуцлагдлаа" хэвлэсэн, бүгд main үргэлжлэхээс өмнө. Өнчин ажил байхгүй.
Дүрэм: async generator нөөц барьдаг бол, task, сүлжээний холболт, нээлттэй файл, түүнийг эрт орхиж болох газарт aclosing-оор боо. Хичээлийн эхний first_result_streaming функц ч яг энэ асуудалтай байсан: return-ийн дараа pipeline хаагдахыг хойшлуулсан. Тэнд барьсан нөөц байгаагүй тул хор хөнөөлгүй, гэвч бодит pipeline-д aclosing зөв хэлбэр.
Хүсвэл Хичээл 2.3-ын log pipeline-ийн чагнуурын санааг энд хэрэглэж үзээрэй: үе шат бүрт "татлаа", "өглөө" гэж хэвлэдэг async generator бичиж, асинхрон pipeline-ийн татах дарааллыг trace хийх. Сонирхвол only_sales ба with_tax-ийг Хичээл 3.1-ийн map ба filter-ийн асинхрон хувилбар болгон ерөнхийлж болно: async def amap(func, source) ба async def afilter(pred, source). Мөн fetch_as_ready-д Хичээл 14.3-ын Semaphore нэмж, зэрэг ажлын тоог хязгаарлах нь энэ бүлгийн бүх хэрэгслийг нэг функцэд нэгтгэх сонирхолтой дасгал. Эдгээр бүгд санал бөгөөд дараагийн хичээлүүд үүнийг хийсэн гэж үзэхгүй.
TypeError: ‘async_generator’ object is not iterable. “async_generator объект iterable биш” гэсэн утгатай. Async generator-ийг энгийн for-оор давтахад гарна. Энгийн for __iter__ хайдаг, async generator зөвхөн __aiter__-тэй. async for хэрэглэ, мөн тэр async def дотор байх ёстой.
TypeError: ‘async for’ requires an object with aiter method, got list. “‘async for’-д aiter method-той объект хэрэгтэй, list ирлээ” гэсэн утгатай. Эсрэг тохиолдол: энгийн жагсаалтыг async for-оор давтахад. Жагсаалтын элементүүд аль хэдийн бэлэн тул энгийн for хангалттай.
SyntaxError: ‘async for’ outside async function. “‘async for’ async функцээс гадна” гэсэн утгатай. Хичээл 14.1-ийн await-ийн дүрэм энд ч: async for, async with ба async comprehension зөвхөн async def дотор. Модулийн түвшинд туршихад Хичээл 14.1-ийн python -m asyncio REPL тохиромжтой.
SyntaxError: ‘yield from’ inside async function. “async функц дотор ‘yield from’” гэсэн утгатай. Хичээл 2.3-т бид generator-ийг өөр generator руу шилжүүлэхэд yield from хэрэглэсэн. Async generator-т энэ синтакс байхгүй: оронд нь async for item in source: yield item гэж бич. Мөн async generator-ээс return value хийх боломжгүй: SyntaxError: 'return' with value in async generator. Хоосон return зөвшөөрөгдөнө.
Эрт гарсны дараа нөөц чөлөөлөгдөхгүй. Мессежгүй, хичээлд харсан. Async generator-ийн finally хаалт хойшлогдож, task, холболт, файл дараа нь, заримдаа програм дуусах үед л чөлөөлөгдөнө. Нөөц барьдаг async generator-ийг aclosing-оор боо.
Async generator бол yield ба await хоёуланг агуулдаг async def: дараагийн элементийг хүлээж болох generator. async for түүнийг давтаж, элемент бэлэн биш үед loop-д ээлжээ өгнө. Async iterator протокол Бүлэг 1-ийн протоколын хувилбар: __aiter__, async def __anext__, StopAsyncIteration. Async comprehension [x async for x in source] бүх элементийг жагсаалтад цуглуулна. Хичээл 2.3-ын pull-based pipeline асинхрон хэлбэрт бүрэн хадгалагдана: үе шат бүр async generator, нэг агшинд нэг элемент. Урсгал эхний үр дүнг ирмэгц өгдөг: хэмжилтэд 0.12 секунд, бүгдийг цуглуулах аргад 1.71 секунд. async for өөрөө дараалсан. Бие даасан эх үүсвэрүүдийг зэрэг ажиллуулахдаа task-уудыг async generator дотор эхлүүлж, asyncio.as_completed-ээр бэлэн болох дарааллаар yield хийнэ. Async generator-ийн finally хаалт эрт гарахад хойшлогдож болох тул нөөц барьдаг generator-ийг contextlib.aclosing-оор боож, цэвэрлэгээг тодорхой агшинд баталгаажуулна.
Энэ бүлгийн бүх жишээ сүлжээг asyncio.sleep-ээр загварчилсан. Дараагийн хичээлд загварчлал дуусна: бид жинхэнэ HTTP хүсэлтийг асинхрон сангаар илгээж, Түвшин 5-ын бүх хэрэгслийг, TaskGroup, timeout, Semaphore, async generator, бодит сүлжээний хариу, алдаа ба удаашралтай нь хэрэглэнэ. Энэ нь сургалтын нэгдсэн төсөлд бэлтгэл болно.
Бүртгэлтэй болсноор энэ сургалтын бүх хичээлд хандах эрх авна.