【问题标题】:When is the reference count for a local variable in a python function decreased?python函数中局部变量的引用计数何时减少?
【发布时间】:2021-12-12 05:58:00
【问题描述】:

我有以下功能:

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

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

  1. consume(big_obj) 被调用时(因为之后在 myfn 中没有引用 big_obj)
  2. 函数返回时
  3. 有点,我还没有

将最后一行更改为:

return consume(big_obj)

编辑(对 cme​​ts 的说明):

  • 在函数返回之前存在局部变量
  • 可以使用 del 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 提供的信息比您预期的要多,因为它也包含一个引用。

【问题讨论】:

  • 变量big_obj 在函数返回之前仍然存在,即使没有更多的代码行可以访问它。要影响它的生命周期,你唯一能做的就是del big_obj,但如果函数无论如何都将要返回,那么这样做是没有意义的。 (如果要在函数中执行进一步的内存密集型操作,这可能非常有用,但不再需要 big_obj。)
  • 是否有可能调用consume(big_obj),以便删除myfn 中的引用并保留consume fn 中的引用。所以基本上类似于 C++ 中的移动操作?
  • 你需要这个做什么?
  • 我有一个懒惰的 SingleLinkedList。大致如下:(value, next_fn)列表的下一项是一个函数,当第一次访问下一项时会调用该函数。下一项替换为 next_fn 的返回值,即另一个 (value, next_fn)None。所以big_obj 持有对列表头部的引用,只要引用了 big_obj,所有生成的项目也都保存在内存中......
  • 试试consume(BigObj())?无论如何,这一切都将依赖于实现细节,没有语言保证当对象的引用计数达到零时会立即回收(尽管在 CPython 中是这样)

标签: python local-variables reference-counting


【解决方案1】:

big_obj 的引用计数何时减少

big_obj 没有引用计数。变量没有引用计数。 价值观做。

big_obj = BigObj()

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

函数返回时

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

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


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

从 cmets 看来,您担心的是内存泄漏的可能性。您显示的代码不会导致内存泄漏 - 但它也不能修复其他地方造成的内存泄漏。在 Python 中,就像在一般的垃圾收集语言中一样,内存泄漏的发生是因为对象保持对彼此的引用,而这是不需要的。但是通常没有引用的“所有权”或“转移”的概念 - 您需要做的只是在没有充分理由的情况下执行“维护曾经创建的每个实例的列表”之类的事情b) 当你想忘记它们时,一种将实例从列表中删除的方法。

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

【讨论】:

  • 我知道 big_obj 没有引用计数,但 BigObj() 的值有。我在这个问题上很草率。你是对的,我担心引用导致的内存泄漏会阻止引用计数降至零。
【解决方案2】:

免责声明:大部分信息来自 cmets。因此,感谢每一位参与讨论的人。

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

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

最后一点甚至对于像f(g()) 这样的临时引用也是有效的。最后对g()的引用被删除,当f返回时(假设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())

【讨论】:

  • # reference is still saved on the stack 啊是的,当然
  • 这让我很头疼。我没有意识到这一点。我相信像移动语义或复制省略会发生在临时工身上。你知道为什么额外的引用会保存在堆栈上吗?是否防止在进入函数之前将引用计数降至零?我不知道这是否有意义。引用也保存在本地字典中。
【解决方案3】:

变量在 Python 中具有函数作用域,因此在函数返回之前不会删除它们。据我所知,您不能从函数外部破坏对该函数中局部变量的引用。我在示例代码中添加了一些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 被打印后,在我们强制垃圾回收发生后,大对象仍然存在,即使在该点之后函数中没有对该变量的引用。在函数正确返回后强制垃圾回收确定对大对象的引用计数为 0,因此它会清理该对象。注意:正如下面的 cmets 所指出的,已删除对象的 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

【讨论】:

  • “我在示例代码中添加了一些 gc 调用来测试这一点。” gc.collect 与此处无关。请理解gc 并不会真正影响这里的对象
  • 注意,如果你使用x = object(); big_id = id(x); any(id(item) == big_id for item in gc.get_objects()),你会得到False,而使用id(x) = big_id一开始就充满了问题,因为它可能会给你带来误报,因为ID只能保证是在对象的整个生命周期中是唯一的,id 可以并且经常被重复使用
  • 虽然,我认为对于用户定义的对象,any( ....) 将是 True。虽然这又是一个实现细节。
  • @juanpa.arrivillaga。 id(x) == big_id 不能返回假阴性。获得False 结果意味着对象被真正删除,这就是输出显示的内容。
  • @rchome 我认为一个可靠的测试是创建一个全局弱引用并检查该弱引用是否仍然有效。
猜你喜欢
  • 1970-01-01
  • 2022-08-02
  • 2018-04-16
  • 2012-01-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-12-28
  • 1970-01-01
相关资源
最近更新 更多