莳萝似乎不尊重元类
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 和函数是单例:它们是由模块中的 def
和 class
语句创建的事实实例化的,恰好执行一次。 (如果你环顾一下 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)
可能的额外问题:
如果你的单例在其 __init__
上确实需要额外的参数,
这些也会出现在 __new__
方法中,但应该
不会转发给 object.__new__
。简单地做
super().__new__(cls)
在基地 class __new__
.
您提到了您不想替换的现有代码库
单例机制。如果这意味着您不能通过普通方式在这些片段中插入 baseclass,那么应该编写 metaclass 上的 __new__
方法以包含 __new__
单身人士的方法
(通过将当前基 class 作为混合插入,或者通过在其中注入 __new__
方法)。在这种情况下,请提出后续问题,并且
添加“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 和函数是单例:它们是由模块中的 def
和 class
语句创建的事实实例化的,恰好执行一次。 (如果你环顾一下 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)
可能的额外问题:
如果你的单例在其
__init__
上确实需要额外的参数, 这些也会出现在__new__
方法中,但应该 不会转发给object.__new__
。简单地做super().__new__(cls)
在基地 class__new__
.您提到了您不想替换的现有代码库 单例机制。如果这意味着您不能通过普通方式在这些片段中插入 baseclass,那么应该编写 metaclass 上的
__new__
方法以包含__new__
单身人士的方法 (通过将当前基 class 作为混合插入,或者通过在其中注入__new__
方法)。在这种情况下,请提出后续问题,并且 添加“metaclass”标签,稍后我应该会看到它。