如何在具有旧样式和新样式类的 PyInstance_NewRaw() 中保持一致的行为?

29 阅读5分钟

在使用 C 语言编写的 Python 扩展中,当尝试在不调用构造函数的情况下实例化对象时,我遇到了一个问题,而这正是扩展程序的要求。 用于创建实例的类是动态获取的:在某些时候,我有一个实例 x,我希望使用它的类来创建其他实例,所以我存储了 x.class 以供以后使用 -- 假设这个值为 klass。 在稍后,我调用 PyInstance_NewRaw(klass, PyDict_New()),然后问题就出现了。如果 klass 是一个旧样式类,那么该调用的结果是期望的新实例。然而,如果它是一个新样式类,结果将是 NULL,并且引发的异常是:

SystemError: ../Objects/classobject.c:521: bad argument to internal function

需要说明的是,我用的是 Python 2.7.5 版本。在谷歌上搜索,我发现只有一個人也在寻找解决方案(而且似乎他在做权宜之计,但没有详细说明)。 需要说明的 #2:扩展程序正在创建的实例是这些 x 实例的代理 -- x.class 和 x.dict's 是已知的,因此扩展程序根据 class 生成新的实例(使用上述 C 函数)并将各自的 dict 设置为新实例(这些 dict's 具有进程间共享内存数据)。不仅在概念上调用实例的 init 第二次是有问题的(首先:它的状态已经知道,其次:对 ctors 预期的行为是它们应该对每个实例只调用一次),而且在实践中也是不可行的,因为扩展程序无法弄清楚参数及其顺序来调用系统中每个实例的 init()。另外,更改系统中每个实例的 init,这些实例可能是代理,并使它们知道它们将受到的代理机制在概念上是有问题的(它们不应该知道它)而且在实践中也是不可行的。 因此,我的问题是:如何无论实例的类样式如何,都能执行 PyInstance_NewRaw 的相同行为?

2、解决方案 答案1: 新样式类的类型不是实例,而是类本身。因此,PyInstance_* 方法对于新样式类甚至没有意义。 事实上,文档明确解释了这一点:

请注意,此处描述的类对象表示旧样式类,这些类将在 Python 3 中消失。在为扩展模块创建新类型时,您将需要使用类型对象(类型对象部分)。

因此,您必须编写一段代码来检查 klass 是旧样式类还是新样式类,并针对每种情况执行适当的操作。旧样式类的类型是 PyClass_Type,而新样式类的类型是 PyType_Type 或自定义元类。 同时,对于新样式类没有直接等效的 PyInstance_NewRaw。或者,更确切地说,直接等效项——调用其 tp_alloc 插槽然后添加一个词典——将为您提供一个非功能性类。您可以尝试复制所有其他适当的工作,但这将很棘手。或者,您可以使用 tp_new,但这将在类(或其任何基类)中有自定义 new 函数时执行错误的操作。有关一些想法,请参阅 #5180 中拒绝的补丁。 但实际上,您一开始尝试做的事情可能不是一个好主意。也许如果您解释为什么这是一个要求,以及您正在尝试做什么,那么可能会有一种更好的方法来做到这一点。

如果目标是通过创建类的未初始化新实例,然后从已初始化的原型复制其 dict_ 来构建对象,那么我认为有一个更简单的解决方案可以为您解决问题: class 是一个可写的属性。因此(在 Python 中显示;C API 基本相同,只是更详细,我可能会在某个地方搞砸引用计数): class NewStyleDummy(object): pass def make_instance(cls, instance_dict): if isinstance(cls, types.ClassType): obj = do_old_style_thing(cls) else: obj = NewStyleDummy() obj.class = cls obj.dict = instance_dict return obj

新对象将是 cls 的一个实例——特别是,它将具有相同的类字典,包括 MRO、元类等。 如果 cls 有一个元类,该元类对于其构造是必需的,或者有一个自定义 new 方法或 slots……那么这个解决方案将不起作用,但是无论如何,在这种情况下,复制 dict 的设计是没有意义的。我相信在任何可能起作用的情况下,这个简单的解决方案都会起作用。

一开始调用 cls.new 似乎是一个很好的解决方案,但实际上却不是。让我解释一下背景。 当您执行此操作时: foo = Foo(1, 2)

(其中 Foo 是一个新样式类),它将转换为类似于这样的伪代码: foo = Foo.new(1, 2) if isinstance(foo, Foo): foo.init(1, 2)

问题在于,如果 Foo 或其某个基类定义了一个 new 方法,它将期望从构造函数调用中获取参数,就像 init 方法一样。 正如您在问题中解释的那样,您不知道构造函数调用参数——事实上,这是您一开始无法调用普通 init 方法的主要原因。所以,您也不能调用 newnew 的基本实现接受并忽略给定的任何参数。因此,如果您没有任何类具有 new 覆盖或 metaclass,那么您将碰巧逃脱,因为 object.new 中的一个怪癖(顺便说一下,该怪癖在 Python 3.x 中以不同的方式工作)。但那些都是先前解决方案可以处理的确切情况,并且该解决方案有更明显的原因。 换句话说:先前的解决方案依赖于没有人定义 new,因为它从不调用 new。此解决方案依赖于没有人定义 new,因为它用错误的参数调用 new