Привет, Хабр! Сегодня разберемся с одной важной темой, которая может серьезно улучшить производительность Python‑кода — параллельные вычисления с помощью Joblib.

Joblib — это Python‑библиотека, которая предоставляет инструменты для параллельных вычислений, кэширования и эффективной обработки данных. Она используется для ускорения выполнения операций, таких как многократные вычисления, обработка больших массивов данных и параллельная обработка однотипных задач.

Почему стоит использовать Joblib вместо стандартного multiprocessing?

  • Простота: Joblib значительно упрощает код и скрывает множество деталей, связанных с многозадачностью.

  • Меньше накладных расходов: для некоторых операций Joblib может быть быстрее, чем использование дефолтных механизмов Python (например, multiprocessing).

  • Меньше кода: Joblib позволяет просто распараллелить вычисления с минимальными усилиями.

Параллельные вычисления с Joblib

Начнем с самого простого — распараллеливания вычислений. Представим, что есть массив данных, и нужно применить одну и ту же функцию к каждому элементу массива.

Допустим, есть задача вычислить квадратный корень для каждого элемента из списка чисел. Без параллельных вычислений это будет работать последовательно:

from math import sqrt

data = [i for i in range(1000000)]

result = [sqrt(i ** 2) for i in data]

Это нормально, но можно ли ускорить процесс? С помощью Joblib можно распараллелить этот цикл.

Вот как это выглядит:

from joblib import Parallel, delayed
from math import sqrt

# Данные
data = [i for i in range(1000000)]

# Параллельная обработка
result = Parallel(n_jobs=4)(delayed(sqrt)(i ** 2) for i in data)
print(result[:10])  # Печатаем первые 10 результатов
  • n_jobs=4: говорим, что хотим использовать 4 ядра процессора для выполнения задачи параллельно.

  • delayed(sqrt): функция delayed просто оборачивает функцию, чтобы её можно было передавать в параллельные вычисления.

Это распараллеливает вычисление и ускоряет его.

Важно помнить, что Joblib использует множество процессов, и каждый процесс работает в своем собственном пространстве памяти. Это важно учитывать, когда у вас есть ограничения по памяти. Если задача использует много памяти, распараллеливание может привести к увеличению нагрузки на систему, а в некоторых случаях даже к её замедлению.

Использование потоков или процессов?

Мы рассмотрели использование процессов, но что, если ваша задача не связана с тяжелыми вычислениями, а, например, работает с I/O или сетевыми запросами? В таком случае использование потоков будет более подходящим, т.к Python позволяет эффективно работать с потоками в таких сценариях.

Пример с потоками:

from joblib import Parallel, delayed
import time

# Пример задачи с I/O операциями
def fetch_data(i):
    time.sleep(1)
    return i

# Параллельная обработка с потоками
result = Parallel(n_jobs=4, prefer="threads")(delayed(fetch_data)(i) for i in range(10))
print(result)

Указываем prefer="threads", чтобы использовать потоки, а не процессы. Это важно, если задача не требует интенсивных вычислений, а скорее связана с ожиданием.

Работа с большими данными и меммапы

Когда данные не помещаются в память, необходимо использовать memory‑mapped files. Это позволяет работать с данными, не загружая их целиком в память. Joblib позволяет легко работать с такими данными.

Пример использования меммапов:

import numpy as np
from joblib import Parallel, delayed
import os
import tempfile

# Создаем временную директорию для хранения данных
temp_folder = tempfile.mkdtemp()

# Создаем массив данных
large_data = np.random.rand(int(1e8))  # Массив из 100 миллионов элементов

# Сохраняем данные с использованием меммапа
filename = os.path.join(temp_folder, "large_data.mmap")
np.save(filename, large_data)

# Загружаем данные через меммап
memmap_data = np.load(filename, mmap_mode='r')

# Параллельная обработка меммапа
result = Parallel(n_jobs=4)(delayed(np.sum)(memmap_data[i:i+10000]) for i in range(0, len(memmap_data), 10000))

print(result[:5])  # Печатаем первые несколько результатов

Здесь создаём массив с огромным количеством данных и используем меммап для того, чтобы не загружать его целиком в память.

Кэширование вычислений

Теперь поговорим о кэшировании. Когда задача повторяется, зачем снова вычислять одно и то же? Joblib позволяет легко кэшировать результаты вычислений, чтобы избежать повторной работы.

Пример кэширования:

from joblib import Memory

# Создаем кэш
memory = Memory("/tmp/joblib_cache", verbose=0)

@memory.cache
def expensive_computation(x):
    print(f"Выполняется сложная операция для {x}")
    return x ** 2

# Вызываем функцию
print(expensive_computation(10))
print(expensive_computation(10))  # Будет взят из кэша

Когда вы повторно вызываете expensive_computation(10), Joblib не будет повторно вычислять результат, а вернёт его из кэша. Это позволяет существенно сэкономить время, если у вас дорогостоящие вычисления.


А более подробно с библиотекой можно ознакомиться здесь.

Статья подготовлена в рамках специализации Python Developer, на которой можно прокачать навыки разработки на Python с нуля и до middle+ уровня. Ознакомиться с полной программой, а также посмотреть записи открытых уроков можно на странице специализации.

Комментарии (0)