为什么在快速调用时Python类的ID不是唯一的?

39

我正在使用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会这样做,或者这可能是一个bug吗?如果不是bug,有人能解释一下如何修复它或一种方法,使得每次调用方法/类时都生成不同的id吗?我很困惑它是如何实现的,因为如果我等待一段时间,它确实会改变,但如果我尝试调用相同的类两次或更多次,它就不会改变。

6个回答

52
对象的id仅在对象生命周期内是唯一的,而不是整个程序的生命周期。您创建的两个someClass对象仅在调用print期间存在 - 之后,它们可供垃圾回收(在CPython中,立即被释放)。由于它们的生命周期不重叠,因此它们共享一个id是有效的。
在这种情况下,这也不奇怪,因为CPython有两个实现细节的结合:首先,它通过引用计数进行垃圾回收(带有一些额外的魔法以避免循环引用问题),其次,对象的id与变量的底层指针的值相关联(即,其内存位置)。因此,最近分配的第一个对象会立即被释放,下一个分配的对象结束时也会出现在同一位置(尽管这可能还取决于解释器编译的详细信息)。
如果您依赖于多个对象具有不同的id,则可以将它们保留下来 - 比如说在列表中 - 以使它们的生命周期重叠。否则,您可以实现类特定的id,它具有不同的保证 - 例如:
class SomeClass:
    next_id = 0

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

6
解释得很好,但有一个小问题。它的表述暗示着内存实际上被 free 然后再次被 malloc(或等价物)分配,而实际上它甚至没有超出Python的PyObject空闲列表,这就是为什么它会如此一致地发生(考虑到您详细解释的注意事项),即使在不同平台或使用调试 malloc 等情况下也是如此。 - abarnert
2
基础的 object tp_dealloc 调用堆类型的 tp_free,它是 PyObject_GC_Del。这又使用了宏 PyObject_FREE。关于 CPython 的编译方式,需要注意的是,如果没有启用 pymalloc,则宏 PyObject_FREE 被定义为 PyMem_FREE,对于非调试版本只是 free。因此,在这一点上,地址重用取决于平台的 malloc - Eryk Sun
关于提到垃圾回收,说得好 :)。 - ivanleoncz

15
如果您阅读id的文档,它会说:
返回一个对象的“标识”,这是一个整数,保证在其生命周期内对于该对象是唯一且不变的。两个生命周期不重叠的对象可能具有相同的id()值。
这正是发生的事情:您有两个生命周期不重叠的对象,因为第一个已经超出范围,而第二个则是在第一个超出范围之后创建的。
但是也不要相信这将始终发生,特别是如果您需要处理其他Python实现或更复杂的类。语言所说的只是这两个对象可能具有相同的id()值,而不是它们一定会具有相同的id()值。它们确实具有相同的id()值取决于两个实现细节:
- 垃圾回收器必须在代码开始分配第二个对象之前清理第一个对象--当没有循环引用时,这对CPython或任何其他引用计数实现是有保证的,但在Jython或IronPython等生成式垃圾收集器中可能非常不太可能。 - 内部分配器必须非常强烈地倾向于重用最近释放的相同类型的对象。这在CPython中是真实的,因为它在基本的C malloc之上有多个层次的花哨的分配器,但大多数其他实现将更多地留给底层虚拟机。
最后一点:object.__repr__恰好包含与十六进制数字id相同的子字符串只是CPython的实现工件,在任何地方都不能保证。根据文档

如果可能的话,这应该看起来像一个有效的Python表达式,可以用于在相同的环境下重新创建具有相同值的对象。如果不是可能的,应返回格式为<...一些有用的描述...>的字符串。

事实上,CPython的object恰好放置了hex(id(self))(实际上,我相信它正在通过%p对其指针进行等效的格式化,但由于CPython的id只返回转换为long的相同指针,因此结果相同),并没有任何保证。即使自早先的2.x天中存在object。您可以安全地依赖它来进行简单的“发生了什么”调试,但不要尝试将其用于其他用途。


4

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

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


我认为OP在这里并没有试图使用id(或者实际上在repr中出现的等价数字)来进行除了调试对象生命周期之外的任何其他目的... 而这正是它所擅长的。 - abarnert
@abarnert,如果您看到mhlester答案中的OP评论,似乎表明OP实际上正在寻找这样的等效行为。 - Preet Kukreti
尽管从他在同一个答案的后续评论中看起来,他似乎并不真正寻找那个,只是在调试过程中感到困惑... - abarnert

3
尝试这个,试着调用以下内容:
a = someClass()
for i in range(0,44):
    print(someClass())
print(a)

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


3

由于实例未被保留,所以它首次释放,然后由于在此期间内存中没有发生任何变化,它第二次实例化到同一位置。


哦,我明白了,有没有办法告诉Python内存已经改变,以便它可以实例化不同的对象?我不确定如何快速更改内存,使其每次分配不同的ID。 - user3130555
我不会使用id作为您的标识符。要么传递并存储计数器变量,要么如果您想使用id,请将实例添加到列表或其他对象中,以防止其被重用。 - mhlester
2
我不知道为什么你需要有不同的ID,但是无论你的原因是什么,它可能是错误的。此外,你必须考虑到由于内部“缓存”,两个不同且表面上不相关的变量可能会共享相同的对象(和ID)(对于不可变类型)。 - smeso
@user3130555:为什么这对你来说是个问题呢?如果第一个变量仍然存在,那么id就不会冲突。如果它不存在,那么也就没有什么可以冲突的了。 - abarnert
@Faust:说得好。举个简单的例子,int(1)在几乎任何合理的Python实现中,无论你调用多少次,都可能只会返回相同的对象... - abarnert
@abarnert 这不是一个大问题,但是当我打印数据以确保一切都正常时,我注意到这些东西具有相同的ID,这让我认为它们是相同的,但实际上并不是这样。 - user3130555

0
一个未释放内存位置(和ID)的示例是:
print([someClass() for i in range(10)])

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接