Обходной путь для использования __name__=='__main__' в многопроцессорной обработке Python

Как мы все знаем, нам нужно защищать main() при запуске кода с multiprocessing в Python с использованием if __name__ == '__main__'.

Я понимаю, что это необходимо в некоторых случаях, чтобы дать доступ к функциям, определенным в основном, но я не понимаю, зачем это нужно в этом случае:

file2.py

import numpy as np
from multiprocessing import Pool
class Something(object):
    def get_image(self):
        return np.random.rand(64,64)

    def mp(self):
        image = self.get_image()
        p = Pool(2)
        res1 = p.apply_async(np.sum, (image,))
        res2 = p.apply_async(np.mean, (image,))
        print(res1.get())
        print(res2.get())
        p.close()
        p.join()

main.py

from file2 import Something
s = Something()
s.mp()

Все функции или импорты, необходимые для работы Something, являются частью file2.py. Почему подпроцессу необходимо повторно запустить main.py?

Я думаю, что решение __name__ не очень хорошее, так как это мешает мне распространять код file2.py, так как я не могу убедиться, что они защищают свою основную часть. Нет ли обходного пути для Windows? Как пакеты решают это (поскольку я никогда не сталкивался с проблемой, не защищая мою основную часть каким-либо пакетом - они просто не используют многопроцессорность?)

изменить: я знаю, что это из-за того, что fork() не реализовано в Windows. Я просто спросил, есть ли хак, позволяющий интерпретатору начинать с file2.py вместо main.py, поскольку я могу быть уверен, что file2.py самодостаточен.


person skjerns    schedule 14.07.2017    source источник
comment
Взлом if __name__ == '__main__' необходим только в Windows, поскольку на этой платформе нет fork(). Если вы выберете любую другую операционную систему, она вам не понадобится.   -  person Sven Marnach    schedule 14.07.2017
comment
ОП, просто чтобы подтвердить, ты работаешь в Windows, верно?   -  person cs95    schedule 14.07.2017
comment
Если я вас правильно понял, вы пишете file2.py как библиотеку и хотите поддерживать пользовательский код, такой как main.py (который может быть написан кем-то другим в будущем). К сожалению, я не думаю, что есть какой-либо способ защитить ваших пользователей от требований multiprocessing. Возможно, вам просто нужно задокументировать, что ваш модуль требует, чтобы код скрипта был помещен в if __name__ == "__main__" блоков, чтобы ничего не запускалось, если модуль импортируется.   -  person Blckknght    schedule 15.07.2017
comment
@Blckknght Спасибо, это был именно тот ответ, который я искал! (Хотя это не тот ответ, на который я надеялся;))   -  person skjerns    schedule 18.07.2017


Ответы (4)


Основной модуль импортируется (но с __name__ != '__main__', потому что Windows пытается имитировать поведение, подобное разветвлению, в системе, в которой нет разветвления). multiprocessing не может знать, что вы не сделали ничего важного в своем основном модуле, поэтому импорт выполняется «на всякий случай», чтобы создать среду, подобную той, что находится в вашем основном процессе. Если бы он этого не сделал, все виды вещей, которые происходят из-за побочных эффектов в основном (например, импорт, вызовы конфигурации с постоянными побочными эффектами и т. д.), могли бы не выполняться должным образом в дочерних процессах.

Таким образом, если они не защищают свои __main__, код не является безопасным для многопроцессорной обработки (а также безопасным для юнит-тестов, безопасным импортом и т. д.). Защитная оболочка if __name__ == '__main__': должна быть частью всех правильных основных модулей. Продолжайте распространять его с примечанием о необходимости многопроцессорной защиты основного модуля.

person ShadowRanger    schedule 14.07.2017

При использовании метода запуска «spawn» новые процессы представляют собой интерпретаторы Python, которые запускаются с нуля. Новые интерпретаторы Python в подпроцессах не могут выяснить, какие модули нужно импортировать, поэтому они снова импортируют основной модуль, который, в свою очередь, импортирует все остальное. Это означает, что должна быть возможность импортировать основной модуль без каких-либо побочных эффектов.

