python 函数中局部变量的引用计数何时减少?

When is the reference count for a local variable in a python function decreased?

我有以下功能:

def myfn():
    big_obj = BigObj()
    result = consume(big_obj)
    return result

BigObj() 值的引用计数何时增加/减少: 是吗:

  1. 当调用 consume(big_obj) 时(因为之后在 myfn 中未引用 big_obj)
  2. 当函数returns
  3. 有一点,我还没有

将最后一行更改为:

会有什么不同吗?
return consume(big_obj)

编辑(评论说明):

但是临时变量(例如 f1(f2()) 是什么?

我用这段代码检查了对临时对象的引用:

import sys
def f2(c):
    print("f2: References to c", sys.getrefcount(c))


def f0():
    print("f0")
    f2(object())

def f1():
    c = object()
    print("f1: References to c", sys.getrefcount(c))
    f2(c)

f0()
f1()

这会打印:

f0
f2: References to c 3
f1: References to c 2
f2: References to c 4

似乎保留了对临时变量的引用。并不是说 getrefcount 给出的值比你预期的多,因为它也包含一个引用。

变量在 Python 中具有函数作用域,因此在函数 returns 之前它们不会被删除。据我所知,您不能从函数外部销毁函数中对局部变量的引用。我在示例代码中添加了一些 gc 调用来对此进行测试。

import gc
class BigObj:
    pass
def consume(obj):
    del obj  # only deletes the local reference to obj, but another one still exists in the calling function

def myfn():
    big_obj = BigObj()
    big_obj_id = id(big_obj)  # in CPython, this is the memory address of the object
    consume(big_obj)
    print(any(id(obj) == big_obj_id for obj in gc.get_objects()))
    return big_obj_id

>>> big_obj_id = myfn()
True
>>> gc.collect()  # I'm not sure where the reference cycle is, but I needed to run this to clean out the big object from the gc's list of objects in my shell
>>> print(any(id(obj) == big_obj_id for obj in gc.get_objects()))
False

自从 True 被打印出来后,即使在函数中的那个点之后没有对该变量的引用,即使在我们强制进行垃圾收集之后大对象仍然存在。在函数 returns 正确地确定对大对象的引用计数为 0 之后强制进行垃圾收集,因此它会清理该对象。注意:正如下面的评论所指出的,已删除对象的 ID 可能会被重复使用,因此检查相同的 ID 可能会导致误报。不过,我相信结论还是正确的。

要尽早回收内存,您可以做的一件事是使大对象成为全局对象,这样您就可以从被调用函数中删除它。

def consume():
    # do whatever you need to do with the big object
    big_obj_id = id(big_obj)
    del globals()["big_obj"]
    print(any(id(obj) == big_obj_id for obj in gc.get_objects()))
    # do anything else you need to do without the big object

def myfn():
    globals()["big_obj"] = BigObj()
    result = consume()
    return result

>>> myfn()
False

这种模式很奇怪,而且可能很难维护,所以我建议不要使用它。如果你只需要在 consume() 调用后立即删除大对象,你可以这样做,以便尽快释放大对象使用的内存。

big_obj = BigObj()
consume(big_obj)
del big_obj

您可以尝试的另一种策略是删除从 consume() 函数传入的大对象中的引用,其中 del big_obj.x 某些属性 x.

When is the reference count for big_obj decreased

big_obj 没有引用计数。变量没有引用计数。 做。

big_obj = BigObj()

这行代码创建了 BigObj class 的一个实例。该实例的引用计数可能会增加或减少多次,具体取决于该创建过程的实现细节(不一定写在 Python 中)。值得注意的是, 对名称 big_obj 的赋值增加了引用计数。

when the function returns

此时,名字big_obj不复存在——名字不会消失只是因为它不会'不能再用了。 (在一般情况下,这真的很难检测到,而且通常没有特别的好处)。如果您必须导致一个名称在操作的特定点不复存在(例如,因为您知道这是最后一个引用并且想要触发垃圾收集;或者可能是因为您正在使用 __weakref__) 做一些棘手的事情,那么这就是 del 语句的目的。

由于对象的名称不复存在,其引用计数减少。 如果该计数达到零,则该对象将被垃圾回收。由于各种各样的原因,它可能在其他地方存储了任意数量的引用。 (例如,在实现 class 的 C 代码中可能存在错误;或者 class 可能故意维护其自己的每个已创建实例的列表。)


请注意,以上所有内容都专门针对参考实现。在其他实现中,情况会有所不同。可能有一些其他触发垃圾收集的发生。 可能根本没有引用计数(与 Jython 一样)。

从评论来看,您担心的似乎是内存泄漏的可能性。您显示的代码不能 导致 内存泄漏 - 但它也不能修复其他地方导致的内存泄漏。在 Python 中,与一般的垃圾收集语言一样,内存泄漏的发生是因为对象保持对彼此的引用 不需要 。但是通常没有引用的“所有权”或“转让”的概念——你需要做的就是而不是做一些事情,比如“维护一个曾经创建的每个实例的列表”,而不是 a)一个很好的理由和 b) 当您想忘记它们时从列表中取出实例的方法。

局部变量,但是,根据定义,不能将对象的生命周期延长到局部范围之外。

免责声明:大部分信息来自评论。所以感谢每一位参与讨论的人。

何时删除对象通常是一个实现细节。 我将参考基于引用计数的 CPython。我 运行 使用 CPython 3.10.0 的代码示例。

  • 当引用计数为零时,对象被删除。
  • 从函数返回会删除所有本地引用。
  • 为新值指定名称会减少旧值的引用计数
  • 传递一个局部变量会增加引用计数。引用在栈上(帧)
  • 从函数返回将从堆栈中删除引用

最后一点甚至对 f(g()) 这样的临时引用也有效。最后对g()的引用被删除,当freturns(假设g没有在某处保存引用)see here

所以对于问题中的示例:

def myfn():
    big_obj = BigObj() # reference 1                     
    result = consume(big_obj) # reference 2 on the stack frame for  
                              # consume. Not yet counting any 
                              # reference inside of consume
                              # after consume returns: The stack frame 
                              # and reference 2 are deleted. Reference  
                              # 1 remains
    return result             # myfn returns reference 1 is deleted. 
                              # BigObj is deleted
def consume(big_obj):
    pass # consume is holding reference 3

如果我们将其更改为:

def myfn():
    return consume(BigObj()) # reference is still saved on the stack 
                             # frame and will be only deleted after  
                             # consume returns
def consume(big_obj):
    pass # consume is holding reference 2

如何可靠地检查对象是否已删除?

您不能依赖 gc.get_objects()。 gc 用于检测和回收引用循环。不是每个引用都被 gc 跟踪。 您可以创建弱引用并检查引用是否仍然有效。

class BigObj:
    pass

import weakref
ref = None

def make_ref(obj):
    global ref
    ref = weakref.ref(obj)
    return obj

def myfn():
    return consume(make_ref(BigObj()))

def consume(obj):
    obj = None # remove to see impact on ref count
    print(sys.getrefcount(ref()))
    print(ref()) # There is still a valid reference. It is the one from consume stack frame

myfn()

如何传递对函数的引用并删除调用函数中的所有引用?

您可以将引用装箱,传递给函数并从函数内部清除装箱的引用:

class Ref:
    def __init__(ref):
        self.ref = ref
    def clear():
        self.ref = None

def f1(ref):
    r = ref.ref
    ref.clear()

def f2():
    f1(Ref(object())