覆盖的 __setitem__ 调用串行工作,但在 apply_async 调用中中断

Overridden __setitem__ call works in serial but breaks in apply_async call

我已经和这个问题斗争了一段时间了,我终于设法缩小了问题的范围并创建了一个最小的工作示例。

问题总结就是我有一个class继承自一个dict,方便解析misc。输入文件。我已经覆盖了 __setitem__ 调用以支持我们输入文件中部分的递归索引(例如 parser['some.section.variable'] 等同于 parser['some']['section']['variable'])。这对我们来说已经工作了一年多了,但是我们只是 运行 在通过 multiprocessing.apply_async 调用传递这些 Parser class 时遇到了问题。

下面显示的是最小工作示例 - 显然 __setitem__ 调用没有做任何特别的事情,但它访问一些 class 属性很重要,比如 self.section_delimiter - 这是它在哪里打破。它不会在初始调用或串行函数调用中中断。但是,当您使用 apply_async 调用 some_function(它也不执行任何操作)时,它会崩溃。

import multiprocessing as mp
import numpy as np

class Parser(dict):

    def __init__(self, file_name : str = None):
        print('\t__init__')
        super().__init__()
        self.section_delimiter = "."
    
    def __setitem__(self, key, value):
        print('\t__setitem__')
        self.section_delimiter
        dict.__setitem__(self, key, value)
           
def some_function(parser):
    pass

if __name__ == "__main__":

    print("Initialize creation/setting")
    parser = Parser()
    parser['x'] = 1

    print("Single serial call works fine")
    some_function(parser)

    print("Parallel async call breaks on line 16?")
    pool = mp.Pool(1)
    for i in range(1):
        pool.apply_async(some_function, (parser,))

    pool.close()
    pool.join()

如果你运行下面的代码,你会得到以下输出

Initialize creation/setting
    __init__
    __setitem__
Single serial call works fine
Parallel async call breaks on line 16?
    __setitem__
Process ForkPoolWorker-1:
Traceback (most recent call last):
  File "/home/ijw/miniconda3/lib/python3.7/multiprocessing/process.py", line 297, in _bootstrap
    self.run()
  File "/home/ijw/miniconda3/lib/python3.7/multiprocessing/process.py", line 99, in run
    self._target(*self._args, **self._kwargs)
  File "/home/ijw/miniconda3/lib/python3.7/multiprocessing/pool.py", line 110, in worker
    task = get()
  File "/home/ijw/miniconda3/lib/python3.7/multiprocessing/queues.py", line 354, in get
    return _ForkingPickler.loads(res)
  File "test_apply_async.py", line 13, in __setitem__
    self.section_delimiter
AttributeError: 'Parser' object has no attribute 'section_delimiter'

非常感谢任何帮助。我花了相当多的时间来追踪这个错误并重现了一个最小的例子。我不仅愿意修复它,而且还想清楚地填补我对这些 apply_async 和 inheritance/overridden 方法如何相互作用的理解上的一些空白。

如果您需要更多信息,请告诉我。

非常感谢!

以撒

原因

问题的原因是 multiprocessing 序列化和反序列化您的 Parser 对象以跨进程边界移动其数据。这是在反序列化 classes 时使用 pickle. By default pickle does not call __init__() 完成的。因此,当反序列化程序调用 __setitem__() 来恢复字典中的项目时,未设置 self.section_delimiter 并且出现错误:

AttributeError: 'Parser' object has no attribute 'section_delimiter'

仅使用 pickle 而不使用多处理会产生相同的错误:

import pickle

parser = Parser()
parser['x'] = 1

data = pickle.dumps(parser)
copy = pickle.loads(data) # Same AttributeError here

反序列化适用于没有项目的对象,section_delimiter 的值将被恢复:

import pickle

parser = Parser()
parser.section_delimiter = "|"

data = pickle.dumps(parser)
copy = pickle.loads(data)

print(copy.section_delimiter) # Prints "|"

所以从某种意义上说,你只是不幸的是 pickle 在恢复你的 Parser.

的其余状态之前调用了 __setitem__()

解决方法

您可以通过在 __new__() 中设置 section_delimiter 并通过实现 __getnewargs__():

告诉 pickle 将哪些参数传递给 __new__() 来解决此问题
def __new__(cls, *args):
    self = super(Parser, cls).__new__(cls)
    self.section_delimiter = args[0] if args else "."
    return self

def __getnewargs__(self):
    return (self.section_delimiter,)

__getnewargs__() returns 参数元组。因为section_delimiter是在__new__()里设置的,所以不用再在__init__()里设置了。

这是你的Parserclass修改后的代码:

class Parser(dict):

    def __init__(self, file_name : str = None):
        print('\t__init__')
        super().__init__()

    def __new__(cls, *args):
        self = super(Parser, cls).__new__(cls)
        self.section_delimiter = args[0] if args else "."
        return self

    def __getnewargs__(self):
        return (self.section_delimiter,)
 
    def __setitem__(self, key, value):
        print('\t__setitem__')
        self.section_delimiter
        dict.__setitem__(self, key, value)

更简单的解决方案

pickle 在您的 Parser 对象上调用 __setitem__() 的原因是因为它 字典。如果您的 Parser 只是一个 class 恰好实现了 __setitem__()__getitem__() 并且 字典来实现这些调用然后 pickle不会调用 __setitem__() 并且序列化将在没有额外代码的情况下工作:

class Parser:

    def __init__(self, file_name : str = None):
        print('\t__init__')
        self.dict = { }
        self.section_delimiter = "."

    def __setitem__(self, key, value):
        print('\t__setitem__')
        self.section_delimiter
        self.dict[key] = value

    def __getitem__(self, key):
        return self.dict[key]

所以如果没有其他原因让你的 Parser 成为字典,我就不会在这里使用继承。