Python 3:在多处理过程中捕获警告

时间:2012-09-29 16:09:30

标签: python python-3.x warnings multiprocessing

太久了;没看过

warnings.catch_warnings()上下文管理器是not thread safe。如何在并行处理环境中使用它?

背景

下面的代码使用Python multiprocessing模块的并行处理解决了最大化问题。它需要一个(不可变的)小部件列表,对它们进行分区(参见Efficient multiprocessing of massive, brute force maximization in Python 3),找到所有分区的最大值(“决赛者”),然后找到那些“决赛者”的最大值(“冠军”)。 “如果我正确理解自己的代码(如果我这样做,我就不会在这里),我将与所有子进程共享内存以为其提供输入小部件,multiprocessing使用操作系统级别管道和酸洗,以便在工人完成后将决赛小工具发送回主要流程。

问题的根源

我希望捕获多数小部件警告,这些小部件警告是在小部件从进程间管道出来时发生的小部件重新实例化后发生的。当窗口小部件对象实例化时,它们会验证自己的数据,从Python标准warnings模块发出警告,告诉应用程序用户该窗口小部件怀疑用户的输入数据存在问题。因为unpickling导致对象实例化,所以我对代码的理解意味着每个widget对象只重新实例化一次,当且仅当它从管道出来后才是决赛者 - 请参阅下一节以了解为什么这不正确

这些小部件在被欺骗之前就已经创建了,所以用户已经痛苦地意识到他输入了什么输入并且不想再听到它。这些是我想要通过warnings模块的catch_warnings()上下文管理器(即with语句)捕获的警告。

解决方案失败

在我的测试中,当多余警告被发送到我在下面标记为 A行 B行之间的任何地方时,我缩小了范围。让我感到惊讶的是,警告是在output_queue.get()附近以外的地方发出的。这对我来说意味着multiprocessing使用酸洗将小部件发送给工人。

结果是将warnings.catch_warnings()创建的上下文管理器甚至围绕从 A行 B行的所有内容,并在此上下文中设置正确的警告过滤器没有发现警告。这意味着警告正在工作进程中发出。将此上下文管理器放在工作器代码周围也不会捕获警告。

代码

此示例省略了用于确定问题大小是否太小而无法解决分叉进程,导入多处理以及定义my_frobnal_countermy_load_balancer的代码。

"Call `frobnicate(list_of_widgets)` to get the widget with the most frobnals"

def frobnicate_parallel_worker(widgets, output_queue):
    resultant_widget = max(widgets, key=my_frobnal_counter)
    output_queue.put(resultant_widget)

def frobnicate_parallel(widgets):
    output_queue = multiprocessing.Queue()
    # partitions: Generator yielding tuples of sets
    partitions = my_load_balancer(widgets)
    processes = []
    # Line A: Possible start of where the warnings are coming from.
    for partition in partitions:
        p = multiprocessing.Process(
                 target=frobnicate_parallel_worker,
                 args=(partition, output_queue))
        processes.append(p)
        p.start()
    finalists = []
    for p in processes:
        finalists.append(output_queue.get())
    # Avoid deadlocks in Unix by draining queue before joining processes
    for p in processes:
        p.join()
    # Line B: Warnings no longer possible after here.
    return max(finalists, key=my_frobnal_counter)

3 个答案:

答案 0 :(得分:2)

您可以尝试覆盖Process.run方法以使用warnings.catch_warnings

>>> from multiprocessing import Process
>>> 
>>> def yell(text):
...    import warnings
...    print 'about to yell %s' % text
...    warnings.warn(text)
... 
>>> class CustomProcess(Process):
...    def run(self, *args, **kwargs):
...       import warnings
...       with warnings.catch_warnings():
...          warnings.simplefilter("ignore")
...          return Process.run(self, *args, **kwargs)
... 
>>> if __name__ == '__main__':
...    quiet = CustomProcess(target=yell, args=('...not!',))
...    quiet.start()
...    quiet.join()
...    noisy = Process(target=yell, args=('AAAAAAaaa!',))
...    noisy.start()
...    noisy.join()
... 
about to yell ...not!
about to yell AAAAAAaaa!
__main__:4: UserWarning: AAAAAAaaa!
>>> 

