莳萝似乎不尊重元类

Dill doesn't seem to respect metaclass

我最近开始使用莳萝。我有一个元类,我用它来创建单例模式。在任何时刻总是有一个对象。我正在使用 dill serialise.The 问题是一旦对象被加载回来,它不遵守单例模式(由元类强制执行)并且 __init__ 被调用。

这是可以重现问题的代码

import os.path
import dill

class SingletonBase(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if (cls not in cls._instances):
            cls._instances[cls] = super(SingletonBase, cls).__call__(*args, **kwargs)
        return cls._instances[cls]



class TestClass(metaclass=SingletonBase) :
    def __init__(self):
        self.testatrr = "hello"

    def set_method(self):
        self.testatrr = "hi"

    def get_method(self):
        print(self.testatrr)


if os.path.isfile("statefile.dill"):
    with open("statefile.dill", 'rb') as statehandle:
        tobj = dill.load(statehandle)
else:
    tobj=TestClass()


tobj.set_method()
tobj=TestClass()  # init Shouldn't get called
tobj.get_method()

with open("statefile.dill", 'wb') as statehandle:
    dill.dump(tobj, statehandle)

第一个 运行 __init__ 只调用一次。所以 tobj.get_method() 会打印“hi”。但是在第二个 运行 中,当从 dill 加载 tobj 时,调用 TestClass() 会触发 __init__。有没有什么办法解决这一问题 ?要获取 dill 合并元类 ?

我明白 Python 中真的不需要像 Singleton 这样的东西。但是我现在已经用数千行代码走得太远了。希望在不重写的情况下找到出路。非常感谢您的帮助。

所以,首先:当序列化一个普通方法时,在反序列化它(通过 pickle 或 dill.load)时,它的初始化普通机制,即调用它的 __init__,不会是 运行。这是期望的结果:您想要对象的先前状态,并且不触发任何初始化副作用

当用带有 dill 的 metaclass 反序列化 class 时,显然,需要相同的结果:所以 dill 不会 运行 metaclass 的__call__,因为这会触发初始化副作用。

问题在于,在这种有问题的单例安排中,保证“单例”的是恰好 class 实例化的副作用。不是 class creation 这意味着将重复验证测试移动到 metaclass __init__,但是当已经创建 class 被实例化 - 即元class' __call__ 为 运行 时。 tobj = dill.load(statehandle) 行中的 dill 正确跳过了此调用。

因此,当您尝试在下面创建 TestClass 的新实例时,_instances 注册表为空,并创建了一个新实例。

现在 - 这就是用普通的“泡菜”而不是“莳萝”会发生的情况(见下文)。

回到你的单身人士:你必须记住在某些时候 单例实际上是在 运行ning 进程中首次创建的。 当 unpickling 一个旨在作为单例运行的对象时,如果它可以在实例化时检测到一个实例已经存在,它可以重用该实例。

但是,unpickling 会跳过 通过metaclass __call__ 和运行 直接实例化class' __new__。所以class__new__肯定是知道单例的 机制。这意味着无论元class如何,都需要一个带有__new__方法的基础class。由于我们要避免重运行宁__init__,我们需要metaclass __call__,否则,Python会在普通的时候调用__init__ (非反序列化)反序列化。因此,baseclass __new__ 必须与 metaclass __call__ 协作使用缓存机制。

通过调用 __new__ 创建实例后,在 class 上调用 __new__ 后,普通的 unpickling 将通过更新在 [= 中公开的命名空间来恢复实例状态30=]属性。

有了协作元class __call__和基础class __new__,序列化单例与普通泡菜一起工作:

import os.path

import pickle

class SingletonMeta(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):

        mcls = type(cls)
        if cls not in mcls._instances:
            # all that type.__call__ does is call the cls' __new__ and its __init__ in sequence:
            instance = cls.__new__(cls, *args, **kwargs)
            instance.__init__(*args, **kwargs)
        else:
            instance = mcls._instances[cls]
        return instance

class SingletonBase(metaclass=SingletonMeta):

    def __new__(cls, *args, **kwargs):
        mcls = type(cls)
        instance = mcls._instances.get(cls)
        if not instance:
            instance = mcls._instances[cls] = super().__new__(cls, *args, **kwargs)

        return instance


class TestClass(SingletonBase) :
    def __init__(self):
        print("at init")
        self.testatrr = "init run"

    def set_method(self):
        self.testatrr = "set run"

    def get_method(self):
        print(self.testatrr)

if os.path.isfile("statefile.pickle"):
    with open("statefile.pickle", 'rb') as statehandle:
        print("unpickling")
        tobj = pickle.load(statehandle)
else:
    tobj=TestClass()

tobj.set_method()
tobj=TestClass()  # init Shouldn't get called
tobj.get_method()

with open("statefile.pickle", 'wb') as statehandle:
    pickle.dump(tobj, statehandle)

使用莳萝

dill 默认情况下实际序列化每个对象 class 本身(它通过将实际的 class 源代码放入序列化文件,并在反序列化时重新执行),使事情复杂化 又一个数量级。

发生的事情与 Python 中的“单例”行为有关:正如我在评论中所写,使模式复杂化是不健康的,因为当您在模块级别绑定变量时(正式称为“全局”变量,但它不同于其他语言中的“全局”,因为它的范围是模块),你已经有了一个“单例”。语言 确实 一直使用 thius 行为:如果您考虑一下,Python 中的任何 class 或函数已经是“单例”了。

不需要特殊的机制来保证 classes 和函数是单例:它们是由模块中的 defclass 语句创建的事实实例化的,恰好执行一次。 (如果你环顾一下 Whosebug,你会看到人们在 Python 中遇到奇怪的错误,如果他们通过滥用导入机制设法导入同一个模块两次)

现在,惊喜:还有一件事打破了 classes 的“单一性”:dill 反序列化本身!在加载 一个文件,它确实会再次执行 class 主体 - 这是唯一可能使 class 在其代码不存在的项目中可用的机制 (这是 dill 的提议)。

如果您不需要 dill 来实际序列化 classes,并且有 必须用 dill 而不是 pickle 来序列化 单例,你可以使用 pickle,或者调用 dill.dump byref=True 可选参数:这将避免序列化 classes 本身,上面的代码将起作用。除此以外, 这是我第二次需要二阶 metaclass,为了避免dill的class口是心非:

import os.path
import dill


import sys

class SingletonMetaMeta(type):
    def __new__(mcls, name, bases, namespace, **kw):
        mod = sys.modules[namespace["__module__"]]
        if inprocess_metaclass := getattr(mod, name, None):
            return inprocess_metaclass
        return super().__new__(mcls, name, bases, namespace, **kw)


def getkey(cls):
    return f"{cls.__module__}.{cls.__qualname__}"


class SingletonMeta(type, metaclass=SingletonMetaMeta):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        # The metaclass __call__ is actually the only way of preventing '__init__' to be run
        # for new instantiations.
        # for ordinay usage of singletons that do not need to preserve state across
        # serialization/deserialization, the approach of creating a single instance
        # of an ordinary class would work.
        mcls = type(cls)
        if getkey(cls) not in mcls._instances:
            # all that type.__call__ does is call the cls' __new__ and its __init__ in sequence.
            instance = cls.__new__(cls, *args, **kwargs)
            # the pickling protocol ordianrily won't run this __call__ method, so we
            #can always call __init__
            instance.__init__(*args, **kwargs)
        else:
            instance = mcls._instances[getkey(cls)]
        return instance

class SingletonBase(metaclass=SingletonMeta):

    def __new__(cls, *args, **kwargs):
        # check if an instance exists at the metaclss.
        # the pickling protocol calls this __new__ in  a
        # standalone way, in order to avoid re-running
        # the class "__init__". It does not rely on
        # the metaclass __call__ which normal instantiation does
        # because that would always run __new__ and __init__

        # due to the singleton being possibly created in two ways:
        # called from code, or unserialized, we replicate the instantiate and cache bit:
        mcls = type(cls)
        instance = mcls._instances.get(getkey(cls))
        if not instance:
            instance = mcls._instances[getkey(cls)] = super().__new__(cls, *args, **kwargs)

        return instance


class TestClass(SingletonBase) :
    def __init__(self):
        print("at init")
        self.testatrr = "init run"

    def set_method(self):
        self.testatrr = "set run"

    def get_method(self):
        print(self.testatrr)

if os.path.isfile("statefile.dill"):
    with open("statefile.dill", 'rb') as statehandle:
        print("unpickling")
        tobj = dill.load(statehandle)
else:
    tobj=TestClass()

tobj.set_method()
tobj=TestClass()  # init Shouldn't get called
tobj.get_method()

with open("statefile.dill", 'wb') as statehandle:
    dill.dump(tobj, statehandle)

可能的额外问题:

  1. 如果你的单例在其 __init__ 上确实需要额外的参数, 这些也会出现在 __new__ 方法中,但应该 不会转发给 object.__new__。简单地做 super().__new__(cls) 在基地 class __new__.

  2. 您提到了您不想替换的现有代码库 单例机制。如果这意味着您不能通过普通方式在这些片段中插入 baseclass,那么应该编写 metaclass 上的 __new__ 方法以包含 __new__ 单身人士的方法 (通过将当前基 class 作为混合插入,或者通过在其中注入 __new__ 方法)。在这种情况下,请提出后续问题,并且 添加“metaclass”标签,稍后我应该会看到它。