7

我有一个高度复杂的类:

class C:
    pass

我有这个测试代码:

for j in range(10):
    c = C()
    print c

这使 :

<__main__.C instance at 0x7f7336a6cb00>
<__main__.C instance at 0x7f7336a6cab8>
<__main__.C instance at 0x7f7336a6cb00>
<__main__.C instance at 0x7f7336a6cab8>
<__main__.C instance at 0x7f7336a6cb00>
<__main__.C instance at 0x7f7336a6cab8>
<__main__.C instance at 0x7f7336a6cb00>
<__main__.C instance at 0x7f7336a6cab8>
<__main__.C instance at 0x7f7336a6cb00>
<__main__.C instance at 0x7f7336a6cab8>

可以很容易地看到 Python 开启了两个不同的值。在某些情况下,这可能是灾难性的(例如,如果我们将对象存储在其他一些复杂对象中)。

现在,如果我将对象存储在 List 中:

lst = []
for j in range(10):
    c = C()
    lst.append(c)
    print c

我明白了:

<__main__.C instance at 0x7fd8f8f7eb00>
<__main__.C instance at 0x7fd8f8f7eab8>
<__main__.C instance at 0x7fd8f8f7eb48>
<__main__.C instance at 0x7fd8f8f7eb90>
<__main__.C instance at 0x7fd8f8f7ebd8>
<__main__.C instance at 0x7fd8f8f7ec20>
<__main__.C instance at 0x7fd8f8f7ec68>
<__main__.C instance at 0x7fd8f8f7ecb0>
<__main__.C instance at 0x7fd8f8f7ecf8>
<__main__.C instance at 0x7fd8f8f7ed40>

这解决了这个问题。

所以现在,我要问一个问题......有没有人可以用复杂的词(我的意思是,深刻的)解释 Python 如何处理对象引用?我想,这是一个优化问题(节省内存,或防止泄漏,......)

非常感谢。

编辑: 好的,让我们更具体一点。我很清楚python有时必须收集垃圾......但是,就我而言:

我有一个由 Cython 定义的类返回的列表:管理“节点”列表的“网络”类(网络和节点类都在 a 中定义Cython extension)。每个节点都有一个对象 [然后转换为 (void *)] 'userdata' 对象。Nodes 列表是从 cython 内部填充的,而 UserData 是从 Python 脚本内部填充的。所以在python中,我有以下内容:

...
def some_python_class_method(self):
    nodes = self.netBinding.GetNetwork().get_nodes()
    ...
    for item in it:
        a_site = PyLabSiteEvent()
        #l_site.append(a_site)        # WARN : Required to get an instance on 'a_site' 
                                      #        that persits - workaround...
    item.SetUserData(a_site)

稍后在相同的 python 类中使用相同的 cython getter 重用此节点列表:

def some_other_python_class_method(self, node):
    s_data = node.GetUserData()
    ...

因此,似乎在节点列表的 UserDatas 中进行存储时,我的 python 脚本完全是盲目的,并且正在释放/重用内存。它通过使用附加列表(这里:'l_site')第二次引用(但显然是python方面的第一次)来工作。这就是为什么我必须更多地了解 Python 本身的原因,但似乎我实现 Python 之间通信的方式并Cython负责解决必须面对的问题。

4

3 回答 3

13

这里不需要“复杂”:在第一个示例中,您没有保留对名称“c”引用的对象的其他引用 - 当在后续迭代中运行“c = C()”行中的代码时在循环中,先前保存在“c”中的一个引用丢失了。

由于标准 Python 使用引用计数来跟踪它应该何时从内存中删除对象,因为此时上一个循环交互的对象的引用计数达到 0,它被销毁,并且它的内存可供其他对象使用。

为什么你有 2 个变化的值?=因为在新迭代中的对象被创建的那一刻——即当 Python 执行in右侧的表达式时c = C(),前一次迭代的对象仍然存在,由名称引用c——所以新对象是在另一个内存中构造的位置。然后,Python 继续分配新对象,c在该点前一个对象被销毁,如上所述 - 这意味着在下一次(第 3 次)迭代中,该内存将可用于C.

在第二个例子中,新创建的对象从不丢失引用,因此它们的内存根本没有被释放——新对象总是占用一个新的内存位置。

最重要的是: 使用 Python 或其他高级语言的目的是不必担心内存分配。语言会照顾你。在这种情况下,正如您所注意到的,CPython(标准)实现做了正确的事情。Pypy 或 Jython 等其他实现在上述示例中的每个实例的“内存位置”方面可能具有完全不同的行为,但所有符合要求的实现(包括这 3 个)从Python 程序:(1)它确实可以访问它保留引用的实例,(2)这些实例的数据无论如何都没有损坏或损坏。

于 2012-10-11T02:29:38.640 回答
5

看起来并不复杂。

在第一个示例中,第二次循环时,0x7f7336a6cb00 处的内存被第一次迭代期间创建的 C 实例占用。因此,Python 为新的 C 对象分配下一个内存块 0x7f7336a6cab8。

但是,一旦您创建第二个 C 对象并将其分配给c0x7f7336a6cb00 处的现在孤立对象,就没有任何引用。因此,第三次循环 Python 可以重新使用该位置的内存用于新对象。当然,一旦这样做,位于 0x7f7336a6cab8 的对象就不再具有对它的引用,并且该内存位置可以通过循环进行第四次回收。

但是,在您的第二个示例中,通过将对象附加到列表中,您将保留对您创建的每个对象的引用。由于这些对象总是至少有一个对它们的引用,因此它们“居住”的内存永远无法被释放和回收。因此 Python 每次都会分配新的内存。

第一个例子中产生的危险幻觉就是——一种幻觉。只要您创建的对象存在引用,该对象就会被维护。当不再存在引用时,Python 可以安全地释放对象使用的内存,因为您的程序无法再使用该对象。

于 2012-10-11T02:33:21.413 回答
3

在每个循环期间,您都会更改对象c引用的对象,因此无法访问原始对象,Python 可以自由地摆脱它(既然您永远无法再次访问它,为什么还要保留任何对象?)。那时,它在同一位置拥有空闲内存,并且似乎可以重用它。我不确定这有什么令人惊讶的。如果解释器从来没有重用内存,你会很快用完。

当您将对象添加到列表时不会发生这种情况,因为该对象仍然可以访问,因此 Python 无法摆脱它(因为您可能会再次使用它)。

这不应该引起问题,因为 Python 不会在您仍然可以使用对象时摆脱它,因此如果您“将对象存储在某个复杂对象中”,它们将保持可访问性并且它们的内存将不会被重用(至少在物体消失之前)。

于 2012-10-11T02:27:57.613 回答