我们正在尝试将元类用于自定义后端选择(multiprocessing.Process
或threading.Thread
(。此实现背后的基本原理是为我们的自定义用途扩展Process/Thread的功能。而下面的代码适用于fork
(unix中的默认值(。但是对于spawn
(windows中的默认值(,我得到了一个错误。
pickle.PicklingError: Can't pickle <class '__main__.DProcess'>: it's not the same object as __main__.DProcess
错误来自pickle模块,因为对象不相同。
obj: <class '__main__.DProcess'>,
obj.__dict__: {'__module__': '__main__', 'run': <function DProcess.run at 0x7fa76ccd97a0>, '__doc__': None, '__slotnames__': []}
hash(obj): 5875061359185
obj2: <class '__main__.DProcess'>,
obj2.__dict__: {'__module__': '__main__', 'run': <function DProcess.run at 0x7fa76ccd97a0>, '__dict__': <attribute '__dict__' of 'DProcess' objects>, '__weakref__': <attribute '__weakref__' of 'DProcess' objects>, '__doc__': None},
hash(obj2): 5875061305336
我不太确定这里发生了什么。
- 为什么这两个对象不同?从pickle模块对类对象执行
save_global
不会失败。是因为__call__
的实现吗?我该如何修复它 - 为什么不对叉子执行此检查
这是代码:
class Backend(type):
_cache = {}
def __new__(cls, name, bases, dct):
_cls = super().__new__(cls, name, bases, dct)
# store the subclass dict to be used during __call__
Backend._cache.update(
{name: {'cls': cls, 'name': name, 'bases': bases, 'dct': dct}}
)
return _cls
def __call__(cls, *args, **kwargs) -> 'Backend':
try:
# check arg & select the base class
if args[0] == 'process':
import multiprocessing
_cls = multiprocessing.Process
elif args[0] == 'thread':
import threading
_cls = threading.Thread
except KeyError:
print('Please pass process or thread as the 1st arg')
for c in cls.mro()[-2::-1]:
# pick args from __new__ and call type()
arg_cls = Backend._cache[c.__name__]['cls']
arg_name = Backend._cache[c.__name__]['name']
arg_dct = Backend._cache[c.__name__]['dct']
_cls = super().__new__(arg_cls, arg_name, (_cls,), arg_dct)
return type.__call__(_cls, *args[1:], **kwargs)
class DProcess(metaclass=Backend):
def run(self):
print('we are in dprocess')
super().run()
if __name__ == '__main__':
from multiprocessing import set_start_method as _set_start_method
_set_start_method('spawn')
DProcess('process').start()
如果你不需要元类,就不应该使用它——有更好的模式可以满足你的需求。首先:您真的需要从Thread或Process继承吗??也许更好的选择是将这些作为DProcess类的关联属性,然后它就可以作为普通的类属性工作。
由于两者的重要接口基本上都设置了可调用的目标start
和join
,因此您可以为它们创建代理方法,也可以直接在class属性中调用该方法。
也就是说,你的设计很可能像这个一样工作
class DProcess():
def __init__(self, backend):
if backend == "process":
self.backend_cls = multiprocessing.Process
elif backend == "thread":
self.backend_cls = threading.Thread
self.worker = self.backend_cls(target=self.run)
def start(self):
self.worker.start()
# or just call "instance.worker.start()" from outside
def join(self):
return self.worker.join()
def run(self):
print('we are in dprocess')
super().run()
现在,您的原始代码失败的原因是因为它是错误的:您实际上在的每个实例化上都为DProcess创建了一个新的同级类DProcess,动态地,在元类__call__
上调用super().__new__
。
因此,在核心中声明的class DProcess
是一个类。但每次尝试实例化它时,都会创建一个新的类obct,并将其实例化——这就是pickle所抱怨的。(当我们这样做的时候:fork
的多处理在新进程上只有完全相同的对象,而Windows的方式必须从头开始一个新进程,并序列化对象,以便将它们发送到新进程-这不是"检查"-而是DProcess的"重影兄弟"不能由Pickle反序列化,因为它在另一个进程上不存在。
现在,如果你真的想让你的类继承自Thread或Process,你可以只创建这两个类,并使用一个工厂函数来选择你想要的。虽然有一个函数来创建两个类似的类并将其放入列表或全局字典中是很琐碎的,但Pickle不太喜欢:它需要在模块的顶层声明要Pickle的实例或类(这样类的限定名就可以让你回到类构造函数(。即使在那里,也不需要重复代码-您只需将mixin类与任何通用代码一起使用,并用两行创建ProcessDworker和ThreadDWorker(然后可以由工厂函数选择(:
class Stub:
"""just needed in case some linter or static checker complain about
these methods not being present in the mixin
But you could also declare these as @abstractmethod
to ensure just a proper class incorporating Thread or Process can
be instantiated
"""
def run(self): pass
def start(self): pass
def join(self): pass
class DProcessMixin(Stub):
def __init__(self, *args, **kw):
# whatever code you need to setup yoru worker - like creating queues, and such
...
super().__init__(self, ...)
...
class ThreadDprocess(DProcessMixin, threading.Thread):
queue_class = threading.Queue
pass
class ProcessDProcess(DProcessMixin, threadng.
queue_class = multiprocessing.Queue
pass
def DProcess(*args, backend, **kwargs):
if backend == "process":
cls = ProcessDProcess
elif backend == "thread":
cls = ThreadDprocess
return cls(*args, **kwargs)
最后,如果你真的想让一个元类发挥作用,只需注意元类中的__call__
方法与上一个例子中的这个Dprocess
工厂函数处于相同的位置。如果预先创建这两个类并实际缓存它们,并在模块globals
中用realname设置它们,那么它就可以工作了。但是如果你回到你的";高速缓存";你可以看到它是假的:它甚至不能真正";高速缓存";同一元类中多个类的信息:您的缓存应该将类名作为关键字,而作为值hten,您可能有另一个字典保存";name,base,namespace";每个班级的英勇事迹。顺便说一句,您还混淆了传递给元类__new__
的cls
参数和类本身——这也是错误的。简言之:我认为你对类机制的工作原理了解不够,无法围绕它构建代码,而且由于你的问题似乎只通过组合就可以轻松解决,所以这应该是不可能的。