Как выполнять асинхронные файловые операции в Python с использованием asyncio

Ответ

Стандартные файловые операции в Python (open, read, write) являются блокирующими. Их прямой вызов в asyncio коде заморозит весь цикл событий (event loop), сводя на нет преимущества асинхронности.

Для корректной асинхронной работы с файлами существуют два основных подхода:

1. Использование библиотеки aiofiles

Это рекомендуемый и идиоматичный способ. Библиотека предоставляет асинхронный API, который полностью совместим с синтаксисом async/await и не блокирует event loop.

Пример:

import asyncio
import aiofiles

async def main():
    async with aiofiles.open('example.txt', mode='w') as f:
        await f.write('Hello, aiofiles!')

    async with aiofiles.open('example.txt', mode='r') as f:
        content = await f.read()
        print(content)

asyncio.run(main())

2. Использование loop.run_in_executor

Этот метод позволяет выполнить любую блокирующую (синхронную) функцию в отдельном потоке из пула потоков, не блокируя основной цикл событий. Это универсальный способ для интеграции любого синхронного кода в асинхронный.

Пример:

import asyncio

async def read_file_in_executor():
    loop = asyncio.get_running_loop()

    # Открываем файл синхронно
    with open('example.txt', 'r') as f:
        # Выполняем блокирующую операцию f.read() в пуле потоков
        content = await loop.run_in_executor(None, f.read)
        print(content)

# Для записи файла нужно обернуть в функцию
def write_sync(content):
    with open('example.txt', 'w') as f:
        f.write(content)

async def main():
    loop = asyncio.get_running_loop()
    await loop.run_in_executor(None, write_sync, 'Hello, executor!')
    await read_file_in_executor()

asyncio.run(main())

Рекомендация: Всегда отдавайте предпочтение aiofiles для работы с файлами в asyncio, так как это более чистый, читаемый и оптимизированный подход. run_in_executor является более общим, но и более громоздким решением для любых блокирующих задач.

Ответ 18+ 🔞

А, слушай, смотри, вот эта вся история с файлами в асинхронном коде — это просто пиздец, если подходить к ней с дубиной. Ты же не хочешь, чтобы твоё приложение встало колом, как вкопанное, пока какой-нибудь файл на 10 гигов читается? Ну вот и я о том же.

Стандартные штуки вроде open() или read() — они же, блядь, блокирующие. Это как если бы ты в очереди за хлебом встал и заснул, а все за тобой стоят и ждут, пока ты проснёшься и купишь свою булку. Весь твой красивый asyncio event loop на это время просто замирает, и вся асинхронность идёт лесом. Пиздец, да?

Но не отчаивайся, есть два проверенных способа не облажаться.

1. Библиотека aiofiles — твой новый лучший друг

Это, блядь, просто конфетка. Она делает ровно то, что тебе нужно, но асинхронно. Всё по канону: async with, await, никаких блокировок. Чисто, красиво, работает.

Смотри, как это выглядит:

import asyncio
import aiofiles

async def main():
    # Пишем асинхронно, не блокируя всех вокруг
    async with aiofiles.open('example.txt', mode='w') as f:
        await f.write('Hello, aiofiles!')

    # Читаем тоже асинхронно
    async with aiofiles.open('example.txt', mode='r') as f:
        content = await f.read()
        print(content)  # Выведет наше сообщение

asyncio.run(main())

Вот видишь? Никакого геморроя. Всё как в обычном коде, только с магическими словами async и await. Рекомендую, ёпта.

2. loop.run_in_executor — универсальная отмычка

А это, можно сказать, тяжёлая артиллерия. Если вдруг aiofiles не подходит или нужно впихнуть какую-то другую, левую, блокирующую хрень в асинхронный контекст — вот он, твой выход. Суть в том, что ты скидываешь эту тяжёлую работу в отдельный поток, а сам цикл событий продолжает крутиться, как ни в чём не бывало.

Вот, смотри пример:

import asyncio

async def read_file_in_executor():
    loop = asyncio.get_running_loop()

    # Открываем файл по-старинке, синхронно
    with open('example.txt', 'r') as f:
        # А вот само чтение — выкидываем в пул потоков!
        content = await loop.run_in_executor(None, f.read)
        print(content)

# Для записи придётся обернуть в обычную функцию
def write_sync(content):
    with open('example.txt', 'w') as f:
        f.write(content)

async def main():
    loop = asyncio.get_running_loop()
    # Запись тоже через executor
    await loop.run_in_executor(None, write_sync, 'Hello, executor!')
    await read_file_in_executor()

asyncio.run(main())

Метод мощный, но, честно говоря, более громоздкий. Как будто вместо отвёртки взял разводной ключ на всё случаи жизни.

Итог, блядь: Если работаешь именно с файлами — бери aiofiles и не парься. Это идиоматично и правильно. run_in_executor оставь для каких-то других, особо извращённых блокирующих задач, которых ну совсем дохуя. Выбор за тобой, но головой думай!