Kuhama kutoka JavaScript kwenda Python kunahisi kama kuhama kwenda kwenye mji wenye alama za barabarani zinazofanana lakini sheria za trafiki ni tofauti. Muundo wa kodi (syntax) unaonekana rafiki na wa kawaida. Unaona async na await zikiwa hapo hapo kwenye sarufi, hivyo unadhani mfumo wa mawazo (mental model) utahamia kirahisi. Hali si hivyo. Tabia moja ya kawaida kutoka JavaScript inaharibu utendaji wa Python bila kusababisha hitilafu (crash), bila kuandika kosa (error), na bila kuonekana wakati wa mapitio ya haraka ya kodi.

Jinsi JavaScript Inavyokufundisha Kuanza na Kusahau

Katika JavaScript, mwito wa async function ni wa haraka (eager). Wakati unapoita, injini inatengeneza Promise na kazi inaanza mara moja. Event loop tayari imeanza kazi. Ndiyo maana watengenezaji wa JavaScript huandika kodi kama hii kiasili:

const userPromise = fetchUser(id);
const ordersPromise = fetchOrders(id);
const user = await userPromise;
const orders = await ordersPromise;

Maombi yote mawili ya mtandao yanaendelea kabla ya kufika kwenye await yoyote. The await ya kwanza inazuia kazi ya sasa hadi fetchUser itakapokamilika, lakini fetchOrders imekuwa ikifanya kazi kwa nyuma tangu mstari uliopita. Unapohitaji variable ya orders, ombi la pili linaweza kuwa limeshakamilika. Mtindo huu unaonekana wa kawaida sana katika JavaScript kiasi kwamba watengenezaji wengi hawaufikirii kama mbinu ya uendeshaji sambamba (concurrency). Ni jinsi tu async inavyofanya kazi.

Mshangao wa Python: Coroutine Baridi

Python inatumia mkataba tofauti. Unapoita function ya async def katika Python, hauanzishi kazi yoyote. Unapokea coroutine object. Ifikirie kama mapishi yaliyoandikwa kwenye karatasi. Viungo vimeorodheshwa, hatua ziko wazi, lakini hakuna kitu kilicho ndani ya oveni. Mpaka kitu fulani kitakachoiendesha coroutine hiyo kupitia event loop, itabaki bila kufanya kazi.

Hapa ndipo mtego ulipo. Mhandisi wa JavaScript anayehitaji mtumiaji na oda zake anaweza kuandika hivi katika Python:

user_coro = fetch_user(id)
orders_coro = fetch_orders(id)
user = await user_coro
orders = await orders_coro

Inaonekana kama inafanya kazi sambamba. Inahisi kama inafanya kazi sambamba. Lakini ni ya mfuatano (sequential) kabisa.

Mstari wa kwanza unampa user_coro coroutine isiyofanya kazi. Mstari wa pili unampa orders_coro coroutine nyingine isiyofanya kazi. Wakati utekelezaji unapofika kwenye await user_coro, Python hatimaye inaanza kazi ya kwanza na kuimaliza. Ni baada ya fetch_user kumalizika pekee ndipo interpreter inafika kwenye await orders_coro na kuanza kazi ya pili. Jumla ya muda wako wa utekelezaji ni jumla ya operesheni zote mbili za I/O, si ile ndefu zaidi. Hukuwa unaziendesha kwa sambamba (parallel). Uliendesha moja baada ya nyingine kwa hatua za ziada.

Kwa Nini Hitilafu Hii Haionekani

Huu ni aina ya upungufu wa utendaji (performance regression) unaoweza kudumu kwa miezi mingi. Kodi ni Python halali. Inapita kwenye type checkers. Inarudisha matokeo sahihi. Inafanya kazi tu kwa kasi ya nusu, au mbaya zaidi. Kwa sababu hakuna stack trace wala onyo, timu za uhandisi mara nyingi hutafuta kwingine kote kwanza. Wanaongeza Redis caches, wanaboresha database tiers, au wanabadilisha maeneo ya hosting. Mhusika halisi ni kutofautiana kidogo kwa matarajio kuhusu kile await kinachofanya hasa.

Njia Tatu za Kufanya Python Ifanye Kazi kwa Sambamba (Concurrently)

Ili kurekebisha hili, lazima uambie event loop ya Python ipangilie kazi mara moja. Unahitaji kitu chenye nguvu zaidi kuliko coroutine tupu. Unahitaji Task.

1. asyncio.create_task

Tafsiri ya moja kwa moja zaidi ya mtindo wa JavaScript ni kuifunga coroutine yako kwenye Task. Task hupangwa kwenye event loop mara tu unapoiunda. Ni sawa zaidi na JavaScript Promise inayofanya kazi katika Python.

user_task = asyncio.create_task(fetch_user(id))
orders_task = asyncio.create_task(fetch_orders(id))

