天天看點

Python Import機制

最近在看《Python源碼剖析》,對Python内部運作機制比以前了解的更深入了,感覺自己有機會也可以做個小型的動态腳本語言了,呵呵,當然是吹牛了。目的當然不是創造一個動态語言,目的隻有一個:更好的使用Python。看到子產品導入那塊的時候,終于對子產品導入機制比較了解了,以防忘記特記錄下來。

子產品的搜尋路徑

子產品的搜尋路徑都放在了sys.path清單中,如果預設的sys.path中沒有含有自己的子產品或包的路徑,可以動态的加入(sys.path.apend)即可。下面是sys.path在Windows平台下的添加規則。

1、sys.path第一個路徑往往是主子產品所在的目錄。在互動環境下添加一個空項,它對應目前目錄。

2、如果PYTHONPATH環境變量存在,sys.path會加載此變量指定的目錄。

3、我們嘗試找到Python Home,如果設定了PYTHONHOME環境變量,我們認為這就是Python Home,否則,我們使用python.exe所在目錄找到lib/os.py去推斷Python Home。

如果我們确實找到了Python Home,則相關的子目錄(Lib、plat-win、lib-tk等)将以Python Home為基礎加入到sys.path,并導入(執行)lib/site.py,将site-specific目錄及其下的包加入。

如果我們沒有找到Python Home,則把系統資料庫Software/Python/PythonCore/2.5/PythonPath的項加入sys.path(HKLM和 HKCU合并後加入),但相關的子目錄不會自動添加的。

4、如果我們沒有找到Python Home,并且沒有PYTHONPATH環境變量,并且不能在系統資料庫中找到PythonPath,那麼預設相對路徑将加入(如:./Lib;./plat-win等)。

總結如下

當在安裝好的主目錄中運作Python.exe時,首先推斷Python Home,如果找到了PythonHome,系統資料庫中的PythonPath将被忽略;否則将系統資料庫的PythonPath加入。

如果PYTHONPATH環境變量存在,sys.path肯定會加載此變量指定的目錄。

如果Python.exe在另外的一個目錄下(不同的目錄,比如通過COM嵌入到其他程式),Python Home将不推斷,此時系統資料庫的PythonPath将被使用。

如果Python.exe不能發現他的主目錄(PythonHome),并且系統資料庫也沒有PythonPath,則将加入預設的相對目錄。

标準Import

Python中所有加載到記憶體的子產品都放在sys.modules。當import一個子產品時首先會在這個清單中查找是否已經加載了此子產品,如果加載了則隻是将子產品的名字加入到正在調用import的子產品的Local名字空間中。如果沒有加載則從sys.path目錄中按照子產品名稱查找子產品檔案,子產品檔案可以是py、pyc、pyd,找到後将子產品載入記憶體,并加入到sys.modules中,并将名稱導入到目前的Local名字空間。

可以看出了,一個子產品不會重複載入。多個不同的子產品都可以用import引入同一個子產品到自己的Local名字空間,其實背後的PyModuleObject對象隻有一個。

說一個容易忽略的問題,import隻能導入子產品,不能導入子產品中的對象(類、函數、變量等)。如一個子產品A(A.py)中有個函數getName,另一個子產品不能通過import A.getName将getName導入到本子產品,隻能用import A。如果想隻導入特定的類、函數、變量則用from A import getName即可。

嵌套Import

嵌套import,我分兩種情況,一種是:本子產品導入A子產品(import A),而A中又有import語句,會激活另一個import動作,如import B,而B子產品又可以import其他子產品,一直下去。

對這種嵌套比較容易了解,注意一點就是各個子產品的Local名字空間是獨立的,是以上面的例子,本子產品import A完了後本子產品隻能通路子產品A,不能通路B及其他子產品。雖然子產品B已經加載到記憶體了,如果要通路還要在明确的在本子產品中import B。

另外一種嵌套指,在子產品A中import B,而在子產品B中import A。這時會怎麼樣呢?這個在Python清單中由RobertChen給出了詳細解釋,抄錄如下:

[A.py]  
from B import D  
class C:pass  
      

[B.py]

from A import C

class D:pass

為什麼執行A的時候不能加載D呢?

如果将A.py改為:import B就可以了。

這是怎麼回事呢?

RobertChen:這跟Python内部import的機制是有關的,具體到from B import D,Python内部會分成幾個步驟:

  1. 在sys.modules中查找符号"B"
  2. 果符号B存在,則獲得符号B對應的module對象<module B>。

    從<module B>的__dict__中獲得符号"D"對應的對象,如果"D"不存在,則抛出異常

  3. 如果符号B不存在,則建立一個新的module對象<module B>,注意,這時,module對象的__dict__為空。

    執行B.py中的表達式,填充<module B>的__dict__ 。

    從<module B>的__dict__中獲得"D"對應的對象,如果"D"不存在,則抛出異常。

