Pickle 动态生成具有灵活父类的类?
Posted
技术标签:
【中文标题】Pickle 动态生成具有灵活父类的类?【英文标题】:Pickle dynamically generated classes with flexible parent classes? 【发布时间】:2021-11-16 10:58:55 【问题描述】:我希望有选择地扩展和覆盖某些功能的一系列类。对于这些类,我希望添加相同的功能,因为它们都是兼容的。我正在使用 Python 3.8+。我实现这一点的方法是将类创建为具有附加功能的type
,并将父类作为基础传递。作为一个基本的例子:
class A:
def __init__(self, a, **kwargs):
self.a = a
self.params = kwargs
class B:
def __init__(self, b, **kwargs):
self.b = b
self.params = kwargs
def extend_class_with_print_params(base_class):
def print_params(self):
for k, v in self.params.items():
print(k, v)
return type(
f"Extendedbase_class.__name__",
(base_class,),
dict(print_params=print_params),
)
在上面,我定义了 A 和 B。函数 extend_class_with_print_params
添加了与两者兼容的功能。我的实际用例是为特定 sklearn 预测器的某些实例添加训练前和预测后挂钩,这就是我需要父级可配置的原因。
import joblib
from test_classes import *
normal_a = A(a=10)
joblib.dump(normal_a, "normal_a")
normal_a = joblib.load("normal_a")
extended_a = extend_class_with_print_params(A)(a=15, alpha=0.1, beta=0.2)
joblib.dump(extended_a, "extended_a")
extended_a = joblib.load("extended_a")
转储extended_a
时,抛出如下错误:
_pickle.PicklingError: Can't pickle <class 'test_classes.ExtendedA'>: it's not found as test_classes.ExtendedA
正如以下帖子之一所建议的,我尝试在全局变量中设置 new_class_name
以在返回函数之前指向新类。这使我能够成功转储,但不能在不同的会话中加载文件,这是有道理的,因为全局变量将被重置。一般来说,我也不希望修改全局变量。
我已经尝试但未能根据以下情况使用__reduce__
找到解决方案:
我没有发现上述方法清楚地适用于我的情况。内容可能是相关的,直接适用的,但是我没找到办法。
我也完全愿意改变我的模式(即使这意味着不动态定义类)。总之,我有以下要求:
扩展和覆盖任意父类的功能,但并非在所有情况下,因为扩展/覆盖类是可选的 对象必须是可腌制的 必须使用 joblib 或 pickle 库来腌制对象,而不是像cloudpickle
这样的东西
【问题讨论】:
【参考方案1】:最好避免动态生成类。理想情况下,您可以从一开始就考虑添加的功能。如果您可以控制 A 类和 B 类,则可以执行如下模式:
class A:
hook: Callable
def __init__(self, b, **kwargs):
self.b = b
self.params = kwargs
def print_param_hook(self):
if self.hook:
self.hook(self.params.items())
else:
raise ArithmeticError("No hook function supplied!")
def set_hook(self, hook: Callable):
self.hook = hook
def hook(items):
for k, v in items:
print(k, v)
a = A("foo", y="bar")
a.set_hook(hook1)
a.print_param_hook()
在这里,A 定义了一个预先存在的方法,该方法将调用用户提供的通用函数。当然,这限制了你的钩子函数可以接受什么样的参数。
另一种选择是创建 A 的子类并将您的方法添加到子类中。继续上面的例子:
class SubA(A):
def print_params(self):
for k, v in self.params.items():
print(k, v)
subA = SubA("foo", y="bar")
subA.print_params()
最后,如果你必须为类添加任意方法,你可以使用 setattr 来做到这一点:
def attr_hook(self):
for k, v in self.params.items():
print(k, v)
setattr(A, 'attr_hook', attr_hook)
new_a = A("foo", y="bar")
new_a.attr_hook()
请注意,这将影响 A 创建的每个实例,包括那些在 setattr 之前创建的实例,这不是非常理想的。您可以在此blog post 中阅读有关以这种方式使用setattr
的更多信息,包括如何制作装饰器以使其更加无缝。
所有选项都是完全可腌制的:
import pickle
with open("test.pyc", "wb") as file:
pickle.dump(new_a, file)
with open("test.pyc", "rb") as file:
b = pickle.load(file)
b.attr_hook()
【讨论】:
欣赏响应,但不幸的是,这些建议都不能同时适用 1. 有选择地应用于一个类(即 Foo 的一些实例,ExtendedFoo 的一些实例) 2. 应用于我发现的任何兼容类使 cloudpickle 适合我的用例的方法,但会留下这个问题,以防有人提出有趣的解决方案。 我想我不明白你的用例,但我很想知道为什么子类化对你不起作用。您可以使用转换有选择地应用它(在 SubFoo 上有一个将 Foo 作为 arg 并返回 SubFoo 的方法),它适用于任意父类。显然,您必须为每个父类显式声明一个子类,但坦率地说,这是更好的设计实践(“显式优于隐式”)。如果扩展代码每次都完全相同,请使用 2 个父类:例如。class SubFoo(Foo, Extension): pass
。扩展包含新功能。以上是关于Pickle 动态生成具有灵活父类的类?的主要内容,如果未能解决你的问题,请参考以下文章