Python编程高级Python开发人员

通过哪种重构机制,**Python**的`pickle`模块允许类直接向`__new__`提供参数,从而绕过`__init__`?

用 Hintsage AI 助手通过面试

问题的答案

pickle模块的协议演变为处理__init__具有副作用或昂贵计算的对象。早期协议要求在反序列化时调用__init__,这导致了文件句柄或数据库连接等资源的问题。协议2引入了__getnewargs__,协议4则通过__getnewargs_ex__扩展了这一点,以支持关键字参数,提供更细致的对象重构控制。

在反序列化对象时,Python通常需要重建对象状态。如果__init__执行验证、打开网络套接字或修改全局状态,在反序列化期间重新执行它可能是错误的或低效的。挑战在于在不触发这些初始化副作用的情况下恢复对象的状态,仅使用存储的数据通过低级__new__构造函数重建实例。

__getnewargs_ex__双下划线方法(或对旧协议使用__getnewargs__)允许类返回一个(args, kwargs)的元组,pickle直接传递给__new__,完全跳过__init__。该方法在重构阶段被调用,其返回值决定如何从序列化字节创建实例。这种方法确保在没有调用可能不适合恢复对象的初始化逻辑的情况下,以正确的初始状态实例化对象。

import pickle class DatabaseConnection: def __new__(cls, dsn, timeout=30): instance = super().__new__(cls) instance.dsn = dsn instance.timeout = timeout return instance def __init__(self, dsn, timeout=30): # 我们希望在反序列化期间跳过的昂贵操作 self.socket = create_socket(dsn, timeout) def __getnewargs_ex__(self): # 返回`__new__`的参数和关键字参数 return ((self.dsn,), {'timeout': self.timeout}) def __getstate__(self): # 不序列化socket return {'dsn': self.dsn, 'timeout': self.timeout} def __setstate__(self, state): self.dsn = state['dsn'] self.timeout = state['timeout'] # 如果需要,重新建立socket,或等待延迟初始化 # 用法 conn = DatabaseConnection('postgresql://localhost', timeout=60) serialized = pickle.dumps(conn, protocol=4) restored = pickle.loads(serialized) # 未调用`__init__`

生活中的情况

数据处理管道缓存持有打开的TCP套接字和身份验证令牌的Redis连接对象。当将这些缓存条目序列化到磁盘以便于应用程序重启之间的持久化时,反序列化期间调用__init__会尝试立即创建新的套接字连接,这在脱机环境中失败,或者导致资源泄露。该场景需要一种序列化策略,同时保留连接参数,实际网络建立的请求必须等到应用程序显式请求时再进行。

实现__getstate__以仅返回连接参数(主机、端口、身份验证),并使用__setstate__手动设置属性并选择性地重新打开连接。这种方法兼容旧的pickle协议并且明确。但是,它在默认反序列化过程中仍然调用__init__,除非使用__reduce__谨慎避免,从而可能在__setstate__可以清理之前触发副作用。

实现__reduce__以返回一个元组(可调用对象, 参数, 状态),其中可调用对象是类方法或__new__本身。这提供了对重构的完全控制,但文档繁琐并且需要手动管理状态字典。这增加了代码复杂性,并导致类结构与序列化数据之间版本不匹配的风险。

实现__getnewargs_ex__以返回((host, port), {'auth': token}),允许pickle直接调用__new__(host, port, auth=token)而跳过__init__。选择这种解决方案是因为它利用了现代协议4的特性,干净地将“创建空实例”阶段与“初始化资源”阶段分开,并避免了__reduce__的样板代码。结果是一个强健的缓存系统,其中连接对象的配置保持不变,但在明确需要之前保持关闭,防止在批量反序列化操作期间耗尽资源。

候选人常常忽略的内容

为什么__getnewargs_ex__防止调用__init__,而仅__setstate__却不这样做?

pickle重构一个对象时,它检查__getnewargs_ex__(或__getnewargs__)。如果存在,反序列化器使用返回的值调用__new__(*args, **kwargs),并立即应用状态通过__setstate__(如果可用),完全跳过__init__。相反,没有这些方法,pickle使用默认构造路径,总是会在__new__之后调用__init__。候选人通常假设__setstate__覆盖初始化,但__setstate__仅在__init__已经执行后对实例进行修补,这对防止副作用来说太迟。

如果__getnewargs_ex__返回的值不是长度为2的元组,会发生什么?

pickle协议严格要求__getnewargs_ex__返回长度为2的元组:(args_tuple, kwargs_dict)。如果它返回一个单一的参数元组(像__getnewargs__),Python将在反序列化时引发TypeError,因为尝试将结果解包到__new__(*args, **kwargs)中。如果返回None或其他类型,反序列化器可能崩溃或表现不稳定,这与期望仅返回参数的__getnewargs__不同。

当两个都定义时,__getnewargs_ex__如何与__reduce_ex__互动?

__reduce_ex__是更高层的协议方法,负责组织序列化。如果类定义了__getnewargs_ex__,则__reduce_ex__(具体是在协议4+中)会自动将其返回值合并到减少元组中,使用NEWOBJ_EX操作码。如果两者都存在但__reduce_ex__返回了不使用标准重构路径的自定义可调用对象,则优先考虑它,可能完全忽略__getnewargs_ex__