user = await user_task
orders = await_orders_task

Sasa fetch_user na fetch_orders zote zinaendelea kabla ya await ya kwanza. Unapofika kwenye await user_task, unazuia kazi hadi Task hiyo mahususi itakapokamilika, lakini Task nyingine inaendelea kufanya kazi. Ikiwa fetch_orders itamalizika kwanza, matokeo yake yatasubiri tu ndani ya orders_task hadi utakapoyahitaji.

Hata hivyo, kuwa mwangalifu. Ukiumba Task na usiiombe (await) kamwe, Python itatoa kosa kuhusu task iliyoharibiwa (destroyed pending task). Bado lazima ukusanye matokeo yako.

2. asyncio.gather

Ikiwa una coroutines kadhaa ambazo zote zinahitaji kumalizika kabla ya kuendelea, asyncio.gather inashughulikia kazi hiyo kwa ajili yako. Inapanga kila coroutine kama Task ndani yake na kuzisubiri kwa pamoja.

user, orders = await asyncio.gather(fetch_user(id), fetch_orders(id))

Hii ni fupi na inasomeka kwa urahisi. Inafanya vizuri sana wakati operesheni ni huru na unataka mstari mmoja unaoeleza "endesha hizi zote, kisha unipe kila matokeo." Pia inahifadhi mpangilio wa hoja (arguments) katika orodha (list) au tuple iliyorudishwa, hata kama kazi za msingi zinakamilika kwa mpangilio tofauti.

3. asyncio.TaskGroup

Python 3.11 ilianzisha TaskGroup, ambayo inaleta ushindani wa kimuundo (structured concurrency) kwenye maktaba ya kawaida (standard library). Badala ya kutengeneza kazi (tasks) kwa mkono, unatumia meneja muktadha (context manager) unaohakikisha kila kazi iliyoanzishwa inamalizika ipasavyo. Ikiwa kazi moja itatupa hitilafu (exception), nyingine zinasitishwa kiotomatiki.

async with asyncio.TaskGroup() as tg:
    user_task = tg.create_task(fetch_user(id))
    orders_task = tg.create_task(fetch_orders(id))

user = user_task.result()
orders = orders_task.result()

Mtindo huu ni bora kwa mifumo ya kazi (workflows) tata. Inaondoa hatari ya kuacha Task bila msimamizi (orphaning), na inaunganisha mzunguko wa maisha wa operesheni zinazohusiana chini ya kilele kimoja cha kimantiki. Ikiwa msimbo wako (codebase) unatumia Python 3.11 au mpya zaidi, mara nyingi huu ndio usanifu safi zaidi kwa ushindani wa fan-out (fan-out concurrency).

Mtindo wa Kifikra: await Inamaanisha "Iendeshe Hii Sasa"

Somo la msingi ni la kilugha. Katika JavaScript, unaweza kusoma await kama "wakati huo huo." Unaanzisha kazi, unafanya mambo mengine, na unakoma tu unapohitaji thamani hiyo. Katika Python, await inamaanisha "iendeshe coroutine hii hadi hatua yake inayofuata ya kusimama au kukamilika." Ikiwa coroutine bado haijapangwa (scheduled), await ndicho kinachoiandaa. Ndiyo maana huwezi kuanzisha coroutines mbili mbichi kisha uka-await baadaye. Hujampa event loop chochote cha kufanya wakati huo.

Fikiria coroutines za Python kama kazi za generator (generator functions). Kuita generator hakuiendeshi (iterate) yenyewe. Unahitaji kuipitia kwa loop, kuita next(), au kuipitisha kwa mlaji (consumer). Async inafanya kazi kwa njia hiyo hiyo. asyncio.create_task ndiye mlaji anayesema "iweke hii kwenye event loop sasa hivi." await inayofuata inasubiri tu ishara ya kukamilika.

Tabia moja madhubuti inayosaidia: kila unapoweka wito wa async function kwenye variable bila await, jiulize ikiwa umeipanga (scheduled). Ikiwa upande wa kulia haujafungwa ndani ya create_task, gather, au TaskGroup, haifanyi kazi. Ni kama mapishi tu yaliyowekwa juu ya kaunta.

Hitimisho

Mfumo wa async wa Python ni wenye nguvu, lakini unahitaji nia ya wazi. Lugha hii haianzi kazi za nyuma (background work) kwa sababu tu umeita function. Ikiwa unatoka JavaScript, kagua kila mahali unapohifadhi coroutine kwenye variable na kuisubiri (await) baadaye. Isipokuwa kama uliifanya iwe Task kwanza, umeandika msimbo wa mfuatano (sequential code) uliovalia mavazi ya async. Anza kazi kwa kutumia Task, kisha subiri matokeo. Hivyo ndivyo unavyobadilisha Python async kutoka kuwa kikwazo cha kimya kuwa chombo halisi cha ushindani (concurrency tool).