Если вы работаете на платформе, отличной от Windows, вы можете вместо этого использовать метод запуска «fork», и у вас не будет этой проблемы.

Тем не менее, что плохого в использовании if __name__ == "__main__":? Он имеет много дополнительных преимуществ, например. инструменты документации смогут обработать ваш основной модуль, а модульное тестирование будет проще и т. д., поэтому вы должны использовать его в любом случае.

person Sven Marnach    schedule 14.07.2017

if __name__ == '__main__' необходим в Windows, поскольку в Windows нет опции «форк» для процессов.

В linux, например, вы можете fork процесс, поэтому родительский процесс будет скопирован, а копия станет дочерним процессом (и у нее будет доступ к уже импортированному коду, который вы загрузили в родительском процессе)

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

поэтому даже в вашем примере main.py будет снова импортирован (поскольку все файлы импортируются снова). python не может угадать, какой конкретный скрипт python должен импортировать дочерний процесс.

К вашему сведению, есть и другие ограничения, о которых вы должны знать, например, использование глобальных переменных, вы можете прочитать об этом здесь https://docs.python.org/2/library/multiprocessing.html#windows

person DorElias    schedule 14.07.2017

Как уже упоминалось, метод spawn() в Windows повторно импортирует код для каждого экземпляра интерпретатора. Этот импорт снова выполнит ваш код в дочернем процессе (и это заставит его создать собственный дочерний процесс и т. д.).

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

Я передаю переменные в сценарий, выбирая их во временном каталоге, и я передаю временный каталог в подпроцесс с помощью argparse.

Затем я сохраняю результаты во временный каталог, откуда их извлекает основной скрипт.

Вот пример функции file_hasher(), которую я написал:

main_program.py

import os, pickle, shutil, subprocess, sys, tempfile

def file_hasher(filenames):
    try:
        subprocess_directory = tempfile.mkdtemp()
        input_arguments_file = os.path.join(subprocess_directory, 'input_arguments.dat')
        with open(input_arguments_file, 'wb') as func_inputs:
            pickle.dump(filenames, func_inputs)
        current_path = os.path.dirname(os.path.realpath(__file__))
        file_hasher = os.path.join(current_path, 'file_hasher.py')
        python_interpreter = sys.executable
        proc = subprocess.call([python_interpreter, file_hasher, subprocess_directory],
                               timeout=60, 
                              )
        output_file = os.path.join(subprocess_directory, 'function_outputs.dat')
        with open(output_file, 'rb') as func_outputs:
            hashlist = pickle.load(func_outputs)
    finally:
        shutil.rmtree(subprocess_directory)
    return hashlist

file_hasher.py

#! /usr/bin/env python
import argparse, hashlib, os, pickle
from multiprocessing import Pool

def file_hasher(input_file):
    with open(input_file, 'rb') as f:
        data = f.read()
        md5_hash = hashlib.md5(data)
    hashval = md5_hash.hexdigest()
    return hashval

if __name__=='__main__':
    argument_parser = argparse.ArgumentParser()
    argument_parser.add_argument('subprocess_directory', type=str)
    subprocess_directory = argument_parser.parse_args().subprocess_directory

    arguments_file = os.path.join(subprocess_directory, 'input_arguments.dat')
    with open(arguments_file, 'rb') as func_inputs:
        filenames = pickle.load(func_inputs)

    hashlist = []
    p = Pool()
    for r in p.imap(file_hasher, filenames):
        hashlist.append(r)

    output_file = os.path.join(subprocess_directory, 'function_outputs.dat')
    with open(output_file, 'wb') as func_outputs:
        pickle.dump(hashlist, func_outputs)

Должен быть лучший способ...

person Chris Hubley    schedule 07.12.2018
comment
Без проблем. У меня была такая же проблема, и это лучшее, что я мог придумать. Это некрасиво, но позволяет запускать многопроцессорный скрипт из модуля на нескольких платформах. Однако есть некоторые накладные расходы, поэтому, если вы знали, что ваш код никогда не будет работать в Windows, я думаю, что лучше просто пренебречь защитой if name__=='__main'. - person Chris Hubley; 12.12.2018