是以,這個例子的執行順序如下:

1、執行A.py中的from B import D

由于是執行的python A.py,是以在sys.modules中并沒有<moduleB>存在,首先為B.py建立一個module對象(<moduleB>),注意,這時建立的這個module對象是空的,裡邊啥也沒有,在Python内部建立了這個module對象之後,就會解析執行B.py,其目的是填充<module B>這個dict。

2、執行B.py中的from A import C

在執行B.py的過程中,會碰到這一句,首先檢查sys.modules這個module緩存中是否已經存在<moduleA>了,由于這時緩存還沒有緩存<moduleA>,是以類似的,Python内部會為A.py建立一個module對象(<moduleA>),然後,同樣地,執行A.py中的語句。

3、再次執行A.py中的from B import D

這時,由于在第1步時,建立的<moduleB>對象已經緩存在了sys.modules中,是以直接就得到了<moduleB>,但是,注意,從整個過程來看,我們知道,這時<moduleB>還是一個空的對象,裡面啥也沒有,是以從這個module中獲得符号"D"的操作就會抛出異常。如果這裡隻是importB,由于"B"這個符号在sys.modules中已經存在,是以是不會抛出異常的。

上面的解釋已經由Zoom.Quiet收錄在啄木鳥了,裡面有圖,可以參考一下。

Package(包) Import

包(Package)可以看成子產品的集合,隻要一個檔案夾下面有個__init__.py檔案,那麼這個檔案夾就可以看做是一個包。包下面的檔案夾還可以成為包(子包)。更進一步,多個較小的包可以聚合成一個較大的包,通過包這種結構,友善了類的管理和維護,也友善了使用者的使用。比如SQLAlchemy等都是以包的形式釋出給使用者的。

包和子產品其實是很類似的東西,如果檢視包的類型import SQLAlchemy type(SQLAlchemy),可以看到其實也是<type 'module'>。import包的時候查找的路徑也是sys.path。

包導入的過程和子產品的基本一緻,隻是導入包的時候會執行此包目錄下的__init__.py而不是子產品裡面的語句了。另外,如果隻是單純的導入包,而包的__init__.py中又沒有明确的其他初始化操作,那麼此包下面的子產品是不會自動導入的。如:

PA

--__init__.py

--wave.py

--PB1

  --__init__.py

  --pb1_m.py

--PB2

  --__init__.py

  --pb2_m.py

__init__.py都為空,如果有以下程式:

    1. import sys
    2. import PA.wave  #1
  1. import PA.PB1   #2
  2. import PA.PB1.pb1_m as m1  #3
  3. import PA.PB2.pb2_m #4
  4. PA.wave.getName() #5
  5. m1.getName() #6
  6. PA.PB2.pb2_m.getName() #7

當執行#1後,sys.modules會同時存在PA、PA.wave兩個子產品,此時可以調用PA.wave的任何類或函數了。但不能調用PA.PB1(2)下的任何子產品。目前Local中有了PA名字。

當執行#2後,隻是将PA.PB1載入記憶體,sys.modules中會有PA、PA.wave、PA.PB1三個子產品,但是PA.PB1下的任何子產品都沒有自動載入記憶體,此時如果直接執行PA.PB1.pb1_m.getName()則會出錯,因為PA.PB1中并沒有pb1_m。目前Local中還是隻有PA名字,并沒有PA.PB1名字。

當執行#3後,會将PA.PB1下的pb1_m載入記憶體,sys.modules中會有PA、PA.wave、PA.PB1、PA.PB1.pb1_m四個子產品,此時可以執行PA.PB1.pb1_m.getName()了。由于使用了as,目前Local中除了PA名字,另外添加了m1作為PA.PB1.pb1_m的别名。

當執行#4後,會将PA.PB2、PA.PB2.pb2_m載入記憶體,sys.modules中會有PA、PA.wave、PA.PB1、PA.PB1.pb1_m、PA.PB2、PA.PB2.pb2_m六個子產品。目前Local中還是隻有PA、m1。

下面的#5,#6,#7都是可以正确運作的。

注意的是:如果PA.PB2.pb2_m想導入PA.PB1.pb1_m、PA.wave是可以直接成功的。最好是采用明确的導入路徑,對于./..相對導入路徑還是不推薦用。