或者你可以使用一些内部......(__warningregistry__

>>> from multiprocessing import Process
>>> import exceptions
>>> def yell(text):
...    import warnings
...    print 'about to yell %s' % text
...    warnings.warn(text)
...    # not filtered
...    warnings.warn('complimentary second warning.')
... 
>>> WARNING_TEXT = 'AAAAaaaaa!'
>>> WARNING_TYPE = exceptions.UserWarning
>>> WARNING_LINE = 4
>>> 
>>> class SelectiveProcess(Process):
...    def run(self, *args, **kwargs):
...       registry = globals().setdefault('__warningregistry__', {})
...       registry[(WARNING_TEXT, WARNING_TYPE, WARNING_LINE)] = True
...       return Process.run(self, *args, **kwargs)
... 
>>> if __name__ == '__main__':
...    p = SelectiveProcess(target=yell, args=(WARNING_TEXT,))
...    p.start()
...    p.join()
... 
about to yell AAAAaaaaa!
__main__:6: UserWarning: complimentary second warning.
>>> 

答案 1 :(得分:2)

unpickling不会导致__init__执行两次。我在Windows上运行了以下代码,但它并没有发生(每个__init__只运行一次)。

因此,您需要向我们提供来自my_load_balancer和来自小部件的代码'类。此时,您的问题根本无法提供足够的信息。

作为一个随机猜测,您可以检查my_load_balancer是否复制小部件,导致它们再次被实例化。

import multiprocessing
import collections

"Call `frobnicate(list_of_widgets)` to get the widget with the most frobnals"

def my_load_balancer(widgets):
    partitions = tuple(set() for _ in range(8))
    for i, widget in enumerate(widgets):
        partitions[i % 8].add(widget)
    for partition in partitions:
        yield partition

def my_frobnal_counter(widget):
    return widget.id

def frobnicate_parallel_worker(widgets, output_queue):
    resultant_widget = max(widgets, key=my_frobnal_counter)
    output_queue.put(resultant_widget)

def frobnicate_parallel(widgets):
    output_queue = multiprocessing.Queue()
    # partitions: Generator yielding tuples of sets
    partitions = my_load_balancer(widgets)
    processes = []
    # Line A: Possible start of where the warnings are coming from.
    for partition in partitions:
        p = multiprocessing.Process(
                 target=frobnicate_parallel_worker,
                 args=(partition, output_queue))
        processes.append(p)
        p.start()
    finalists = []
    for p in processes:
        finalists.append(output_queue.get())
    # Avoid deadlocks in Unix by draining queue before joining processes
    for p in processes:
        p.join()
    # Line B: Warnings no longer possible after here.
    return max(finalists, key=my_frobnal_counter)

class Widget:
    id = 0
    def __init__(self):
        print('initializing Widget {}'.format(self.id))
        self.id = Widget.id
        Widget.id += 1

    def __str__(self):
        return str(self.id)

    def __repr__(self):
        return str(self)

def main():

    widgets = [Widget() for _ in range(16)]
    result = frobnicate_parallel(widgets)
    print(result.id)


if __name__ == '__main__':
    main()

答案 2 :(得分:0)

几年后,我终于有了解决方案(在解决不相关的问题时发现)。我已经在Python 3.7、3.8和3.9上进行了测试。

暂时用空白列表[] 修补sys.warnoptions。您只需要在对process.start()的调用周围执行此操作。 sys.warnoptions被记录为您不应手动修改的实现细节。 official recommendations将使用warnings模块中的功能,并在PYTHONWARNINGS中设置os.environ。这行不通。唯一可行的方法是修补sys.warnoptions。在测试中,您可以执行以下操作:

import multiprocessing
from unittest.mock import patch
p = multiprocessing.Process(target=my_function)
with patch('sys.warnoptions', []):
    p.start()
p.join()

如果您不想使用unittest.mock,只需手动打补丁:

import multiprocessing
import sys
p = multiprocessing.Process(target=my_function)
old_warnoptions = sys.warnoptions
try:
    sys.warnoptions = []
    p.start()
finally:
    sys.warnoptions = old_warnoptions
p.join()