Сообщить об ошибке.

Прокси-объекты менеджера, модуля multiprocessing в Python

Что такое прокси-объект и как его использовать в процессах

Что такое Proxy Object?

Proxy Object (прокси-объект) - это специальный объект, который ссылается на разделяемый объект, находящийся (обычно) в другом процессе. Такой разделяемый объект называют referent (референт). Несколько прокси-объектов могут ссылаться на один и тот же референт.

Прокси-объект предоставляет методы, которые вызывают соответствующие методы референта (но не обязательно все методы референта доступны через прокси). Благодаря этому прокси-объект можно использовать почти так же, как и сам референт.

Прокси-объекты создаются менеджерами процесса multiprocessing.Manager().

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

import multiprocessing

mp_context = multiprocessing.get_context('spawn')
manager = mp_context.Manager()
l = manager.list([i*i for i in range(10)])

print(l)           
# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
print(repr(l))     
# <ListProxy object, typeid 'list' at 0x...>
print(l[4])        
# 16
print(l[2:5])      
# [4, 9, 16]
  • str(l) возвращает строковое представление референта (например, [0, 1, 4, ...]).
  • repr(l) возвращает представление самого прокси-объекта (<ListProxy object, ...>).

Передача между процессами

Прокси-объекты можно сериализовать (pickle), поэтому их можно передавать между процессами (это происходит "под капотом" при передаче между процессами). Более того, референт может содержать другие прокси-объекты, что позволяет создавать вложенные структуры:

a = manager.list()
b = manager.list()
# теперь референт a содержит референт b
a.append(b)
print(a, b)
# [<ListProxy object, ...>] []
b.append('hello')
print(a[0], b)
# ['hello'] ['hello']

Вложенные структуры

Можно создавать вложенные списки и словари, используя прокси-объекты:

l_outer = manager.list([ manager.dict() for i in range(2) ])
d_first_inner = l_outer[0]
d_first_inner['a'] = 1
d_first_inner['b'] = 2
l_outer[1]['c'] = 3
l_outer[1]['z'] = 26

print(l_outer[0])   
# {'a': 1, 'b': 2}
print(l_outer[1])   
# {'c': 3, 'z': 26}

Важно: обычные объекты внутри прокси

Если внутри референта (например, списка) лежит обычный (не прокси) изменяемый объект (например, dict), то изменения этого объекта не будут автоматически синхронизироваться между процессами. Прокси не узнает, что объект внутри изменился.

Пример:

lproxy = manager.list()
lproxy.append({})   # добавляем обычный dict

d = lproxy[0]
d['a'] = 1
d['b'] = 2

# Изменения в d не синхронизированы!
# Чтобы синхронизировать, нужно присвоить обратно:
lproxy[0] = d

Рекомендация: Для вложенных структур лучше использовать вложенные прокси-объекты (manager.list(), manager.dict()), чтобы изменения автоматически синхронизировались.

Новое в Python 3.14. Прокси-объекты для list и dict получили ранее отсутствовавшие методы

Для list-прокси:

  • clear()
  • copy()

Пример:

l = manager.list([1, 2, 3])
l.clear()
print(l)  # []
l2 = manager.list([4, 5])
l3 = l2.copy()
print(l3) # [4, 5]

Для dict-прокси:

  • fromkeys()
  • reversed(d)
  • Операции объединения: d | {}, { } | d, d |= {'b': 2}

Пример:

d = manager.dict({'a': 1, 'b': 2})
d2 = d.copy()
print(d2)  # {'a': 1, 'b': 2}

d3 = d | {'c': 3}
print(d3)  # {'a': 1, 'b': 2, 'c': 3}

d |= {'d': 4}
print(d)   # {'a': 1, 'b': 2, 'd': 4}

print(list(reversed(d)))  # ['d', 'b', 'a']

Сравнение прокси-объектов

Прокси-объекты не поддерживают сравнение по значению:

manager.list([1,2,3]) == [1,2,3]   # False

Рекомендация: Для сравнения используйте копию референта:

list(manager.list([1,2,3])) == [1,2,3]   # True

Практический пример: обмен данными между процессами

import multiprocessing

def worker(shared_list):
    for i in range(5):
        shared_list.append(i)

if __name__ == '__main__':
    with multiprocessing.Manager() as manager:
        l = manager.list()
        p = multiprocessing.Process(target=worker, args=(l,))
        p.start()
        p.join()
        print(list(l))  # [0, 1, 2, 3, 4]

pickle

Прокси-объекты и сериализация (pickle)

Прокси-объекты автоматически поддерживают сериализацию через pickle. Это значит, что когда передаётся прокси-объект между процессами (например, через очередь, Pipe, аргумент функции процесса), то Python сам сериализует (pickle) этот объект, а в другом процессе - десериализует (unpickle). То есть это происходит "под капотом" при передаче между процессами.

Передача прокси-объекта через очередь

import multiprocessing

def worker(q):
    # Получаем прокси-объект из очереди
    shared_list = q.get()
    shared_list.append('worker')
    print("В процессе worker:", list(shared_list))

if __name__ == '__main__':
    with multiprocessing.Manager() as manager:
        l = manager.list(['main'])
        q = multiprocessing.Queue()
        q.put(l)  # Прокси-объект l сериализуется (pickle) автоматически

        p = multiprocessing.Process(target=worker, args=(q,))
        p.start()
        p.join()

        print("В главном процессе:", list(l))

# В процессе worker: ['main', 'worker']
# В главном процессе: ['main', 'worker']

Пояснение: Прокси-объект l был передан через очередь в другой процесс. Python автоматически сериализовал (pickle) и десериализовал (unpickle) его. Оба процесса работают с одним и тем же референтом.

Передача прокси-объекта как аргумента процесса

import multiprocessing

def worker(shared_dict):
    shared_dict['worker'] = 42

if __name__ == '__main__':
    with multiprocessing.Manager() as manager:
        d = manager.dict()
        p = multiprocessing.Process(target=worker, args=(d,))
        p.start()
        p.join()
        print(d)  # {'worker': 42}

Пояснение: Прокси-объект d передаётся как аргумент в другой процесс. Python сам сериализует его через pickle.

Передача через Pipe

import multiprocessing

def worker(conn):
    shared_list = conn.recv()  # Получаем прокси-объект
    shared_list.append('from worker')
    conn.send(shared_list)

if __name__ == '__main__':
    with multiprocessing.Manager() as manager:
        l = manager.list(['main'])
        parent_conn, child_conn = multiprocessing.Pipe()
        p = multiprocessing.Process(target=worker, args=(child_conn,))
        p.start()
        parent_conn.send(l)  # Автоматический pickle
        l2 = parent_conn.recv()
        p.join()
        print(list(l2))  # ['main', 'from worker']