Sunday 13 February 2022

async for vs for await

There's an important difference between how the async iteration constructs work in JavaScript (for await) and Python (async for), the ability (or lack of) to use it also to iterate a synchronous iterable/iterator.

Based on the idea that I mentioned in my previous post, the sloppy promise semantics, in JavaScript a for await loop can iterate both an asynchronous and a synchronous iterable. This is pretty coherent with the fact that we can also await for a non Promise value. The Iteration Protocols dictate that an Iterable object must have a method available with the Symbol.iterator key, that returns an Iterator object. This Iterator object must have a next method. For an Async Iterable we must have a Symbol.asyncIterator method, and the returned Async Iterator must have also a next method that returns a Promise.

A for await tries first to obtain an Async iterable checking if the object has a Symbol.asyncIterable, if that's not the case, it'll try to obtain a normal Iterator checking if the object has a Symbol.iterator. Then, as both iterables and asyncIterables have a next method, and as we know awaiting for a non awaitable value is perfectly fine, the Polymorphic behaviour is all set. Beautiful!.


let cities = ["Toulouse", "Lyon", "Xixon"];

async function* asyncCities(){
		for (let city of cities){
			yield await new Promise(res => setTimeout(() => res(city), 700));
		}
}


async function print(items){
	for await (let it of items){
		console.log(it.toUpperCase());
	}
}

(async () => {
	await print(cities);
	console.log("------");
	await print(asyncCities());
})();


That's not the case in Python. An async for will try to obtain an async iterator through the aiter() function (that invokes the __aiter__ method in the object). If the object lacks that __aiter__ method, we get a:
#TypeError: 'async for' requires an object with an __aiter__ method.
No attempt is done to invoke iter/__iter__. So writing code that supports both sync and async iterables is not so elegant:



import asyncio
import time

async def getCitiesAsync():
    print('getCitiesAsync')
    await asyncio.sleep(0.5)
    yield "Xixon"
    await asyncio.sleep(0.5)
    yield "Toulouse"
    await asyncio.sleep(0.5)
    yield "Paris"

def getCities():
    print('getCities')
    yield "Xixon"
    yield "Toulouse"
    yield "Paris"



async def printCities(cities):
    if hasattr(cities, "__aiter__"):
        async for city in cities:
            print(city)
    else:
        for city in cities:
            print(city)


async def main():
    await printCities(getCitiesAsync())



No comments:

Post a Comment