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__ 找到解决方案:

Pickling dynamically generated classes? How can I pickle a dynamically created nested class in python? Pickle a dynamically parameterized sub-class

我没有发现上述方法清楚地适用于我的情况。内容可能是相关的,直接适用的,但是我没找到办法。

我也完全愿意改变我的模式(即使这意味着不动态定义类)。总之,我有以下要求:

扩展和覆盖任意父类的功能,但并非在所有情况下,因为扩展/覆盖类是可选的 对象必须是可腌制的 必须使用 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 动态生成具有灵活父类的类?的主要内容,如果未能解决你的问题,请参考以下文章

设计模式——装饰器模式

Java中动态代理技术生成的类与原始类的区别 (good)

java有这种写法吗?生成一个对象的同时还覆盖重写了父类的方法…………

类的加载机制和反射——使用反射生成JDK动态代理

Java运行时动态生成类几种方式

@Entity注解的类编译后未自动生成动态查询类的解决办法