Python内存管理洞察id()

2024-06-24 13:31:30 发布

您现在位置:Python中文网/ 问答频道 /正文

玩弄id()。从查看不相同对象中相同属性的地址开始。但我想那现在已经不重要了。具体到代码:

class T(object):
    pass

class N(object):
    pass

第一次测试(在交互控制台中):

^{pr2}$

实际上,这并不奇怪。n.__class__与{}不同,所以很明显它们不可能是同一个对象。此时__class__是否只是这些对象之间的区别?假设,如:

>>> n1 = N()
>>> n2 = N()
>>> id(n1) == id(n2)
False

或者Python只是创建单独的对象,即使它们在内容上完全相同,而不是首先将名称n1n2分配给相同的对象(内存中),然后在修改n1或{}时重新赋值?为什么?我知道这可能是一个惯例、优化、情绪、低级问题(别放过我),但我还是很好奇。在

现在,与前面相同的类,T()&;N()——在shell中一个接一个地执行:

>>> id(N())
4298619728
>>> id(N())
4298619792
>>> id(N())
4298619728
>>> id(N())
4298619792

为什么要耍杂耍?在

但奇怪的部分来了。同样的类,shell:

>>> id(N()), id(T())
(4298619728, 4298619728)
>>> id(N()), id(T())
(4298619728, 4298619728)
>>> id(N()), id(T())
(4298619728, 4298619728)

不仅杂耍停止了,而且N()和T()似乎是同一个对象。由于它们不可能,我将其理解为N()返回的内容在id()调用之后,在整个语句结束之前被销毁。在

我知道这可能是一个很难回答的问题。但我希望有人能告诉我,我在这里观察到的是什么,我的理解是否正确,分享一些关于口译员内部工作和记忆管理的黑暗魔法,或者指出一些关于这个问题的好资源?在

谢谢你在这件事上的时间。在


Tags: 对象代码id内容属性object地址pass
3条回答

文档says it all

id(object):

Return the “identity” of an object. This is an integer (or long integer) which is guaranteed to be unique and constant for this object during its lifetime. Two objects with non-overlapping lifetimes may have the same id() value.

无论何时调用构造函数,都会创建一个新对象。该对象的id与当前活动的任何其他对象的id都不同。在

>>> n1 = N()
>>> n2 = N()
>>> id(n1) == id(n2)
False

两个对象的“内容”并不重要。它们是两个不同的实体;它们得到不同的ID似乎完全合乎逻辑。在

在CPython中,id只是内存地址。它们确实会被回收:如果一个对象被垃圾回收,那么在将来某个时候创建的另一个对象可能会获得相同的id。这就是您在重复的id(N()), id(T())测试中看到的行为:由于您没有保留对新创建对象的引用,解释器可以自由地对它们进行垃圾收集并重用它们的id。在

ids的回收显然是一个实现/平台的产物,不应该依赖它。在

我可能错了,但我想你看到的是垃圾回收员在行动。对N()或T()的调用会创建一个不存储在任何地方的对象,然后由GC获取。然后,可以重用存储器地址。在

你问了很多问题。我会尽我所能回答其中的一些问题,希望你能找出其余的问题(如果需要帮助,可以询问)。在

第一个问题:解释id

的行为
>>> n1 = N()
>>> n2 = N()
>>> id(n1) == id(n2)
False

这表明Python每次调用对象构造函数时都会创建一个新对象。这是有道理的,因为这正是你所要求的!如果您只想分配一个对象,但是给它两个名称,那么您可以写下:

^{pr2}$

第二个问题:为什么不写就抄?

您接着会问Python为什么不为对象分配实现一个写时拷贝策略。现在的策略是,每次调用构造函数时构造一个对象,是:

  1. 实施简单
  2. 明确(完全按照你的要求去做)
  3. 易于记录和理解。在

另外,写时拷贝的用例也不引人注目。它只在创建了许多相同的对象且从未被修改的情况下保存存储。但既然如此,为什么要创建许多相同的对象呢?为什么不使用一个对象呢?在

第三个问题:解释分配行为

在CPython中,对象的id是(秘密地!)它在内存中的地址。请参见^{}, line 907中的函数builtin_id。在

通过使用__init____del__方法生成一个类,可以研究Python的内存分配行为:

class N:
    def __init__(self):
        print "Creating", id(self)
    def __del__(self):
        print "Destroying", id(self)

>>> id(N())
Creating 4300023352
Destroying 4300023352
4300023352

您可以看到Python能够立即销毁对象,这使它能够回收空间,以便在下一次分配时重新使用。Python使用reference counting来跟踪每个对象有多少个引用,当不再有对对象的引用时,它就会被销毁。在同一语句的执行过程中,同一内存可能会被多次重复使用。例如:

>>> id(N()), id(N()), id(N())
Creating 4300023352
Destroying 4300023352
Creating 4300023352
Destroying 4300023352
Creating 4300023352
Destroying 4300023352
(4300023352, 4300023352, 4300023352)

第四个问题:解释“杂耍”

恐怕我无法重现您所展示的“杂耍”行为(交替创建的对象获得不同的地址)。你能提供更多的细节吗,比如Python版本和操作系统?如果你使用我的类N,你会得到什么结果?

好的,如果我让我的类Nobject继承,我可以重现这种杂耍。在

我有一个关于为什么会发生这种情况的理论,但我还没有在调试器中检查过,所以请稍加注意。在

首先,您需要了解一下Python的内存管理器是如何工作的。通读^{},看完后再回来。我等一下。在

。。。在

明白了吗?很好。现在您知道Python通过按大小将小对象分类到池中来管理小对象:每个4kib池包含大小范围较小的对象,并且有一个空闲列表来帮助分配器快速为下一个要分配的对象找到一个插槽。在

现在,Python交互式shell也在创建对象:例如抽象语法树和编译的字节码。我的理论是,当N是一个新样式的类时,它的大小与交互shell分配的其他对象进入同一个池中。事件的顺序是这样的:

  1. 用户输入id(N())

  2. Python在poolp中为刚刚创建的对象分配一个槽(将这个槽称为a)。

  3. Python销毁对象并将其槽返回到poolp的空闲列表中。

  4. 交互式shell分配一些对象,称之为O。这正好是进入poolP的正确大小,因此它得到刚刚释放的slotA

  5. 用户再次输入id(N())

  6. Python在poolp中为刚刚创建的对象分配一个插槽。SlotA已满(仍包含objectO),因此它取而代之的是SlotB

  7. 交互shell忘记了objectO,因此它被销毁,而slotA将返回池P的空闲列表。

你可以看到这解释了交替行为。在用户输入id(N()),id(N())的情况下,交互式shell没有机会在两个分配之间插入oar,因此它们可以在池中的同一个槽中运行。在

这也解释了为什么旧样式的对象没有发生这种情况。假设旧样式的对象大小不同,所以它们放在不同的池中,不会与交互式shell创建的任何对象共享插槽。在

第五个问题:交互式shell可能分配哪些对象?

有关详细信息,请参见^{},但基本上是交互式shell:

  1. 读取输入并分配包含代码的字符串。

  2. 调用解析器,它构造描述代码的抽象语法树。

  3. 调用编译器,编译器构造编译后的字节码。

  4. 调用计算器,它为堆栈帧、局部变量、全局变量等分配对象。

我不知道究竟是哪一个物体造成了“杂耍”。不是输入字符串(字符串有自己的专用分配器);也不是抽象语法树(它在编译后被丢弃)。可能是字节码对象。在

相关问题 更多 >