1. 程式人生 > >在Python中安全使用解構函式

在Python中安全使用解構函式

作者: Eli Bendersky

原文連結:https://eli.thegreenplace.net/2009/06/12/safely-using-destructors-in-python

本文適用於Python 2.52.6——如果你看到Python 3有任何不同,請讓我知道。

C++中,解構函式是一個非常重要的概念,它們是RAIIresource acquisition is initialization)的一個基本成分——在丟擲異常的程式中,基本上是編寫涉及資源分配與釋放程式碼僅有的安全方式。

Python中,解構函式的需求少得多,因為Python有進行記憶體管理的垃圾收集器。不過,雖然記憶體是最常見的分配資源,它不是唯一的。還有要關閉的套接字與資料庫連線,要重新整理的檔案、緩衝與快取,以及在一個物件用完時需要釋放的另外幾種資源。

因此Python有解構函式的概念——__del__方法。出於某個原因,Python社群裡的許多人認為__del__是邪惡的,不應該使用。不過,簡單grep標準庫顯示,在我們使用且喜歡的類中使用了數以十計的__del__,那麼要點在哪裡?在本文中,我將嘗試澄清它(首先是為我自己),何時應該使用__del__,以及如何使用。

簡單的例子程式碼

首先一個基本例子:

class FooType(object):

    def __init__(self, id):

        self

.id = id

        print self.id, 'born'

 

    def __del__(self):

        print self.id, 'died'

 

ft = FooType(1)

這打印出:

1 born

1 died

現在,回憶由於一個引用計數垃圾收集器的使用,Python在一個物件退出作用域時,不會清理它。在該物件的最後一個引用退出作用域時,才將清理它。下面是一個展示:

class FooType(object):

    def __init__(self, id):

        self.id = id

        print self.id, 'born'

 

    def __del__(self):

        print self.id, 'died'

 

def make_foo():

    print 'Making...'

    ft = FooType(1)

    print 'Returning...'

    return ft

 

print 'Calling...'

ft = make_foo()

print 'End...'

這打印出:

Calling...

Making...

1 born

Returning...

End...

1 died

在程式終止時呼叫了這個解構函式,不是在ft退出make_foo裡的作用域時。

解構函式的替代品

在我繼續之前,一個合適的揭露:對資源的管理,Python提供了比解構函式更好的方法——上下文(context)。我不會把這變成上下文的一個教程,但你應該熟悉with語句,以及可以在內部使用的物件。例如,處理檔案寫入的最好方法是:

with open('out.txt', 'w') as of:

    of.write('222')

這確保在退出with內部的程式碼塊時,該檔案被正確關閉,即使丟擲異常。注意這展示了一個標準的上下文管理器。另一個是threading.lock,它返回一個非常適合在一個with語句中使用的上下文管理器。更多細節,閱讀PEP 343

雖然推薦,with不總是適用的。例如,假設你有一個封裝了某種資料庫的物件,在該物件生命期結束時,必須提交併關閉該資料庫。現在,假定該物件應該是某種大且複雜的類(比如一個GUI會話,或者一個MVC模型類)的一個成員變數。父親在別的方法中不時地與該DB物件互動,因此使用with是不現實的。所需要的是一個起作用的解構函式。

解構函式何處走偏

為了解決我在上一段展示的用例,你可以採用__del__解構函式。不過,知道這不總是工作良好是重要的。引用計數垃圾收集器的死對頭是迴圈引用。下面是一個例子:

class FooType(object):

    def __init__(self, id, parent):

        self.id = id

        self.parent = parent

        print 'Foo', self.id, 'born'

 

    def __del__(self):

        print 'Foo', self.id, 'died'

 

class BarType(object):

    def __init__(self, id):

        self.id = id

        self.foo = FooType(id, self)

        print 'Bar', self.id, 'born'

 

    def __del__(self):

        print 'Bar', self.id, 'died'

 

b = BarType(12)

輸出:

Foo 12 born

Bar 12 born

噢……發生了什麼?解構函式在哪裡?下面是Python文件在這件事上的陳述:

在啟用了可選的迴圈檢測器(預設開啟)時,檢測垃圾的迴圈引用,但僅在不涉及Python層面的__del__()方法時,才能被清理。

Python不知道銷燬彼此持有迴圈引用的物件的安全次序,因此作為一個設計決策,它只是不對這樣的方法呼叫解構函式!

那麼,現在怎麼辦?

因為其缺陷,我們不應該使用解構函式嗎?我非常吃驚地看到許多Python支持者認為這樣,並建議使用顯式的close方法。但我不同意——顯式的close方法不那麼安全,因為它們容易忘記呼叫。另外,在發生異常時(在Python裡,它們隨時出現),管理顯式關閉變得非常困難且煩人。

我確實認為解構函式可以且應該在Python裡被安全地使用。帶著幾分小心,這絕對可能。

首先以及最重要的,注意到合理的迴圈引用是罕見的。我故意說合理的(justified)——出現迴圈引用的大量使用是壞的設計以及有漏洞抽象的樣本。

作為一個經驗規則,資源儘可能由最底層的物件持有。不要在你的GUI會話裡直接持有一個DB資源。使用一個物件封裝這個DB連線,並在解構函式裡安全地關閉它。DB物件沒有理由持有你程式碼裡其他物件的引用。如果這樣——它違反了幾個好的設計實踐。

有時,在複雜程式碼中,依賴性注入(dependency injection)有助於防止迴圈引用,不過即使在你發現需要一個真迴圈引用的罕見情形裡,也存在解決方案。Python為此提供了weakref模組。文件很快揭示,這正是我們這裡所需要的:

一個物件的弱引用不足以保持物件存活:當一個被引用物件僅有的引用是弱引用時,垃圾收集可以自由地銷燬這個被引用物件,併為其他物件重用其記憶體。弱引用的主要使用是實現快取或持有大物件的對映,其中期望大物件不僅僅因為出現在快取或對映中,而被保持存活。

下面是用weakref重寫的前面的例子:

import weakref

 

class FooType(object):

    def __init__(self, id, parent):

        self.id = id

        self.parent = weakref.ref(parent)

        print 'Foo', self.id, 'born'

 

    def __del__(self):

        print 'Foo', self.id, 'died'

 

class BarType(object):

    def __init__(self, id):

        self.id = id

        self.foo = FooType(id, self)

        print 'Bar', self.id, 'born'

 

    def __del__(self):

        print 'Bar', self.id, 'died'

 

b = BarType(12)

現在我們得到希望的結果:

Foo 12 born

Bar 12 born

Bar 12 died

Foo 12 died

這個例子裡的小改動是,在FooType建構函式裡,我使用weakref.refparent引用賦值。這是一個弱引用,因此它不會真正建立一個環。因此GC看不到環,它銷燬了這兩個物件。

結論

Python有經由__del__方法的完美、可用的物件解構函式。對絕大多數用例,它工作良好,但堵塞在迴圈引用處。不過,迴圈引用通常是壞設計的一個跡象,它們很少是合理的。對極少數使用了合理的迴圈引用的用例裡,使用弱引用很容易打破迴圈,Pythonweakref模組裡提供弱引用。

參考文獻

在準備本文時,某些有用的連結: