Python · Синтаксис · Просунутий рівень
Асинхронний Python
Пишіть конкурентні програми з `async def`, `await` та `asyncio`. Охоплює корутини, `asyncio.gather`, завдання, `asyncio.Queue` та асинхронні ітератори.
Короткий вступ до теми та пояснення перед вправами (вправи нижче):
Tasks, async for/with, таймаут, конкурентні HTTP-запити
#Поширені помилки, asyncio vs потоки vs multiprocessing, довідник
#Вправи:
Перша корутина з asyncio.run
#Напишіть корутину `async def greet(name: str)`, що виводить `'Привіт, <name>!'`, очікує 0.1 секунди через `asyncio.sleep`, потім виводить `'До побачення, <name>!'`. Запустіть її через `asyncio.run(greet('Аліса'))`.
import asyncio
async def greet(name: str) -> None:
pass
asyncio.run(greet('Аліса'))
# Привіт, Аліса!
# До побачення, Аліса!
Рішення
import asyncio
async def greet(name: str) -> None:
print(f'Привіт, {name}!')
await asyncio.sleep(0.1)
print(f'До побачення, {name}!')
asyncio.run(greet('Аліса'))
# Привіт, Аліса!
# До побачення, Аліса!
Конкурентні завдання з asyncio.gather
#Напишіть корутину `fetch(url: str, delay: float) -> str`, що симулює HTTP-запит: чекає `delay` секунд і повертає `f'Response from {url}'`. Використайте `asyncio.gather` для запуску трьох fetch конкурентно: `fetch('A', 0.3)`, `fetch('B', 0.1)`, `fetch('C', 0.2)`. Виведіть всі результати. Загальний час — близько 0.3с, а не 0.6с.
import asyncio
async def fetch(url: str, delay: float) -> str:
pass
async def main() -> None:
results = await asyncio.gather(
fetch('A', 0.3),
fetch('B', 0.1),
fetch('C', 0.2),
)
for r in results:
print(r)
asyncio.run(main())
Рішення
import asyncio
async def fetch(url: str, delay: float) -> str:
await asyncio.sleep(delay)
return f'Response from {url}'
async def main() -> None:
results = await asyncio.gather(
fetch('A', 0.3),
fetch('B', 0.1),
fetch('C', 0.2),
)
for r in results:
print(r)
asyncio.run(main())
# Response from A
# Response from B
# Response from C
Фонові завдання з create_task
#Напишіть корутину `main()`, що створює два фонові завдання через `asyncio.create_task`: одне відраховує від 3 до 1 (виводить кожне число з затримкою 0.1с), інше виводить `'Працюємо...'` тричі із затримкою 0.15с. Дочекайтесь обох завдань наприкінці. Зверніть увагу, що виводи переплітаються.
import asyncio
async def countdown() -> None:
for i in range(3, 0, -1):
print(f'Зворотній відлік: {i}')
await asyncio.sleep(0.1)
async def worker() -> None:
for _ in range(3):
print('Працюємо...')
await asyncio.sleep(0.15)
async def main() -> None:
# створіть завдання та дочекайтесь їх
pass
asyncio.run(main())
Рішення
import asyncio
async def countdown() -> None:
for i in range(3, 0, -1):
print(f'Зворотній відлік: {i}')
await asyncio.sleep(0.1)
async def worker() -> None:
for _ in range(3):
print('Працюємо...')
await asyncio.sleep(0.15)
async def main() -> None:
task1 = asyncio.create_task(countdown())
task2 = asyncio.create_task(worker())
await task1
await task2
asyncio.run(main())
Виробник-споживач з asyncio.Queue
#Реалізуйте патерн виробник-споживач з `asyncio.Queue`. Корутина `producer` кладе числа 1–5 в чергу (затримка 0.05с між числами), потім кладе `None` як сигнал завершення. Корутина `consumer` читає елементи з черги до появи `None`, виводячи `'Consuming: <n>'` для кожного числа.
import asyncio
async def producer(queue: asyncio.Queue) -> None:
pass
async def consumer(queue: asyncio.Queue) -> None:
pass
async def main() -> None:
queue: asyncio.Queue = asyncio.Queue()
await asyncio.gather(producer(queue), consumer(queue))
asyncio.run(main())
Рішення
import asyncio
async def producer(queue: asyncio.Queue) -> None:
for i in range(1, 6):
await queue.put(i)
await asyncio.sleep(0.05)
await queue.put(None) # сигнал завершення
async def consumer(queue: asyncio.Queue) -> None:
while True:
item = await queue.get()
if item is None:
break
print(f'Consuming: {item}')
async def main() -> None:
queue: asyncio.Queue = asyncio.Queue()
await asyncio.gather(producer(queue), consumer(queue))
asyncio.run(main())
Таймаут з asyncio.wait_for
#Напишіть корутину `slow_op(seconds: float) -> str`, що чекає `seconds` секунд та повертає `'done'`. Використайте `asyncio.wait_for(slow_op(5), timeout=0.2)` для спроби операції з таймаутом 0.2 секунди. Перехопіть `asyncio.TimeoutError` та виведіть `'Operation timed out!'`.
import asyncio
async def slow_op(seconds: float) -> str:
await asyncio.sleep(seconds)
return 'done'
async def main() -> None:
try:
result = await asyncio.wait_for(slow_op(5), timeout=0.2)
print(result)
except asyncio.TimeoutError:
pass # виведіть повідомлення тут
asyncio.run(main())
# Operation timed out!
Рішення
import asyncio
async def slow_op(seconds: float) -> str:
await asyncio.sleep(seconds)
return 'done'
async def main() -> None:
try:
result = await asyncio.wait_for(slow_op(5), timeout=0.2)
print(result)
except asyncio.TimeoutError:
print('Operation timed out!')
asyncio.run(main())
# Operation timed out!
Асинхронний генератор
#Напишіть асинхронний генератор `async_range(start: int, stop: int, delay: float)`, що видає цілі числа від `start` до `stop-1`, роблячи паузу `delay` секунд між кожним значенням. Використайте `async for` для споживання та виводу значень.
import asyncio
async def async_range(start: int, stop: int, delay: float):
pass
async def main() -> None:
async for value in async_range(0, 4, 0.05):
print(value)
asyncio.run(main())
# 0
# 1
# 2
# 3
Рішення
import asyncio
async def async_range(start: int, stop: int, delay: float):
for i in range(start, stop):
yield i
await asyncio.sleep(delay)
async def main() -> None:
async for value in async_range(0, 4, 0.05):
print(value)
asyncio.run(main())
Асинхронний контекстний менеджер
#Створіть клас `AsyncTimer`, що працює як асинхронний контекстний менеджер. `__aenter__` фіксує час початку та виводить `'Timer started'`. `__aexit__` обчислює час виконання та виводить `f'Elapsed: {elapsed:.2f}s'`. Використайте з `async with AsyncTimer():`.
import asyncio
import time
class AsyncTimer:
async def __aenter__(self):
pass
async def __aexit__(self, *args):
pass
async def main() -> None:
async with AsyncTimer():
await asyncio.sleep(0.1)
asyncio.run(main())
# Timer started
# Elapsed: 0.10s
Рішення
import asyncio
import time
class AsyncTimer:
async def __aenter__(self):
self._start = time.perf_counter()
print('Timer started')
return self
async def __aexit__(self, *args):
elapsed = time.perf_counter() - self._start
print(f'Elapsed: {elapsed:.2f}s')
async def main() -> None:
async with AsyncTimer():
await asyncio.sleep(0.1)
asyncio.run(main())
Обмеження конкурентності з asyncio.Semaphore
#У вас є 8 завдань, але одночасно має виконуватися не більше 3. Використайте `asyncio.Semaphore(3)` для обмеження конкурентності. Кожне завдання виводить `'Task N started'`, чекає 0.1с, потім `'Task N done'`. Запустіть всі 8 завдань через `asyncio.gather`.
import asyncio
async def task(n: int, sem: asyncio.Semaphore) -> None:
async with sem:
print(f'Task {n} started')
await asyncio.sleep(0.1)
print(f'Task {n} done')
async def main() -> None:
sem = asyncio.Semaphore(3)
# створіть і запустіть 8 завдань (n від 1 до 8)
pass
asyncio.run(main())
Рішення
import asyncio
async def task(n: int, sem: asyncio.Semaphore) -> None:
async with sem:
print(f'Task {n} started')
await asyncio.sleep(0.1)
print(f'Task {n} done')
async def main() -> None:
sem = asyncio.Semaphore(3)
await asyncio.gather(*(task(n, sem) for n in range(1, 9)))
asyncio.run(main())
Виконання блокуючого коду через run_in_executor
#Функція `blocking_sum(n)` обчислює суму 1..n через цикл (симулює повільну роботу). Оберніть її в асинхронну `async_sum(n)`, використовуючи `loop.run_in_executor(None, blocking_sum, n)`, щоб не блокувати цикл подій. Запустіть два виклики `async_sum` конкурентно через `asyncio.gather`.
import asyncio
def blocking_sum(n: int) -> int:
return sum(range(n + 1))
async def async_sum(n: int) -> int:
loop = asyncio.get_running_loop()
# запустіть blocking_sum у пулі потоків
pass
async def main() -> None:
results = await asyncio.gather(async_sum(1000), async_sum(2000))
print(results) # [500500, 2001000]
asyncio.run(main())
Рішення
import asyncio
def blocking_sum(n: int) -> int:
return sum(range(n + 1))
async def async_sum(n: int) -> int:
loop = asyncio.get_running_loop()
return await loop.run_in_executor(None, blocking_sum, n)
async def main() -> None:
results = await asyncio.gather(async_sum(1000), async_sum(2000))
print(results) # [500500, 2001000]
asyncio.run(main())
Координація з asyncio.Event
#Використайте `asyncio.Event` для координації двох корутин. Корутина `setter` чекає 0.15с, виводить `'Setting event'`, потім встановлює подію. Корутина `waiter` виводить `'Waiting for event...'`, чекає через `await event.wait()`, потім виводить `'Event received!'`. Запустіть обидві через `asyncio.gather`.
import asyncio
async def setter(event: asyncio.Event) -> None:
await asyncio.sleep(0.15)
print('Setting event')
event.set()
async def waiter(event: asyncio.Event) -> None:
pass # зачекайте подію, потім виведіть 'Event received!'
async def main() -> None:
event = asyncio.Event()
await asyncio.gather(setter(event), waiter(event))
asyncio.run(main())
Рішення
import asyncio
async def setter(event: asyncio.Event) -> None:
await asyncio.sleep(0.15)
print('Setting event')
event.set()
async def waiter(event: asyncio.Event) -> None:
print('Waiting for event...')
await event.wait()
print('Event received!')
async def main() -> None:
event = asyncio.Event()
await asyncio.gather(setter(event), waiter(event))
asyncio.run(main())