2014-10-02 71 views
8

我正在Python 2.7中工作,我喜歡那個讓我困惑的問題。爲什麼要爲python對象設置綁定方法創建循環引用?

這是最簡單的例子:

>>> class A(object): 
    def __del__(self): 
     print("DEL") 
    def a(self): 
     pass 

>>> a = A() 
>>> del a 
DEL 

這是確定的預期一樣......我現在試圖改變對象aa()方法,什麼發生的是更改後它,我不能刪除a更多:

>>> a = A() 
>>> a.a = a.a 
>>> del a 

只是做一些檢查,我之前和分配之後打印a.a參考

>>> a = A() 
>>> print a.a 
<bound method A.a of <__main__.A object at 0xe86110>> 
>>> a.a = a.a 
>>> print a.a 
<bound method A.a of <__main__.A object at 0xe86110>> 

最後我用objgraph模塊,試圖理解爲什麼對象沒有被釋放:

>>> b = A() 
>>> import objgraph 
>>> objgraph.show_backrefs([b], filename='pre-backref-graph.png') 

pre-backref-graph.png

>>> b.a = b.a 
>>> objgraph.show_backrefs([b], filename='post-backref-graph.png') 

post-backref-graph.png

正如你可以在post-backref-graph.png圖像有見是b中的__self__引用,因爲自我r對我沒有意義實例方法的推斷應該被忽略(就像在賦值之前那樣)。

有人可以解釋爲什麼這種行爲,我該如何解決它?

回答

3

當你寫a.a,它有效地運行:

A.a.__get__(a, A) 

因爲你沒有訪問預結合的方法,但類的方法正被 勢必在運行時。

當你

a.a = a.a 

你有效的「緩存」的結合方法的行爲。由於綁定方法具有對象的引用(顯然,因爲它必須將self傳遞給該函數),因此會創建一個循環引用。


所以我喜歡建模您的問題:

class A(object): 
    def __del__(self): 
     print("DEL") 
    def a(self): 
     pass 

def log_all_calls(function): 
    def inner(*args, **kwargs): 
     print("Calling {}".format(function)) 

     try: 
      return function(*args, **kwargs) 
     finally: 
      print("Called {}".format(function)) 

    return inner 

a = A() 
a.a = log_all_calls(a.a) 

a.a() 

您可以使用弱引用按需綁定內lof_all_calls,如:

import weakref 

class A(object): 
    def __del__(self): 
     print("DEL") 
    def a(self): 
     pass 

def log_all_calls_weakmethod(method): 
    cls = method.im_class 
    func = method.im_func 
    instance_ref = weakref.ref(method.im_self) 
    del method 

    def inner(*args, **kwargs): 
     instance = instance_ref() 

     if instance is None: 
      raise ValueError("Cannot call weak decorator with dead instance") 

     function = func.__get__(instance, cls) 

     print("Calling {}".format(function)) 

     try: 
      return function(*args, **kwargs) 
     finally: 
      print("Called {}".format(function)) 

    return inner 

a = A() 
a.a = log_all_calls_weakmethod(a.a) 

a.a() 

這實在是太醜了,所以我寧願抽出來做一個weakmethod裝飾:

import weakref 

def weakmethod(method): 
    cls = method.im_class 
    func = method.im_func 
    instance_ref = weakref.ref(method.im_self) 
    del method 

    def inner(*args, **kwargs): 
     instance = instance_ref() 

     if instance is None: 
      raise ValueError("Cannot call weak method with dead instance") 

     return func.__get__(instance, cls)(*args, **kwargs) 

    return inner 

class A(object): 
    def __del__(self): 
     print("DEL") 
    def a(self): 
     pass 

def log_all_calls(function): 
    def inner(*args, **kwargs): 
     print("Calling {}".format(function)) 

     try: 
      return function(*args, **kwargs) 
     finally: 
      print("Called {}".format(function)) 

    return inner 

a = A() 
a.a = log_all_calls(weakmethod(a.a)) 

a.a() 

完成!


FWIW,不僅是Python 3。4沒有這些問題,它也有爲您預製的WeakMethod

+0

好的......有一種方法可以避免這種情況?我應該緩存一些方法並在稍後恢復方法:這可能嗎? – 2014-10-02 09:49:12

+0

這取決於你想要做什麼。 – Veedrac 2014-10-02 09:49:41

+0

好的,我找到了解決方案:aa = types.MethodType(Aa,a,A) – 2014-10-02 09:54:37

