python模組匯入細節
python模組匯入細節
官方手冊: ofollow,noindex" target="_blank">https://docs.python.org/3/tutorial/modules.html
可執行檔案和模組
python原始碼檔案按照功能可以分為兩種型別:
- 用於執行的可執行程式檔案
- 不用與執行,僅用於被其它python原始碼檔案匯入的模組檔案
例如檔案a.py和b.py在同一目錄下,它們的內容分別是:
# b.py x="var x in module b" y=5 # a.py: import b import sys print(b.x) print(b.y)
a.py匯入其它檔案(b.py)後,就可以使用b.py檔案中的屬性(如變數、函式等)。這裡,a.py就是可執行檔案,b.py就是模組檔案,但模組名為b,而非b.py。
python提供了一些標準庫,是預定義好的模組檔案,例如上面的sys模組。
在此有幾個注意點,在後面會詳細解釋:
- 模組b的檔名為b.py,但import匯入的時候,使用的名稱為b,而非b.py
- a.py和b.py是在同一個目錄下的,如果不在同目錄下能否匯入?
- 在a.py中訪問b.py模組中的屬性時,使用的是
b.x
、b.y
- 上面都是直接以模組名匯入的,python還支援更復雜的包匯入方式,例如匯入abc/b.py時,使用
import abc.b
。下一篇文章會詳細解釋包的匯入方式
python模組搜尋路徑
在a.py中匯入模組b的時候,python會做一系列的模組檔案路徑搜尋操作:b.py在哪裡?只有找到它才能讀取、執行(裝載)該模組。
在任何一個python程式啟動時,都會將模組的搜尋路徑收集到sys模組的path屬性中( sys.path
)。 當python需要搜尋模組檔案在何處時,首先搜尋內建模組,如果不是內建模組,則搜尋sys.path中的路徑列表,搜尋時會從該屬性列出的路徑中按照從前向後的順序進行搜尋,並且只要找到就立即停止搜尋該模組檔案 (也就是說不會後搜尋的同名模組覆蓋先搜尋的同名模組)。
例如,在a.py檔案中輸出一下這個屬性的內容:
# a.py: import sys print(sys.path)
結果:
['G:\\pycode', 'C:\\Program Files (x86)\\Python36-32\\python36.zip', 'C:\\Program Files (x86)\\Python36-32\\DLLs', 'C:\\Program Files (x86)\\Python36-32\\lib', 'C:\\Program Files (x86)\\Python36-32', 'C:\\Users\\malong\\AppData\\Roaming\\Python\\Python36\\site-packages', 'C:\\Program Files (x86)\\Python36-32\\lib\\site-packages']
python模組的搜尋路徑包括幾個方面,按照如下順序搜尋:
- 程式檔案(a.py)所在目錄,即
G:\\pycode
- 環境變數
PYTHONPATH
所設定的路徑(如果定義了該環境變數,則從左向右的順序搜尋) - 標準庫路徑
- .pth檔案中定義的路徑
需要注意,上面sys.path的結果中,除了 .zip
是一個檔案外,其它的搜尋路徑全都是目錄,也就是從這些目錄中搜索模組X的檔案X.py是否存在。
程式所在目錄
這個目錄是最先搜尋的,且是python自動搜尋的,無需對此進行任何設定。從互動式python程式終輸出sys.path的結果:
>>> sys.path ['', 'C:\\WINDOWS\\system32', 'C:\\Program Files (x86)\\Python36-32\\Lib\\idlelib', 'C:\\Program Files (x86)\\Python36-32\\python36.zip', 'C:\\Program Files (x86)\\Python36-32\\DLLs', 'C:\\Program Files (x86)\\Python36-32\\lib', 'C:\\Program Files (x86)\\Python36-32', 'C:\\Users\\malong\\AppData\\Roaming\\Python\\Python36\\site-packages', 'C:\\Program Files (x86)\\Python36-32\\lib\\site-packages']
其中第一個 ''
表示的就是程式所在目錄。
注意程式所在目錄和當前目錄是不同的。例如,在/tmp/目錄下執行/pycode中的a.py檔案
cd /tmp python /pycode/a.py
其中/tmp為當前目錄,而/pycode是程式檔案a.py所在的目錄。如果a.py中匯入b.py,那麼將首先搜尋/pycode,而不是/tmp。
環境變數PYTHONPATH
這個變數中可以自定義一系列的模組搜尋路徑列表,這樣可以跨目錄搜尋(另一種方式是設定.pth檔案)。但預設情況下這個環境變數是未設定的。
在windows下,設定PYTHONPATH環境變數的方式: 命令列中輸入:SystemPropertiesAdvanced-->環境變數-->系統環境變數新建
如果是多個路徑,則使用英文格式的分號分隔。以下是臨時設定當前命令列視窗的PYTHONPATH:
set PYTHONPATH='D:\pypath; d:\pypath1'
在unix下,設定PYTHONPATH環境變數的方式,使用冒號分隔多個路徑:
PYTHONPATH=/tmp/pypath1:/tmp/pypath2
如果要永久生效,則寫入配置檔案中:
echo 'export PYTHONPATH=/tmp/pypath1:/tmp/pypath2' >/etc/profile.d/pypth.sh chmod +x /etc/profile.d/pypth.sh source /etc/profile.d/pypth.sh
標準庫路徑
在Linux下,標準庫的路徑一般是在/usr/lib/pythonXXX/下(XXX表示python版本號),此目錄下有些分了子目錄。
例如:
['', '/usr/lib/python35.zip', '/usr/lib/python3.5', '/usr/lib/python3.5/plat-x86_64-linux-gnu', '/usr/lib/python3.5/lib-dynload', '/usr/local/lib/python3.5/dist-packages', '/usr/lib/python3/dist-packages']
其中/usr/lib/python3.5和其內的幾個子目錄都是標準庫的搜尋路徑。
注意其中/usr/lib/python35.zip,它是ZIP檔案元件,當定義此檔案為搜尋路徑時,將自動解壓縮該檔案,並從此檔案中搜索模組。
Windows下根據python安裝位置的不同,標準庫的路徑不同。如果以預設路徑方式安裝的python,則標準庫路徑為 C:\\Program Files (x86)\\Python36-32
及其分類的子目錄。
.pth檔案自定義路徑
可以將自定義的搜尋路徑放進一個.pth檔案中,每行一個搜尋路徑。然後將.pth檔案放在python安裝目錄或某個標準庫路徑內的sitepackages目錄下即可。
這是一種替換PYTHONPATH的友好方式,因為不同作業系統設定環境變數的方式不一樣,而以檔案的方式記錄是所有作業系統都通用的。
例如,windows下,在python安裝目錄 C:\\Program Files (x86)\\Python36-32
下新增一個mypath.pth檔案,內容如下:
d:\pypath1 d:\pypath2
再去輸出sys.path,將可以看到這兩個路徑已經放進了搜尋列表中。
修改搜尋路徑
除了上面環境變數和.pth檔案,還可以直接修改sys.path或者site.getsitepackages()的結果。
例如,在import匯入sys模組之後,可以修改sys.path,向這個列表中新增其它搜尋路徑,這樣之後匯入其它模組的時候,也會搜尋該路徑。
例如:
import sys sys.path.append('d:\\pypath3') print(sys.path)
sys.path的最後一項將是新新增的路徑。
匯入模組的細節
匯入模組時的過程
python的import是在程式執行期間執行的,並非像其它很多語言一樣是在編譯期間執行。也就是說,import可以出現在任何地方,只有執行到這個import行時,才會執行匯入操作。且在import某個模組之前,無法訪問這個模組的屬性。
python在import匯入模組時,首先搜尋模組的路徑,然後編譯並執行這個模組檔案。雖然概括起來只有兩個過程,但實際上很複雜。
前文已經解釋了import的模組搜尋過程,所以這裡大概介紹import的其它細節。
以前面的a.py中匯入模組檔案b.py為例:
import b
import匯入模組時,搜尋到模組檔案b.py後:
1.首先在記憶體中為每個待匯入的模組構建module類的例項:模組物件。這個模組物件目前是空物件,這個物件的名稱為全域性變數b。
注意細節:module類的物件,變數b。
輸出下它們就知道:
print(b) print(type(b))
輸出結果:
<module 'b' from 'g:\\pycode\\b.py'> <class 'module'>
因為b是全域性變數,所以當前程式檔案a.py中不能重新對全域性變數b進行賦值,這會使匯入的模組b被丟棄。例如,下面是錯誤的:
import b b=3 print(b.x)# 已經沒有模組b了
另外,因為import匯入時是將模組物件賦值給模組變數,所以模組變數名不能是python中的一些關鍵字,比如if、for等,這時會報錯。雖然模組檔名可以為list、keys等這樣的內建函式名,但這會導致這些內建函式不可用,因為根據變數查詢的作用域規則,首先查詢全域性變數,再查詢內建作用域。 也就是說,模組檔案的檔名不能是這些關鍵字、也不應該是這些內建函式名 。
File "g:/pycode/new.py", line 11 import if ^ SyntaxError: invalid syntax
2.構造空模組例項後,將編譯、執行模組檔案b.py,並按照一定的規則將一些結果放進這個模組物件中。
注意細節,編譯、執行b.py、將結果儲存到模組物件中。
模組第一次被匯入的時候,會進行編譯,並生成.pyc位元組碼檔案,然後python執行這個pyc檔案。當模組被再次匯入時,如果檢查到pyc檔案的存在,且和原始碼檔案的上一次修改時間戳mtime完全對應(也就是說,編譯後原始碼沒有進行過修改),則直接裝載這個pyc檔案並執行,不會再進行額外的編譯過程。當然,如果修改過原始碼,將會重新編譯得到新的pyc檔案。
注意,並非所有的py檔案都會生成編譯得到的pyc檔案,對於那些只執行一次的程式檔案,會將記憶體中的編譯結果在執行完成後直接丟棄(多數時候如此,但仍有例外,比如使用 compileall 模組可以強制編譯成pyc檔案),但模組會將記憶體中的編譯結果持久化到pyc檔案中。另外,執行位元組碼pyc檔案並不會比直接執行py檔案更快,執行它也一樣是一行行地解釋、執行,唯一快的地方在於匯入裝載的時候無需重新編譯而已。
執行模組檔案(已完成編譯)的時候,按照一般的執行流程執行:一行一行地、以程式碼塊為單元執行。一般地,模組檔案中只用來宣告變數、函式等屬性,以便提供給匯入它的模組使用,而不應該有其他任何操作性的行為,比如print()操作不應該出現在模組檔案中,但這並非強制。
總之,執行完模組檔案後,這個模組檔案將有一個自己的全域性名稱空間,在此模組檔案中定義的變數、函式等屬性,都會記錄在此名稱空間中。
最後,模組的這些屬性都會儲存到模組物件中。由於這個模組物件賦值給了模組變數b,所以通過變數b可以訪問到這個物件中的屬性(比如變數、函式等),也就是模組檔案內定義的全域性屬性。
只匯入一次
假設a.py中匯入了模組b和模組sys,在b.py中也匯入了模組sys,但python預設對某個模組只會匯入一次,如果a.py中先匯入sys,再匯入b,那麼匯入b並執行b.py的時候,會發現sys已經匯入了,不會再去匯入sys。
實際上,python執行程式的時候,會將所有已經匯入的模組放進sys.module屬性中,這是一個dict,可以通過下面的方式檢視已匯入的模組名:
>>> import sys >>> list(sys.module.keys())
如果某個程式檔案中多次使用import(或from)匯入同一個模組,雖然不會報錯,但實際上還是直接使用記憶體中已裝載好的模組物件。
例如,b.py中x=3,匯入它之後修改該值,然後再次匯入,發現b.x並不會發生改變:
import b print(b.x)# 3 b.x=33 print(b.x)# 33 import b print(b.x)# 33
但是python提供了reload進行多次重複匯入的方法,見後文。
使用別名
import匯入時,可以使用 as
關鍵字指定一個別名作為模組物件的變數,例如:
import b as bb bb.x=3 print(bb.x)
這時候模組物件將賦值給變數bb,而不是b,b此時不再是模組物件變數,而僅僅只是模組名。使用別名並不會影響效能,因為它僅僅只是一個賦值過程,只不過是從原來的賦值物件變數b變為變數bb而已。
from匯入部分屬性
import語句是匯入模組中的所有屬性,並且訪問時需要使用模組變數來引用。例如:
import b print(b.x)
除了import,還有一個from語句,表示從模組中匯入部分指定的屬性,且使得可以直接使用這些屬性的名稱來引用這些屬性,而不需要加上模組變數名。例如原來import匯入時訪問變數x使用 b.x
,from匯入時只需使用x即可。實際上,from匯入更應該稱為屬性的再次賦值(拷貝)。
例如,b.py中定義了變數x、y、z,同時定義了函式f()和g(),在a.py中匯入這個模組檔案,但只匯入x變數和f函式:
# a.py檔案內容: from b import x,f print(x) f() # b.py檔案內容: x=3 y=4 z=5 def f(): print("function f in b.py") def g(): print("function g in b.py")
注意上面a.py中引用模組b中屬性的方式沒有加上 b.X
,而是直接使用x和f()來引用。這和import是不一樣的。至於from和import匯入時的變數名稱細節,在下面的內容中會詳細解釋。
雖然from語句只匯入模組的部分屬性,但實際上仍然會完整地執行整個模組檔案。
同樣的,from語句也可以指定匯入屬性的變數別名,例如,將b.py中的屬性x賦值給xx,將y賦值給yy:
from b import x as xx,y as yy print(xx) print(yy)
from語句還有一個特殊匯入統配符號 *
,它表示匯入模組中的所有屬性。
# a.py檔案: from b import * print(x,y,z) f() g()
多數時候,不應該使用 from *
的方式,因為我們可能會忘記某個模組中有哪些屬性拷貝到了當前檔案,特別是多個 from *
時可能會出現屬性覆蓋的問題。
過載模組:imp.reload()
無論時import還是from,都只匯入一次模組,但使用reload()可以強制重新裝載模組。
reload()是imp模組中的一個函式,所以要使用imp.reload()之前,必須先匯入imp。
from imp import reload reload(b)
reload()是一個函式,它的引數是一個已經成功被匯入過的模組變數(如果使用了別名,則應該使用別名作為reload的引數),也就是說該模組必須在記憶體中已經有自己的模組物件。
reload()會重新執行模組檔案,並將執行得到的屬性完全覆蓋到原有的模組物件中。也就是說, reload()會重新執行模組檔案,但不會在記憶體中建立新的模組物件,所以原有模組物件中的屬性可能會被修改 。
例如,模組檔案b.py中x=3,匯入b模組,修改其值為33,然後reload這個模組,會發現值重新變回了3。
import b print(b.x)# 3 b.x=33 print(b.x)# 33 from imp import reload reload(b) print(b.x)# 3
有時候reload()很有用,可以讓程式無需重啟就執行新的程式碼。例如,在python的互動式模式下匯入模組b,然後修改python原始碼,再reload匯入:
>>> import b >>> b.x 3 # 不要關掉互動式直譯器,直接修改原始碼中的b=3333 >>> from imp import reload >>> reload(b) <module 'b' from 'G:\\pycode\\b.py'> >>> b.x 3333
但正因為reload()過載模組會改變原始的值,這可能是很危險的行為,一定要清楚地知道它是在幹什麼。
匯入模組時的變數名稱細節
import匯入的變數
import匯入時,模組物件中的屬性有自己的名稱空間,然後將整個模組物件賦值給模組變數。
例如,在a.py中匯入b:
import b print(b.x)
這個過程唯一和當前檔案a.py作用域有關的就是模組物件變數b,b.py中宣告的屬性和當前檔案無任何關係。無論是訪問還是修改,都是直接修改這個模組物件自身作用域中的值。所以,只要模組變數b不出現衝突問題,可以放心地修改模組b中的屬性。
另一方面,因為每個程序都有自己的記憶體空間,所以 在a.py、c.py中都匯入b時,a.py中修改b的屬性值不會影響c.py中匯入的屬性,a.py和c.py中模組物件所儲存的屬性都是執行b.py後得到的,它們相互獨立 。
from匯入的變數
from匯入模組時,會先執行完模組檔案,然後將指定的部分屬性重新賦值給當前程式檔案的同名全域性變數。
例如,在模組檔案b.py中定義了x、y、z變數和f()、g()函式:
# b.py: x=3 y=4 b=5 def f(): print("function f in b.py") def g(): print("function g in b.py")
當在a.py中匯入b模組時,如果只匯入x、y和f():
# a.py: from b import x, y, f
實際上的行為是 構造模組物件後,將這個模組物件對應的名稱空間中的屬性x、y和f重新賦值給a.py中的變數x、y和f,然後丟棄整個模組物件以及整個名稱空間。換句話說,b不再是一個有效的模組變數(所以和import不一樣),來自b的x,y,z,f和g也都被丟棄 。
這裡有幾個細節,需要詳細解釋清楚,只有理解了才能搞清楚它們是怎麼生效的。
假設現在模組檔案b.py的內容為,並且a.py中匯入x,y,f屬性:
# b.py: x=3 y=[1,2] z=5 def f(): print("function f in b.py") def g(): print("function g in b.py") # a.py: from b import x,y,f
首先在執行模組檔案b.py時,會構造好自己的模組物件,並且模組物件有自己的名稱空間(作用域),模組物件構造完成後,它的名稱空間大致如下:
然後python會在a.py的全域性作用域內建立和匯入屬性同名的全域性變數x,y和f,並且通過賦值的方式將模組的屬性賦值給這些全域性變數,也就是:
x = b.x y = b.y f = b.f
上面的b只是用來演示,實際上變數b是不存在的。
賦值完成後,我們和構造的整個模組物件就失去聯絡了,因為沒有變數b去引用這個物件。但需要注意,這個物件並沒有被刪除,僅僅只是我們無法通過b去找到它。
所以,現在的示意圖如下:
因為是賦值的方式傳值的,所以在a.py中修改這幾個變數的值時,是直接在模組物件作用域內修改的:對於不可變物件,將在此作用域內建立新物件,對於可變物件,將直接修改原始物件的值。
另一方面,由於模組物件一直保留在記憶體中,下次繼續匯入時,將直接使用該模組物件。對於import和from,是直接使用該已存在的模組物件,對於reload,是覆蓋此模組物件。
例如,在a.py中修改不可變物件x和可變物件y,之後import或from時,可變物件的值都會隨之改變,因為它們使用的都是原來的模組物件:
from b import x,y x=33 y[0]=333 from b import x,y print((x,y))# 輸出(3, [333, 2]) import b print((b.x,b.y))# 輸出(3, [333, 2])
from匯入時,由於b不再是模組變數,所以無法再使用reload(b)去過載物件。如果想要過載,只能先import,再reload:
from b import x,y ...CODE... # 想要過載b import b from imp import reload reload(b)
檢視模組中的屬性
內建函式dir可用於列出某模組中定義了哪些屬性。
import b dir(b)
輸出結果:
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'f', 'g', 'x', 'y', 'z']
可見,模組的屬性中除了自己定義的屬性外,還有一些內建的屬性,比如上面以 __
開頭和結尾的屬性。
如果dir()不給任何引數,則輸出當前環境下定義的名稱屬性:
>>> import b >>> x=3 >>> aaa=333 >>> dir() ['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'aaa', 'b', 'x']
每個屬性都對應一個物件,例如x對應的是int物件,b對應的是module物件:
>>> type(x) <class 'int'> >>> type(b) <class 'module'>
既然是物件,那麼它們都會有自己的屬性。例如:
>>> dir(x) ['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__gt__', '__hash__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'numerator', 'real', 'to_bytes']
所以,也可以直接dir某個模組內的屬性:
import b dir(b.x) dir(b.__name__)
dir()不會列出內建的函式和變數,如果想要輸出內建的函式和變數,可以去標準模組builtins中檢視,因為它們定義在此模組中:
import builtins dir(buildins)