为什么我不能更改对象实例的 __class__ 属性?

Ric*_*cco 10 python python-3.x

class A(object):
    pass

class B(A):
    pass

o = object()
a = A()
b = B()
Run Code Online (Sandbox Code Playgroud)

虽然我可以改变a.__class__,但我不能做同样的事情o.__class__(它会引发TypeError错误)。为什么?

例如:

isinstance(a, A) # True
isinstance(a, B) # False
a.__class__ = B
isinstance(a, A) # True
isinstance(a, B) # True

isinstance(o, object) # True
isinstance(o, A) # False
o.__class__ = A # This fails and throws a TypeError
# isinstance(o, object)
# isinstance(o, A)
Run Code Online (Sandbox Code Playgroud)

我知道这通常不是一个好主意,因为如果处理不当会导致一些非常奇怪的行为。这只是为了好奇。

Min*_*Max 7

CPythonObjects/typeobject.c 中有关于这个主题的评论:

在 3.5 之前的 CPython 版本中,代码 compatible_for_assignment未设置为正确检查非 HEAPTYPE 类的内存布局/插槽/等兼容性,因此我们只是禁止__class__在任何不是 HEAPTYPE -> HEAPTYPE 的情况下赋值。

在 3.5 开发周期中,我们修复了代码 compatible_for_assignment以正确检查任意类型之间的兼容性,并开始允许__class__在新旧类型实际上具有兼容插槽和内存布局的所有情况下进行分配(无论它们是否被实现为 HEAPTYPEs或不)。

然而,就在 3.5 发布之前,我们发现这导致了像 int 这样的不可变类型的问题,其中解释器假定它们是不可变的,并且会保留一些值。以前这不是问题,因为它们确实是不可变的——特别是,解释器应用这种实习技巧的所有类型碰巧也是静态分配的,所以旧的 HEAPTYPE 规则“意外地”阻止了它们允许__class__分配。但是随着__class__赋值的改变,我们开始允许像这样的代码

class MyInt(int):
#   ...
# Modifies the type of *all* instances of 1 in the whole program,
# including future instances (!), because the 1 object is interned.
 (1).__class__ = MyInt
Run Code Online (Sandbox Code Playgroud)

(见https://bugs.python.org/issue24912)。

理论上,正确的解决方法是确定哪些类依赖于这个不变量,并以某种方式禁止__class__仅对它们进行赋值,可能是通过某种机制,如新的 Py_TPFLAGS_IMMUTABLE 标志(一种“黑名单”方法)。但在实践中,由于这个问题在 3.5 RC 周期的后期没有被注意到,我们采取了保守的方法并恢复了我们曾经拥有的相同的 HEAPTYPE->HEAPTYPE 检查,以及一个“白名单”。目前,白名单仅包含 ModuleType 子类型,因为这些是首先激发补丁的情况——请参阅https://bugs.python.org/issue22986——并且由于模块对象是可变的,我们可以确定他们绝对不会被拘留。所以现在我们允许 HEAPTYPE->HEAPTYPE ModuleType 子类型 -> ModuleType 子类型。

就我们所知,除以下“if”语句之外的所有代码都将正确处理非 HEAPTYPE 类,并且只需要 HEAPTYPE 检查来保护解释器已为其烘焙的非 HEAPTYPE 类的子集所有实例都是真正不可变的。

解释:

CPython以两种方式存储对象:

对象是在堆上分配的结构。特殊规则适用于对象的使用,以确保它们被正确地垃圾收集。对象永远不会静态分配或在堆栈上;它们只能通过特殊的宏和函数访问。 (类型对象是第一条规则的例外;标准类型由静态初始化的类型对象表示,尽管 Python 2.2 的类型/类统一工作也使得拥有堆分配的类型对象成为可能)。

来自Include/object.h 中注释的信息。

当您尝试将新值设置为 时some_obj.__class__,将object_set_class调用该函数。它继承自PyBaseObject_Type,请参见/* tp_getset */字段。此函数检查:新类型可以替换旧类型some_obj吗?

举个例子:

class A:
    pass

class B:
    pass

o = object()
a = A() 
b = B() 
Run Code Online (Sandbox Code Playgroud)

第一种情况:

a.__class__ = B 
Run Code Online (Sandbox Code Playgroud)

a对象的类型是A堆类型,因为它是动态分配的。以及B. 该a的类型是没有问题的改变。

第二种情况:

o.__class__ = B
Run Code Online (Sandbox Code Playgroud)

的类型o是内置类型object( PyBaseObject_Type)。它不是堆类型,因此TypeError引发了:

TypeError: __class__ assignment only supported for heap types or ModuleType subclasses.
Run Code Online (Sandbox Code Playgroud)


Dav*_*ing 5

您只能更改__class__为具有相同内部 (C)布局的另一种类型。运行时甚至不知道布局,除非类型本身是动态分配的(“堆类型”),因此这是排除作为源或目标的内置类型的必要条件。您还必须具有__slots__相同名称的相同集合。