9.1 建立模組的循環依賴
假設你有兩個文件,a.py 和 b.py,每個都以以下方式導入另一個:
在 a.py 中:
import b
def f():
return b.x
print(f())
在 b.py 中:
import a
x = 1
def g():
print(a.f())
首先嘗試導入 a.py:
import a
# 1
運行得很順利。這可能會讓你感到驚訝。畢竟,模組相互循環導入,這可能應該是個問題,對吧?
答案是,簡單的模組循環導入本身在 Python 中不是問題。如果模組已被導入,Python 足夠聰明,不會嘗試重複導入它。然而,根據每個模組試圖訪問另一個模組中定義的函數或變數的時間點,你可能確實會遇到問題。
因此,回到我們的例子,當我們導入 a.py 時,它在導入 b.py 時沒有問題,因為 b.py 在它被導入時不需要 a.py 中定義的任何東西。b.py 中唯一的 a 引用是 a.f() 的調用。但是這個調用在 g() 中,而在 a.py 或 b.py 中都沒有調用 g()。所以一切運行良好。
但是如果我們嘗試導入 b.py(在不先導入 a.py 的情況下,即是說)會發生什麼?
import b
Traceback (most recent call last):
File "<stdin>", line 1, in
File "b.py", line 1, in
import a File "a.py", line 6, in
print(f()) File "a.py", line 4, in f return b.x AttributeError: 'module' object has no attribute 'x'
這裡的問題是,在導入 b.py 的過程中,它試圖導入 a.py,然後它調用了 f(),這又試圖訪問 b.x。但 b.x 尚未被定義。因此引發 AttributeError。
至少,一種解決此問題的方法相當簡單。只需更改 b.py,以便在 g() 中導入 a.py:
x = 1
def g():
import a # 只有在 g() 被調用時才會評估
print(a.f())
現在當我們導入它時,一切正常:
import b
b.g()
# 1 第一次列印是因為模組 'a' 在結尾處呼叫 'print(f())'
# 1 第二次列印,這是我們對 'g()' 的調用
9.2 與 Python 標準庫模組名稱的重合
Python 的一大優勢就是它自帶的豐富模組。但結果是,如果你不小心,會發現你的模組名可能與隨 Python 提供的標準庫中的模組名稱相同(例如,你的代碼中可能有名為 email.py 的模組,這會與標準庫中的模組發生衝突)。
這可能會導致嚴重的問題。例如,如果某些模組嘗試導入標準庫中的模組版本,而你的專案中有相同名稱的模組,它會錯誤地導入你的模組而不是標準庫中的模組。
因此應該小心,避免使用與 Python 標準庫模組同名名稱。在專案中更改模組名稱要比提交請求更改標準庫中模組名稱然後等待批準簡單得多。
9.3 異常的可見性
考慮以下文件 main.py:
import sys
def bar(i):
if i == 1:
raise KeyError(1)
if i == 2:
raise ValueError(2)
def bad():
e = None
try:
bar(int("1"))
except KeyError as e:
print('key error')
except ValueError as e:
print('value error')
print(e)
bad()
看似一切正常,代碼應該工作,讓我們看看它會輸出什麼:
$ python main.py 1
Traceback (most recent call last):
File "C:\Projects\Python\TinderBolt\main.py", line 19, in <module>
bad()
File "C:\Projects\Python\TinderBolt\main.py", line 17, in bad
print(e)
^
UnboundLocalError: cannot access local variable 'e' where it is not associated with a value
這裡剛剛發生了什麼?問題在於,在 Python 中,異常區塊中的對象在區塊外不可用。(其原因是,否則區塊中的對象將在內存中保持,直到垃圾收集器啟動並刪除對它們的引用)。
避免此問題的一種方法是將異常區塊對象的引用保存在區塊之外,以便它保持可用。以下是使用此技術的前一示例的版本,從而使代碼可運行:
import sys
def bar(i):
if i == 1:
raise KeyError(1)
if i == 2:
raise ValueError(2)
def good():
exception = None
try:
bar(int("1"))
except KeyError as e:
exception = e
print('key error')
except ValueError as e:
exception = e
print('value error')
print(exception)
good()
9.4 誤用 __del__ 方法
當解釋器刪除對象時,它會檢查 - 該對象是否有 __del__ 函數,如果有,則在刪除對象前調用它。當你希望你的對象清理一些外部資源或緩存時,這非常方便。
假設你有一個文件 mod.py 看起來像這樣:
import foo
class Bar(object):
...
def __del__(self):
foo.cleanup(self.myhandle)
你嘗試從另一個文件 another_mod.py 做這樣的事情:
import mod
mybar = mod.Bar()
然後你會得到一個可怕的 AttributeError。
為什麼?因為,如這裡所述,當解釋器結束時,所有模組的全局變數都被設置為 None。因此,在上述示例中的 __del__ 調用時,名稱 foo 已經設置為 None。
解決這個“星標問題”的方法是使用 atexit.register()} 特殊方法。因此,當你的程式結束時(即是正常退出時),你的 handle 在解釋器結束前被刪除。
考慮到這一點,以上 mod.py 代碼的修正版本可能如下所示:
import foo
import atexit
def cleanup(handle):
foo.cleanup(handle)
class Bar(object):
def __init__(self):
...
atexit.register(cleanup, self.myhandle)
這種實現提供了一種簡單而可靠的方式來在程式正常結束後調用任何必要的清理。顯然,決定如何處理與名稱 self.myhandle 關聯的對象的負責權仍然屬於 foo.cleanup,但我想你已經理解了這個概念。
GO TO FULL VERSION