为啥快速调用 Python 类的 id 不是唯一的?

Posted

技术标签:

【中文标题】为啥快速调用 Python 类的 id 不是唯一的?【英文标题】:Why is the id of a Python class not unique when called quickly?为什么快速调用 Python 类的 id 不是唯一的? 【发布时间】:2014-01-12 06:02:44 【问题描述】:

我在 Python (3.3.3) 中做一些事情,我遇到了一些让我感到困惑的事情,因为据我所知,类每次被调用时都会获得一个新的 id。

假设你在某个 .py 文件中有这个:

class someClass: pass

print(someClass())
print(someClass())

上面返回相同的 id,这让我感到困惑,因为我正在调用它,所以它不应该是相同的,对吧?当同一类连续调用两次时,Python 是这样工作的吗?当我等待几秒钟时,它会给出一个不同的 id,但如果我像上面的示例一样这样做,它似乎不会那样工作,这让我感到困惑。

>>> print(someClass());print(someClass())
<__main__.someClass object at 0x0000000002D96F98>
<__main__.someClass object at 0x0000000002D96F98>

它返回相同的东西,但为什么呢?例如,我也注意到它的范围

for i in range(10):
    print(someClass())

当类被快速调用时,Python 这样做有什么特别的原因吗?我什至不知道 Python 做到了这一点,或者它可能是一个错误?如果它不是错误,有人可以向我解释如何修复它或方法,以便每次调用方法/类时生成不同的 id?我对它是如何做到的感到非常困惑,因为如果我等待,它确实会改变,但如果我尝试调用同一个类两次或更多次,它不会改变。

【问题讨论】:

【参考方案1】:

对象的id 只保证在该对象的生命周期内是唯一的,而不是在程序的整个生命周期内。您创建的两个 someClass 对象仅在调用 print 期间存在 - 之后,它们可用于垃圾收集(并且在 CPython 中,立即释放)。由于它们的生命周期不重叠,因此它们共享一个 id 是有效的。

在这种情况下也不足为奇,因为结合了两个 CPython 实现细节:首先,它通过引用计数进行垃圾收集(使用一些额外的魔法来避免循环引用的问题),其次,id对象的值与变量的底层指针的值(即它的内存位置)有关。因此,第一个对象,即最近分配的对象,会立即被释放 - 分配的 next 对象最终会出现在同一个位置也就不足为奇了(尽管这也可能取决于解释器如何编译的详细信息)。

如果您依赖于具有不同 ids 的多个对象,您可以将它们保留在周围 - 例如,在列表中 - 以便它们的生命周期重叠。否则,您可能会实现具有不同保证的特定于类的 id - 例如:

class SomeClass:
    next_id = 0

    def __init__(self):
         self.id = SomeClass.nextid
         SomeClass.nextid += 1

【讨论】:

很好的解释,但有一点小问题。它的编写方式意味着内存实际上得到了freed,然后是mallocd(或其他等价物),而实际上它甚至没有超出Python的PyObject空闲列表,就是为什么它如此一致地发生(取决于您解释清楚的警告),甚至跨平台或使用调试 malloc 等等。 objecttp_dealloc调用heap type's tp_free,即PyObject_GC_Del。这反过来使用宏PyObject_FREE。需要注意的是,关于 CPython 的编译方式,without pymalloc 宏 PyObject_FREE 被定义为 PyMem_FREE,对于非调试版本,它只是 free。因此,此时地址重用取决于平台malloc 说得好,提到垃圾收集 :)。【参考方案2】:

如果您阅读 id 的文档,它会说:

返回对象的“身份”。 这是一个整数,保证该对象在其生命周期内是唯一且恒定的。生命周期不重叠的两个对象可能具有相同的id() 值。

这正是正在发生的事情:您有两个生命周期不重叠的对象,因为在创建第二个对象之前,第一个对象已经超出范围。


但也不要相信这会总是发生。特别是如果您需要处理其他 Python 实现,或更复杂的类。该语言所说的只是这两个对象可能具有相同的id() 值,而不是它们。他们的事实取决于两个实现细节:

