bookworm-smart-assistant/skills/python-pro/references/async-patterns.md

357 lines
9.5 KiB
Markdown
Raw Normal View History

# Async Programming Patterns
## Basic Async/Await
```python
import asyncio
from collections.abc import Coroutine
# Basic async function
async def fetch_data(url: str) -> dict[str, str]:
await asyncio.sleep(1) # Simulate I/O
return {"url": url, "status": "ok"}
# Running async code
async def main() -> None:
result = await fetch_data("https://api.example.com")
print(result)
if __name__ == "__main__":
asyncio.run(main())
# Multiple concurrent operations
async def fetch_all(urls: list[str]) -> list[dict[str, str]]:
tasks = [fetch_data(url) for url in urls]
return await asyncio.gather(*tasks)
# Error handling with gather
async def safe_fetch_all(urls: list[str]) -> list[dict[str, str] | None]:
tasks = [fetch_data(url) for url in urls]
results = await asyncio.gather(*tasks, return_exceptions=True)
return [r if not isinstance(r, Exception) else None for r in results]
```
## Task Groups (Python 3.11+)
```python
from asyncio import TaskGroup
# Task groups for structured concurrency
async def process_batch(items: list[int]) -> list[int]:
results: list[int] = []
async with TaskGroup() as tg:
tasks = [tg.create_task(process_item(item)) for item in items]
# All tasks complete before this line
return [task.result() for task in tasks]
# Error handling with TaskGroup
async def robust_processing(items: list[str]) -> tuple[list[str], list[Exception]]:
results: list[str] = []
errors: list[Exception] = []
try:
async with TaskGroup() as tg:
for item in items:
tg.create_task(process_item_safe(item))
except ExceptionGroup as eg:
for exc in eg.exceptions:
errors.append(exc)
return results, errors
```
## Async Context Managers
```python
from typing import Self
from collections.abc import AsyncIterator
class AsyncDatabaseConnection:
def __init__(self, url: str) -> None:
self.url = url
self._conn: Connection | None = None
async def __aenter__(self) -> Self:
self._conn = await connect(self.url)
return self
async def __aexit__(
self,
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: Any,
) -> None:
if self._conn:
await self._conn.close()
async def query(self, sql: str) -> list[dict[str, Any]]:
if not self._conn:
raise RuntimeError("Not connected")
return await self._conn.execute(sql)
# Usage
async def get_users() -> list[dict[str, Any]]:
async with AsyncDatabaseConnection("postgresql://...") as db:
return await db.query("SELECT * FROM users")
# Async context manager with contextlib
from contextlib import asynccontextmanager
@asynccontextmanager
async def get_db_session() -> AsyncIterator[Session]:
session = await create_session()
try:
yield session
await session.commit()
except Exception:
await session.rollback()
raise
finally:
await session.close()
```
## Async Generators
```python
from collections.abc import AsyncIterator
# Async generator for streaming data
async def read_lines(filepath: str) -> AsyncIterator[str]:
async with aiofiles.open(filepath) as f:
async for line in f:
yield line.strip()
# Process stream
async def process_file(filepath: str) -> int:
count = 0
async for line in read_lines(filepath):
await process_line(line)
count += 1
return count
# Async generator with cleanup
async def fetch_paginated(url: str) -> AsyncIterator[dict[str, Any]]:
page = 1
session = await create_session()
try:
while True:
data = await session.get(f"{url}?page={page}")
if not data:
break
yield data
page += 1
finally:
await session.close()
```
## Async Comprehensions
```python
# Async list comprehension
async def fetch_all_users(user_ids: list[int]) -> list[User]:
return [user async for user in fetch_users(user_ids)]
# Async dict comprehension
async def build_user_map(user_ids: list[int]) -> dict[int, User]:
return {
user.id: user
async for user in fetch_users(user_ids)
}
# Conditional async comprehension
async def get_active_users(user_ids: list[int]) -> list[User]:
return [
user
async for user in fetch_users(user_ids)
if user.is_active
]
```
## Synchronization Primitives
```python
import asyncio
# Lock for critical sections
class SharedResource:
def __init__(self) -> None:
self._lock = asyncio.Lock()
self._data: dict[str, Any] = {}
async def update(self, key: str, value: Any) -> None:
async with self._lock:
# Critical section
current = self._data.get(key, 0)
await asyncio.sleep(0.1) # Simulate processing
self._data[key] = current + value
# Semaphore for rate limiting
class RateLimiter:
def __init__(self, max_concurrent: int) -> None:
self._semaphore = asyncio.Semaphore(max_concurrent)
async def process(self, item: str) -> str:
async with self._semaphore:
return await expensive_operation(item)
# Event for coordination
class AsyncWorker:
def __init__(self) -> None:
self._ready = asyncio.Event()
self._shutdown = asyncio.Event()
async def start(self) -> None:
# Initialization
await self._initialize()
self._ready.set()
# Wait for shutdown
await self._shutdown.wait()
async def wait_ready(self) -> None:
await self._ready.wait()
def stop(self) -> None:
self._shutdown.set()
```
## Async Queue Patterns
```python
from asyncio import Queue
# Producer-consumer pattern
async def producer(queue: Queue[int], n: int) -> None:
for i in range(n):
await queue.put(i)
await asyncio.sleep(0.1)
async def consumer(queue: Queue[int], name: str) -> None:
while True:
item = await queue.get()
try:
await process_item(item)
finally:
queue.task_done()
async def run_pipeline(num_items: int, num_workers: int) -> None:
queue: Queue[int] = Queue(maxsize=10)
# Start producer and consumers
async with TaskGroup() as tg:
tg.create_task(producer(queue, num_items))
for i in range(num_workers):
tg.create_task(consumer(queue, f"worker-{i}"))
# Wait for all items to be processed
await queue.join()
```
## Async Timeouts
```python
# Timeout for single operation
async def fetch_with_timeout(url: str, timeout: float) -> dict[str, Any]:
try:
async with asyncio.timeout(timeout):
return await fetch_data(url)
except TimeoutError:
return {"error": "timeout"}
# Timeout for multiple operations
async def fetch_all_with_timeout(
urls: list[str],
timeout: float
) -> list[dict[str, Any] | None]:
try:
async with asyncio.timeout(timeout):
return await fetch_all(urls)
except TimeoutError:
return [None] * len(urls)
```
## Background Tasks
```python
from asyncio import create_task, Task
class BackgroundTaskManager:
def __init__(self) -> None:
self._tasks: set[Task[None]] = set()
def create_task(self, coro: Coroutine[None, None, None]) -> Task[None]:
task = create_task(coro)
self._tasks.add(task)
task.add_done_callback(self._tasks.discard)
return task
async def shutdown(self) -> None:
# Cancel all background tasks
for task in self._tasks:
task.cancel()
# Wait for cancellation
await asyncio.gather(*self._tasks, return_exceptions=True)
# Usage
manager = BackgroundTaskManager()
manager.create_task(background_job())
```
## Async Iteration Protocol
```python
class AsyncRange:
def __init__(self, start: int, end: int) -> None:
self.start = start
self.end = end
self.current = start
def __aiter__(self) -> Self:
return self
async def __anext__(self) -> int:
if self.current >= self.end:
raise StopAsyncIteration
await asyncio.sleep(0.1) # Simulate async work
value = self.current
self.current += 1
return value
# Usage
async for i in AsyncRange(0, 5):
print(i)
```
## Mixing Sync and Async
```python
from concurrent.futures import ThreadPoolExecutor
import functools
# Run sync code in executor
async def run_in_executor(func: Callable[..., T], *args: Any) -> T:
loop = asyncio.get_running_loop()
return await loop.run_in_executor(None, func, *args)
# Run async code from sync context
def sync_wrapper(coro: Coroutine[None, None, T]) -> T:
loop = asyncio.new_event_loop()
try:
return loop.run_until_complete(coro)
finally:
loop.close()
# Async wrapper for sync function
def to_async(func: Callable[..., T]) -> Callable[..., Coroutine[None, None, T]]:
@functools.wraps(func)
async def wrapper(*args: Any, **kwargs: Any) -> T:
loop = asyncio.get_running_loop()
return await loop.run_in_executor(
None,
functools.partial(func, *args, **kwargs)
)
return wrapper
```