導入時和運行時比較
為了正確地做元編程,你必須知道 Python 解釋器什么時候計算各個代碼
塊。Python 程序員會區分“導入時”和“運行時”,不過這兩個術語沒有嚴
格的定義,而且二者之間存在著灰色地帶。在導入時,解釋器會從上到
下一次性解析完 .py 模塊的源碼,然后生成用于執行的字節碼。如果句
法有錯誤,就在此時報告。如果本地的 __pycache__
文件夾中有最新
的 .pyc 文件,解釋器會跳過上述步驟,因為已經有運行所需的字節碼
了。
編譯肯定是導入時的活動,不過那個時期還會做些其他事,因為 Python
中的語句幾乎都是可執行的,也就是說語句可能會運行用戶代碼,修改
用戶程序的狀態。尤其是 import 語句,它不只是聲明 ,在進程中首
次導入模塊時,還會運行所導入模塊中的全部頂層代碼——以后導入相
同的模塊則使用緩存,只做名稱綁定。那些頂層代碼可以做任何事,包
括通常在“運行時”做的事,例如連接數據庫。 因此,“導入時”與“運行
時”之間的界線是模糊的:import 語句可以觸發任何“運行時”行為。
在前一段中我寫道,導入時會“運行全部頂層代碼”,但是“頂層代碼”會
經過一些加工。導入模塊時,解釋器會執行頂層的 def 語句,可是這么
做有什么作用呢?解釋器會編譯函數的定義體(首次導入模塊時),把
函數對象綁定到對應的全局名稱上,但是顯然解釋器不會執行函數的定
義體。通常這意味著解釋器在導入時定義頂層函數,但是僅當在運行時
調用函數時才會執行函數的定義體。
對類來說,情況就不同了:在導入時,解釋器會執行每個類的定義體,
甚至會執行嵌套類的定義體。執行類定義體的結果是,定義了類的屬性
和方法,并構建了類對象。從這個意義上理解,類的定義體屬于“頂層
代碼”,因為它在導入時運行。
上述說明模糊又抽象,下面通過練習理解各個時期所做的事情。
理解計算時間的練習
假設在 evaltime.py 腳本中導入了 evalsupport.py 模塊。這兩個模塊調用
了幾次 print 函數,打印 <[N]> 格式的標記,其中 N 是數字。下述兩
個練習的目標是,確定各個調用在何時執行。
那兩個模塊的代碼在示例 21-6 和示例 21-7 中。先別運行代碼,拿出紙
和筆,按順序寫出下述兩個場景輸出的標記。
場景 1
在 Python 控制臺中以交互的方式導入 evaltime.py 模塊:
>> import evaltime
場景 2
在命令行中運行 evaltime.py 模塊:
$ python3 evaltime.py
示例 21-6 evaltime.py:按順序寫出輸出的序號標記 <[N]>
from evalsupport import deco_alpha
print('<[1]> evaltime module start')
class ClassOne():print('<[2]> ClassOne body')def __init__(self):print('<[3]> ClassOne.__init__')def __del__(self):print('<[4]> ClassOne.__del__')def method_x(self):print('<[5]> ClassOne.method_x')
class ClassTwo(object):print('<[6]> ClassTwo body')@deco_alpha
class ClassThree():print('<[7]> ClassThree body')def method_y(self):print('<[8]> ClassThree.method_y')
class ClassFour(ClassThree):print('<[9]> ClassFour body')def method_y(self):print('<[10]> ClassFour.method_y')
if __name__ == '__main__':print('<[11]> ClassOne tests', 30 * '.')one = ClassOne()one.method_x()print('<[12]> ClassThree tests', 30 * '.')three = ClassThree()three.method_y()print('<[13]> ClassFour tests', 30 * '.')four = ClassFour()four.method_y()print('<[14]> evaltime module end')
示例 21-7 evalsupport.py:evaltime.py 導入的模塊
print('<[100]> evalsupport module start')
def deco_alpha(cls):print('<[200]> deco_alpha')def inner_1(self):print('<[300]> deco_alpha:inner_1')cls.method_y = inner_1return cls
class MetaAleph(type):print('<[400]> MetaAleph body')def __init__(cls, name, bases, dic):print('<[500]> MetaAleph.__init__')def inner_2(self):print('<[600]> MetaAleph.__init__:inner_2')cls.method_z = inner_2
print('<[700]> evalsupport module end')
場景1的解答
在 Python 控制臺中導入 evaltime.py 模塊后得到的輸出如示例 21-8
所示。
示例 21-8 場景 1:在 Python 控制臺中導入 evaltime 模塊
>>> import evaltime
<[100]> evalsupport module start ?
<[400]> MetaAleph body ?
<[700]> evalsupport module end
<[1]> evaltime module start
<[2]> ClassOne body ?
<[6]> ClassTwo body ?
<[7]> ClassThree body
<[200]> deco_alpha ?
<[9]> ClassFour body
<[14]> evaltime module end ?
? evalsupport 模塊中的所有頂層代碼在導入模塊時運行;解釋
器會編譯 deco_alpha 函數,但是不會執行定義體。
? MetaAleph 類的定義體運行了。
? 每個類的定義體都執行了……
? ……包括嵌套的類。
? 先計算被裝飾的類 ClassThree 的定義體,然后運行裝飾器函
數。
? 在這個場景中,evaltime 模塊是導入的,因此不會運行 if
name == ‘main’: 塊。
對于場景 1,要注意以下幾點。
(1) 這個場景由簡單的 import evaltime 語句觸發。
(2) 解釋器會執行所導入模塊及其依賴(evalsupport)中的每個
類定義體。
(3) 解釋器先計算類的定義體,然后調用依附在類上的裝飾器函
數,這是合理的行為,因為必須先構建類對象,裝飾器才有類對象
可處理。
(4) 在這個場景中,只運行了一個用戶定義的函數或方法
——deco_alpha 裝飾器。
下面來看場景 2。
場景2的解答
運行 python3 evaltime.py 命令后得到的輸出如示例 21-9 所
示。
示例 21-9 場景 2:在 shell 中運行 evaltime.py
$ python3 evaltime.py
<[100]> evalsupport module start
<[400]> MetaAleph body
<[700]> evalsupport module end
<[1]> evaltime module start
<[2]> ClassOne body
<[6]> ClassTwo body
<[7]> ClassThree body
<[200]> deco_alpha
<[9]> ClassFour body ?
<[11]> ClassOne tests ..............................
<[3]> ClassOne.__init__ ?
<[5]> ClassOne.method_x
<[12]> ClassThree tests ..............................
<[300]> deco_alpha:inner_1 ?
<[13]> ClassFour tests ..............................
<[10]> ClassFour.method_y
<[14]> evaltime module end
<[4]> ClassOne.__del__ ?
? 目前為止,輸出與示例 21-8 相同。
? 類的標準行為。
? deco_alpha 裝飾器修改了 ClassThree.method_y 方法,因此
調用 three.method_y() 時會運行 inner_1 函數的定義體。
? 只有程序結束時,綁定在全局變量 one 上的 ClassOne 實例才
會被垃圾回收程序回收。
場景 2 主要想說明的是,類裝飾器可能對子類沒有影響。在示例
21-6 中,我們把 ClassFour 定義為 ClassThree 的子
類。ClassThree 類上依附的 @deco_alpha 裝飾器把 method_y 方
法替換掉了,但是這對 ClassFour 類根本沒有影響。當然,如果
ClassFour.method_y 方法使用 super(…) 調用
ClassThree.method_y 方法,我們便會看到裝飾器起作用,執行
inner_1 函數。
與此不同的是,如果想定制整個類層次結構,而不是一次只定制一
個類,使用下一節介紹的元類更高效。