آموزش ماژول asyncio – مروری بر Futures

Please login to bookmark Close

Future چیست؟

در asyncio، یک Future شیئی است که نماینده‌ی یک «نتیجه‌ی آینده» است؛ یعنی مقدار یا استثنایی که هنوز آماده نیست اما قرار است در آینده در دسترس قرار گیرد. می‌توانید Future را مثل یک «قبض تحویل» تصور کنید: هنوز خودِ کالا (نتیجه) نرسیده، اما یک برگه دارید که به‌محض آماده‌شدن، کالا را با آن تحویل می‌گیرید. Future یا مقدار دارد (انجام‌شده / done) یا ندارد (انجام‌نشده / pending).

ساخت و استفاده‌ی ساده از یک Future

در ساده‌ترین حالت، یک Future می‌سازیم، نتیجه‌اش را با set_result تنظیم می‌کنیم و با result() می‌خوانیم:

import asyncio

# create a Future object
f = asyncio.Future()

# set the result of the Future
f.set_result(1)

# access the result
print(f, f.result())

اما اجرای کد بالا این هشدار را می‌دهد:

DeprecationWarning: There is no current event loop
<Future pending>

دلیل هشدار این است که ما این Future را در هیچ event loop‌ای نساخته‌ایم؛ در حالی که هر Future باید به یک event loop تعلق داشته باشد. با کمی تغییر و ساختِ Future داخل یک coroutine که با asyncio.run اجرا می‌شود، مشکل حل می‌شود:

import asyncio

async def main():
    f = asyncio.Future()
    f.set_result(1)
    print(f, f.result())

asyncio.run(main())

روش تمیزترِ ساخت Future، استفاده از خودِ حلقه است: loop = asyncio.get_running_loop() و سپس fut = loop.create_future(). این روشِ توصیه‌شده است.

منتظرماندن (await) روی یک Future

قدرت واقعی Future وقتی دیده می‌شود که یک coroutine روی آن await کند و coroutine دیگری نتیجه‌اش را در آینده تنظیم کند. تا وقتی نتیجه تنظیم نشده، await معلق می‌ماند (بدون مصرف CPU) و به‌محض set_result، ادامه می‌یابد:

import asyncio

async def set_after(fut, delay, value):
    await asyncio.sleep(delay)
    fut.set_result(value)              # resolve the future later

async def main():
    loop = asyncio.get_running_loop()
    fut = loop.create_future()

    asyncio.create_task(set_after(fut, 1, "done"))
    print("waiting for the future...")
    result = await fut                 # suspends until the future is resolved
    print("got:", result)

asyncio.run(main())

تنظیم خطا با set_exception

یک Future می‌تواند به‌جای مقدار، یک «استثنا» را نگه دارد. اگر با set_exception خطایی روی Future بگذاریم، هنگام await کردن همان خطا پرتاب می‌شود:

import asyncio

async def main():
    loop = asyncio.get_running_loop()
    fut = loop.create_future()
    fut.set_exception(ValueError("boom"))

    try:
        await fut
    except ValueError as e:
        print("caught:", e)

asyncio.run(main())

اجرای کاری هنگام آماده‌شدن با add_done_callback

می‌توانید یک تابع را ثبت کنید تا به‌محض «آماده‌شدن» Future (چه با نتیجه، چه با خطا) اجرا شود:

import asyncio

async def main():
    loop = asyncio.get_running_loop()
    fut = loop.create_future()
    fut.add_done_callback(lambda f: print("callback got:", f.result()))
    fut.set_result(42)
    await asyncio.sleep(0)          # give the loop a chance to run the callback

asyncio.run(main())

Future در برابر Task

نکته‌ی مهم: Task در واقع نوع خاصی از Future است که یک coroutine را اجرا می‌کند و نتیجه‌اش را خودش تنظیم می‌کند. یعنی وقتی asyncio.create_task(coro) می‌نویسید، یک Future می‌گیرید که به‌طور خودکار با پایان آن coroutine «آماده» می‌شود. به همین دلیل در کد روزمره معمولاً مستقیماً Future نمی‌سازیم؛ با Task، gather و… کار می‌کنیم. ساختِ دستی Future بیشتر برای نویسندگان کتابخانه و «پل‌زدن» کدهای قدیمیِ مبتنی بر callback به دنیای async کاربرد دارد.

مثال واقعی: پل‌زدن یک API مبتنی بر callback به async

رایج‌ترین کاربرد واقعیِ ساختِ دستیِ Future این است: یک کتابخانه‌ی قدیمی دارید که وقتی داده آماده شد یک «callback» را صدا می‌زند، اما شما می‌خواهید به‌سبک await با آن کار کنید. یک Future می‌سازید، در callback نتیجه را روی آن set_result می‌کنید و در کد async خود آن را await می‌کنید. اینجا این رفتار را با loop.call_later شبیه‌سازی می‌کنیم (که مثل یک callbackِ تأخیری عمل می‌کند):

import asyncio

async def main():
    loop = asyncio.get_running_loop()
    fut = loop.create_future()

    # imagine a legacy library that invokes a callback when data is ready.
    # here we simulate it: after 1 second, the callback resolves the future.
    loop.call_later(1, lambda: fut.set_result("data ready"))

    print("waiting...")
    data = await fut                # now we can use it with async/await
    print("received:", data)

asyncio.run(main())

به این ترتیب، Future مثل یک «پل» بین دو دنیا عمل می‌کند: کدی که با callback خبر می‌دهد و کدی که می‌خواهد با await منتظر بماند. همچنین یک Future می‌تواند به‌عنوان یک «سیگنال یک‌باره» بین دو coroutine استفاده شود؛ یکی await fut می‌کند و دیگری هر وقت خواست با fut.set_result(...) آن را بیدار می‌کند.

جمع‌بندی

  • Future شیئی است که نماینده‌ی یک نتیجه‌ی آینده (مقدار یا استثنا) است و یا «انجام‌نشده» است یا «انجام‌شده».
  • نتیجه را با set_result و خطا را با set_exception تنظیم می‌کنیم و با result() یا await می‌خوانیم.
  • هر Future باید در یک event loop ساخته شود؛ روش توصیه‌شده loop.create_future() است.
  • Task نوعی Future است که یک coroutine را اجرا می‌کند؛ در کد روزمره معمولاً با Task کار می‌کنیم و ساخت دستیِ Future بیشتر برای پل‌زدن کدهای callback-محور است.

Please login to bookmark Close
پیشرفت شما در «دوره آموزش کانکارنسی در پایتون» (48%)
نظرات

دیدگاهتان را بنویسید

48%
پیشرفت

سرفصل دوره

فهرست مطالب

سرفصل دوره

تمرین

این قسمت تمرین ندارد!

پاسخ تمرین ها

هنوز برای تمرین‌های این قسمت پاسخی ثبت نشده است!

اشتراک گذاری

چرا بهتره از فیلترشکن استفاده کنید؟

من همه ویدئو ها و پادکست های کُدباز رو توی یوتیوب و ساندکلود و پلتفرم هایی آپلود می‌کنم که اغلب فیلتر هستند.

اغلب آموزش‌ها ویدئو و پادکست دارند. پس اگر می‌خواهید از محتوای سایت بیشترین استفاده رو ببرید نیاز به فیلتر شکن دارید.

توجه داشته باشید که برای خرید از فروشگاه بهتره فیلتر شکن رو خاموش کنید.

تنظیمات

انتخاب زبان
تغییر تم