Python - 如何调整弱代理对象列表

5 投票
1 回答
1149 浏览
提问于 2025-04-18 06:35

所以,weakref.proxy 对象在检查引用是否有效、解引用(也就是获取它指向的对象)或者其他方面,跟 weakref.ref 对象完全不一样。:P

不过,它们还是有用的,比如用来维护一个响应事件的对象列表。因为你不需要先解引用就能调用这些弱引用对象的方法(这跟 weakref.ref 对象不同,后者需要先调用一下),所以在进行成千上万次操作时,使用 proxy 对象可以节省一些时间。然而,一旦它们指向的对象消失,识别这些“死”的引用并清理它们似乎就比较困难了。举个例子 -

>>> mylist    #note the object at mylist[1] is already dead...
[<weakproxy at 0x10ccfe050 to A at 0x10ccf9f50>, <weakproxy at 0x10ccfe1b0 to NoneType at 0x10cb53538>]
>>> for i in mylist[:]:
...     try:
...             getattr(i, 'stuff')   #the second arg could be any sentinel;
...                                   #I just want to hit a ReferenceError
...     except AttributeError:
...             pass
...     except ReferenceError:
...             mylist.remove(i)
... 
Traceback (most recent call last):
  File "<stdin>", line 7, in <module>
ReferenceError: weakly-referenced object no longer exists

所以它在那里活着,只是不能像变量那样直接引用或传递,因为这样会导致 Python “解引用”,并与它弱引用的对象进行交互。(或者是其他什么。)

到目前为止,我发现的唯一比较可靠的方法是通过 try/except 来处理,但这感觉有点绕。

for i, obj in enumerate(mylist):
    try:
        obj.__hash__
    except ReferenceError:
        del mylist[i]

这样做是有效的,但 Python 通常是一个“所有事情都有一个正确答案”的语言,而这似乎是一个比较复杂的解决方案 - 我可能错了,但如果列表足够大,这样复制列表会不会引发问题呢?

不幸的是,这似乎是我能想到的唯一不涉及类型检查或其他麻烦的潜在解决方案,但我猜我在 weakref 的文档中可能漏掉了关于如何正确处理 weakref.proxy 对象的内容。感觉上,确保 weakref.proxy 不是在 特殊 情况下,所以使用 try/except 是一种一次性的工具。

所以,如果我对这里使用 try/except 的假设是正确的,是否有更好的方法来识别“死”的 weakref.proxy 对象呢?

编辑:我已经接受了一个答案,所以谢谢你们 - try/except 似乎是唯一可接受的解决方案。

至于我为什么不使用 WeakSets - 让我展示一个简单的演示,这最终让我选择了使用代理对象。

>>> from weakref import WeakSet, proxy
>>> import cProfile
>>> class A(object):
...     def __init__(self):
...             self.x = 0
...     def foo(self, v=1):
...             self.x += v
... 
>>> Stick = A()
>>> Dave = A()
>>> Jupiter = A()
>>> ##just a list of objects, no fancy
>>> one_group = [Stick, Dave, Jupiter]
>>> cProfile.run("for i in xrange(0, 2**16): [x.foo(i) for x in one_group]")
         196610 function calls in 0.136 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
   196608    0.049    0.000    0.049    0.000 <stdin>:4(foo)
        1    0.087    0.087    0.136    0.136 <string>:1(<module>)
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}


>>> Stick.x
2147450880
>>> Dave.x
2147450880
>>> Jupiter.x
2147450880

所以我们知道它有效,并且在 65,000 次迭代中相当快。看起来不错;让我们看看 WeakSets。

>>> ##now a WeakSet of objects. should be ideal; but...
>>> two_group = WeakSet((Stick, Dave, Jupiter))
>>> cProfile.run("for i in xrange(0, 2**16): [x.foo(i) for x in two_group]")
         851970 function calls in 0.545 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
   196608    0.055    0.000    0.055    0.000 <stdin>:4(foo)
        1    0.158    0.158    0.545    0.545 <string>:1(<module>)
    65536    0.026    0.000    0.026    0.000 _weakrefset.py:16(__init__)
    65536    0.043    0.000    0.051    0.000 _weakrefset.py:20(__enter__)
    65536    0.063    0.000    0.095    0.000 _weakrefset.py:26(__exit__)
    65536    0.024    0.000    0.024    0.000 _weakrefset.py:52(_commit_removals)
   262144    0.159    0.000    0.331    0.000 _weakrefset.py:58(__iter__)
    65536    0.009    0.000    0.009    0.000 {method 'add' of 'set' objects}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
    65536    0.008    0.000    0.008    0.000 {method 'remove' of 'set' objects}

过早优化?不。:) 这大约慢了 4 倍。哎呀。

>>> ##now finally, a list of proxy objects
>>> three_group = [proxy(x) for x in one_group]
>>> cProfile.run("for i in xrange(0, 2**16): [x.foo(i) for x in three_group]")
         196610 function calls in 0.139 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
   196608    0.050    0.000    0.050    0.000 <stdin>:4(foo)
        1    0.089    0.089    0.139    0.139 <string>:1(<module>)
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

根据这些数据,我得出的结论是,维护一个必须正确标记为已释放的对象列表的最简单和最快的方法是使用代理对象。我认为,策略性地使用 try/except 作为“列表清理工具”将确保死掉的代理不会引发错误。为了方便起见,我可能会回到使用 weakref.ref 对象的方法,但使用代理对象的方式很有趣,因为它们似乎可以“直接”访问对象。

1 个回答

3

你的弱引用对象在执行 mylist.remove(i) 这一行时会再次引发 ReferenceError 异常,而不是在循环或访问时。这很可能是因为 Python 尝试在代理对象上使用 __eq__ 方法。

你的第二种方法使用了索引,这样就不会触发这个异常,所以在你的第一个循环中可以使用这种方法:

remove = set()
for index, proxy in enumerate(mylist):
    try:
        getattr(proxy, 'stuff')
    except AttributeError:
        pass
    except ReferenceError:
        remove.add(index)

mylist = [p for i, p in enumerate(mylist) if i not in remove]

示例:

>>> import weakref
>>> class Foo(object): pass
... 
>>> remove = set()
>>> mylist = [weakref.proxy(Foo())]  # instant dead proxy
>>> for index, proxy in enumerate(mylist):
...     try:
...         getattr(proxy, 'stuff')
...     except AttributeError:
...         pass
...     except ReferenceError:
...         remove.add(index)
... 
>>> remove
set([0])

如果你特别想要一个函数来检查对象是否仍然存在,可以使用:

def proxy_live(p)
    try:
        bool(p)
    except ReferenceError:
        return False
    return True

但要注意,布尔测试本身可能会导致对象被删除。如果被代理的对象在访问属性时触发了 __nonzero____len__ 方法并引发删除,那么再加上线程的因素,就可能导致竞争条件,这样上述函数会返回 True,但你仍然需要考虑对该对象的操作可能会引发 ReferenceError

如果顺序不是很重要,我建议在这里使用一个 weakref.WeakSet() 对象;在遍历这些对象时,你会得到活着的对象;已经去掉了无效的引用,避免了竞争条件的风险。

撰写回答