垃圾收集器必须在你的代码开始分配第二个对象之前清理第一个对象——这保证在 CPython 或任何其他引用计数实现中发生(当没有循环引用时),但不太可能使用 Jython 或 IronPython 中的分代垃圾收集器。

幕后的分配器必须对重用最近释放的相同类型的对象有非常强烈的偏好。这在 CPython 中是正确的,它在基本 C malloc 之上具有多层精美的分配器,但大多数其他实现留给底层虚拟机更多。


最后一件事:object.__repr__ 恰好包含一个与id 相同的十六进制数字的子字符串,这只是 CPython 的一个实现工件,在任何地方都无法保证。根据the docs:

如果可能的话,这应该看起来像一个有效的 Python 表达式,可用于重新创建具有相同值的对象(给定适当的环境)。如果这不可行,则应返回 &lt;...some useful description…&gt; 形式的字符串。

事实上,CPython 的 object 碰巧放了 hex(id(self))(实际上,我相信它相当于 sprintf-ing 它的指针通过 %p,但由于 CPython 的 id 只是返回相同的指针转换到最终相同的long)在任何地方都无法保证。即使它是真的……在object 甚至存在于早期的 2.x 天之前。在交互式提示符下进行这种简单的“这里发生了什么”调试是安全的,但不要尝试在此之外使用它。

【讨论】:

【参考方案3】:

我在这里感觉到了一个更深层次的问题。您不应依赖 id 在程序的整个生命周期内跟踪唯一实例。您应该简单地将其视为每个对象实例持续时间的非保证内存位置指示器。如果您立即创建和释放实例,那么您很可能会在同一内存位置创建连续的实例。

也许您需要做的是跟踪一个类静态计数器,该计数器为每个新实例分配一个唯一的 id,并为下一个实例增加类静态计数器。

【讨论】:

我不认为 OP 在这里尝试使用id(或者,实际上,出现在repr 中的等效数字)用于调试对象生命周期以外的任何目的......这是一件事它有好处。 @abarnert 如果您在 mhlester 的回答中看到 OP 的评论,这似乎表明 OP 实际上正在寻找这样的等效行为。 虽然从他对同一答案的后续评论来看,他似乎不是真的在寻找那个,他只是在调试时感到困惑......【参考方案4】:

它正在释放第一个实例,因为它没有被保留,然后由于在此期间内存没有发生任何事情,它第二次实例化到同一位置。

【讨论】:

哦,我明白了,有没有办法告诉 python 内存发生了变化,所以它的实例化方式不同?我不确定如何快速更改内存,以便每次都分配不同的 id。 我不会使用 id 作为您的标识符。传入并存储一个计数器变量,或者如果您想使用 id,请将实例添加到列表或其他对象中以防止其被重用。 我不知道为什么你需要有不同的 id,但是,不管你的原因是什么,这可能是错误的。此外,您还必须考虑到,由于内部“缓存”,两个不同且明显不相关的变量可能会发生(使用不可变类型)共享同一个对象(和 id)。 @user3130555:首先为什么这对您来说是个问题?如果第一个变量仍然存在,则ids 保证不会发生冲突。如果它不在左右,那么没有什么冲突。 @Faust:好点。举个简单的例子,int(1) 可能只会返回同一个对象,无论你调用多少次,在几乎任何合理的 Python 实现中……【参考方案5】:

试试这个,尝试调用以下:

a = someClass()
for i in range(0,44):
    print(someClass())
print(a)

你会看到不同的东西。为什么?因为“foo”循环中第一个对象释放的内存被重用了。另一方面,a 未被重用,因为它被保留了。

【讨论】:

【参考方案6】:

不释放内存位置(和id)的一个例子是:

print([someClass() for i in range(10)])

现在所有的 id 都是唯一的。

【讨论】:

以上是关于为啥快速调用 Python 类的 id 不是唯一的?的主要内容,如果未能解决你的问题,请参考以下文章

CUDA 中的每个内核调用是不是保证唯一线程 ID?

子类为啥不能直接调用父类的属性

Object在其子类中,为啥不能调用clone()???

为啥Python调用方法,有的前面加类名,有的不加?

Python入门

java,hashset,string一点问题?为啥hashset可以识别String唯一性?