4

Veedrac關於保持引用實例的綁定方法的答案只是答案的一部分。 CPython中的垃圾回收器知道如何檢測和處理循環引用 - 除非某些對象是週期的一部分具有__del__方法,如下https://docs.python.org/2/library/gc.html#gc.garbage提到:有__del__()方法,是一個參考週期 的一部分

對象導致整個參考週期無法收集,包括 對象不一定在循環中,但只能從循環中訪問。 Python不會自動收集這些循環,因爲一般而言, Python不可能猜測運行方法的安全順序。 (...)通常情況下,最好避免使用__del__()方法創建包含對象的循環,在這種情況下可以檢查 垃圾,以驗證沒有創建這樣的循環 。

IOW:刪除您的__del__方法,你應該沒問題。

編輯:WRT /您的評論:

我用它的對象爲功能a.a = functor(a.a)。當測試 完成後,我想用原來的方法替換函子。

然後將溶液是簡單明瞭:

a = A() 
a.a = functor(a.a) 
test(a) 
del a.a 

除非你明確地將其綁定,a已經沒有「A」的實例屬性附加傷害,所以它擡頭的類並返回一個新的method實例(請參閱https://wiki.python.org/moin/FromFunctionToMethod瞭解更多信息)。然後這個method實例被調用,並且(通常)被丟棄。

+0

這不是一個解決方案....我需要__del__方法(原因在這裏超出了範圍),這個問題是唯一一個,我不知道如何解決其他弱引用效果不錯 – 2014-10-02 10:51:19

+1

,它實際上回答了你的問題:「有人可以解釋爲什麼這種行爲,我該如何解決它?」 - 你沒有提到你需要'__del__',你的代碼段並不意味着它有任何真正的用途;)現在,如果你看看我指出的鏈接,那麼有更多的話題...... – 2014-10-02 11:03:00

+0

由於寫在鏈接中你發佈了最好的方法是不創建循環,我問的是「我不明白爲什麼該循環出生,如果有辦法不創建那種循環」......但刪除__del__不要刪除循環,只是循環的副作用:) – 2014-10-02 11:30:41

1

至於爲什麼Python這樣做。技術上全部對象包含循環引用,如果他們有方法。但是,如果垃圾收集器必須對對象方法進行顯式檢查以確保釋放對象不會導致問題,則垃圾收集將花費更長時間。像這樣Python將方法與對象的__dict__分開存儲。因此,當您編寫a.a = a.a時,您在該對象的a字段中使用了自己的方法。因此,對於防止對象被正確釋放的方法有明確的參考。

問題的解決方案並不打算保留原始方法的「緩存」,只需在完成後刪除陰影變量。這將淡化該方法並使其再次可用。

>>> class A(object): 
...  def __del__(self): 
...   print("del") 
...  def method(self): 
...   print("method") 
>>> a = A() 
>>> vars(a) 
{} 
>>> "method" in dir(a) 
True 
>>> a.method = a.method 
>>> vars(a) 
{'method': <bound method A.method of <__main__.A object at 0x0000000001F07940>>} 
>>> "method" in dir(a) 
True 
>>> a.method() 
method 
>>> del a.method 
>>> vars(a) 
{} 
>>> "method" in dir(a) 
True 
>>> a.method() 
method 
>>> del a 
del 

這裏vars顯示了在一個對象的屬性__dict__。請注意0​​如何不包含對自身的引用,即使a.__dict__有效。 dir產生從給定對象可到達的所有屬性的列表。在這裏,我們可以看到對象上的所有屬性和方法以及它的類和它們的基礎的所有方法和屬性。這表明a的綁定方法被存儲在獨立於a的屬性存儲位置。

+0

感謝您的回答,但真正的問題不是」爲什麼對象沒有被銷燬?「但爲什麼'a.a = a.a'創建一個循環引用。如果刪除'__del__'覆蓋並嘗試在作業前後繪製圖表,則會發現圖形不同,第二個圖形具有循環引用。 – 2014-10-03 13:50:56

+0

誤解了這個問題。我已經完全改變了我的答案,並提出了一個新建議,並在此過程中自己學習了一些東西 – Dunes 2014-10-03 16:13:24

+0

THX:這正是我所說的「爲什麼要爲python對象設置綁定方法創建循環引用?」。 Veedrac讓我有辦法處理與此有關的一些問題,但用我真正理解的答案。 – 2014-10-03 20:36:55

相關問題