一、概述
模塊(module)和 包(package)是Python用於組織大型程序的利器。
模塊 是一個由 變量、函數、類 等基本元素組成的功能單元,設計良好的模塊通常是高內聚、低耦合、可復用、易維護的。包 是管理模塊的容器,它具有 可嵌套性:一個包可以包含模塊和其他包。從文件系統的視角來看,包就是目錄,模塊就是文件。
從本質上講,一個模塊就是一個獨立的名字空間(namespace),單純的多個模塊只能構成扁平結構的名字空間集;而包的可嵌套性,使得多個模塊可以呈現出多層次結構的名字空間樹。
二、導入語句
如果要在一個模塊A中使用另一個模塊B(即訪問模塊B的屬性),則必須首先 導入 模塊B。此時模塊A稱為導入模塊(即importer),而模塊B稱為被導入模塊(即importee)。
導入語句(import statement)有兩種風格:import <>
和from <> import
。對模塊的導入同時支持這兩種風格。
1、基本語法
1)導入模塊module(重命名為name)
import module [as name]
>>> import sys
>>> sys.version
'2.7.3 (default, Apr 10 2013, 05:46:21) \n[GCC 4.6.3]'
>>> import sys as s
>>> s.version
'2.7.3 (default, Apr 10 2013, 05:46:21) \n[GCC 4.6.3]'
2)導入模塊module1(重命名為name1),模塊module2(重命名為name2),等等
import module1 [as name1], module2 [as name2], ...
>>> import sys, os
>>> sys.platform, os.name
('linux2', 'posix')
>>> import sys as s, os as o
>>> (s.platform, o.name)
('linux2', 'posix')
3)從模塊module中導入屬性attribute(重命名為name)
from module import attribute [as name]
>>> from sys import executable
>>> executable
'/usr/bin/python'
>>> from sys import executable as exe
>>> exe
'/usr/bin/python'
4)從模塊module中導入屬性attribute1(重命名為name1),屬性attribute2(重命名為name2),等等
from module import attribute1 [as name1], attribute2 [as name2], ...
>>> from sys import platform, executable
>>> platform, executable
('linux2', '/usr/bin/python')
>>> from sys import platform as plf, executable as exe
>>> plf, exe
('linux2', '/usr/bin/python')
5)從模塊module中導入屬性attribute1(重命名為name1),屬性attribute2(重命名為name2),等等
from module import (attribute1 [as name1], attribute2 [as name2], ...)
>>> from sys import (platform, executable)
>>> platform, executable
('linux2', '/usr/bin/python')
>>> from sys import (platform as plf, executable as exe)
>>> plf, exe
('linux2', '/usr/bin/python')
6)從模塊module中導入所有屬性
from module import *
>>> from sys import *
>>> platform, executable
('linux2', '/usr/bin/python')
2、推薦風格
以下是在Python程序中推薦使用的導入語句:
import module [as name]
(導入單個模塊)from module import attribute [as name]
(導入單個屬性)from module import attribute1 [as name1], attribute2 [as name2], ...
(導入較少屬性時,單行書寫)from module import (attribute1 [as name1], attribute2 [as name2], ...)
(導入較多屬性時,分行書寫)
應當盡量避免使用的導入語句是:
-
import module1 [as name1], module2 [as name2], ...
它會降低代碼的可讀性,應該用多個
import module [as name]
語句代替。 -
from module import *
它會讓importer的名字空間變得不可控(很可能一團糟)。
三、模塊
1、模塊名
一個 模塊 就是一個Python源碼文件。如果文件名為mod.py,那么模塊名就是mod。
模塊的導入和使用都是借助模塊名來完成的,模塊名的命名規則與變量名相同。
2、模塊屬性
模塊屬性 是指在模塊文件的全局作用域內,或者在模塊外部(被其他模塊導入后)可以訪問的所有對象名字的集合。這些對象名字構成了模塊的名字空間,這個名字空間其實就是全局名字空間(參考 名字空間與作用域)。
模塊的屬性由兩部分組成:固有屬性 和 新增屬性。可以通過 M.__dict__ 或 dir(M) 來查看模塊M的屬性。
1)固有屬性
固有屬性 是Python為模塊默認配置的屬性。
例如,新建一個空文件mod.py:
$ touch mod.py
$ python
...
>>> import mod # 導入模塊mod
>>> mod.__dict__ # 模塊mod的屬性全貌
{'__builtins__': {...}, '__name__': 'mod', '__file__': 'mod.pyc', '__doc__': None, '__package__': None}
>>> dir(mod) # 只查看屬性名
['__builtins__', '__doc__', '__file__', '__name__', '__package__']
上述示例中,空模塊mod的所有屬性都是固有屬性,包括:
__builtins__
內建名字空間(參考 名字空間)__file__
文件名(對於被導入的模塊,文件名為絕對路徑格式;對於直接執行的模塊,文件名為相對路徑格式)__name__
模塊名(對於被導入的模塊,模塊名為去掉“路徑前綴”和“.pyc后綴”后的文件名,即os.path.splitext(os.path.basename(__file__))[0]
;對於直接執行的模塊,模塊名為__main__
)__doc__
文檔字符串(即模塊中在所有語句之前第一個未賦值的字符串)__package__
包名(主要用於相對導入,請參考 PEP 366)
2)新增屬性
新增屬性 是指在模塊文件的頂層(top-level),由賦值語句(如import、=、def和class)創建的屬性。
例如,修改文件mod.py為:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
'''this is a test module'''
import sys
debug = True
_static = ''
class test_class(object):
def say(self): pass
def test_func():
var = 0
再次查看模塊mod的屬性:
>>> import mod
>>> dir(mod)
['__builtins__', '__doc__', '__file__', '__name__', '__package__', '_static', 'debug', 'sys', 'test_class', 'test_func']
對比上一小節可知,除開固有屬性外的其他屬性都是新增屬性,包括:
- sys(由“import”創建)
- debug和_static(均由“=”創建)
- test_class(由“class”創建)
- test_func(由“def”創建)
這些屬性的共同點是:它們都在模塊文件的頂層創建。相比之下,類方法say(在類test_class內部創建)和局部變量var(在函數test_func內部創建)都不在頂層,因此不在新增屬性之列。(作為一個例子,'''this is a test module'''就是模塊mod的文檔字符串,即mod.__doc__
的值)
3、可導出的公有屬性
在 『導入語句』 中描述的基本語法可以歸納為三類:
import module
在導入模塊(importer)中,可以通過module.*的形式訪問模塊module中的所有屬性from module import attribute
只能通過名字attribute訪問模塊module中的指定屬性module.attributefrom module import *
可以直接通過屬性名訪問模塊module中的所有 公有屬性
換句話說,模塊的 公有屬性 就是那些可以通過from module import *
被導出給其他模塊直接使用的屬性。
模塊的公有屬性有以下特點:
- 可以在模塊中定義一個特殊列表
__all__
,其中包含所有可導出的公有屬性的字符串名稱,從而實現對公有屬性的定制 - 如果沒有定義
__all__
,那么默認所有不以下划線“_”開頭的屬性都是可導出的公有屬性
以 『新增屬性』 中的mod.py為例,沒有定義__all__
的公有屬性:
>>> dir() # 導入模塊mod前的名字空間
['__builtins__', '__doc__', '__name__', '__package__']
>>> from mod import * # 導入模塊mod中的所有公有屬性
>>> dir() # 導入模塊mod后的名字空間
['__builtins__', '__doc__', '__name__', '__package__', 'debug', 'sys', 'test_class', 'test_func']
對比導入模塊mod前后的情況可知,模塊mod中的sys、debug、test_class和test_func都屬於公有屬性,因為它們的名字不以“_”開頭;而其他以“_”開頭的屬性(包括所有“固有屬性”,以及“新增屬性”中的_static)都不在公有屬性之列。
如果在模塊文件mod.py中頂層的任何位置,增加定義一個特殊列表__all__ = ['sys', '_static', 'test_func']
(此時__all__
也是模塊屬性),那么此時的公有屬性:
>>> dir() # 導入模塊mod前的名字空間
['__builtins__', '__doc__', '__name__', '__package__']
>>> from mod import * # 導入模塊mod中的所有公有屬性
>>> dir() # 導入模塊mod后的名字空間
['__builtins__', '__doc__', '__name__', '__package__', '_static', 'sys', 'test_func']
可以看出,只有在__all__
中指定的屬性才是公有屬性。
4、直接執行
模塊可以在命令行下被直接執行,以模塊mod(對應文件mod.py)為例:
1)以腳本方式執行
python mod.py <arguments>
2)以模塊方式執行
python -m mod <arguments>
四、包
一個 包 就是一個含有__init__.py文件的目錄。
包與模塊之間的包含關系是:一個包可以包含子包或子模塊,但一個模塊卻不能包含子包和子模塊。
1、包名
與模塊名類似,包名的命名規則也與變量名相同。
此外,需要特別注意的是:如果在同一個目錄下,存在兩個同名的包和模塊,那么導入時只會識別包,而忽略模塊。(參考 specification for packages 中的 『What If I Have a Module and a Package With The Same Name?』)
例如,在目錄dir下新建一個文件spam.py(即模塊spam),此時import spam
會導入模塊spam:
$ cd dir/
$ touch spam.py
$ python
...
>>> import spam
>>> spam
<module 'spam' from 'spam.py'>
如果在目錄dir下再新建一個含有__init__.py文件的目錄spam(即包spam),此時import spam
則會導入包spam(而不再是模塊spam):
$ mkdir spam && touch spam/__init__.py
$ python
...
>>> import spam
>>> spam
<module 'spam' from 'spam/__init__.py'>
2、包屬性
包屬性與模塊屬性非常相似,也分為 固有屬性 和 新增屬性。
1)固有屬性
與模塊相比,包的 固有屬性 僅多了一個__path__
屬性,其他屬性完全一致(含義也類似)。
__path__
屬性即包的路徑(列表),用於在導入該包的子包或子模塊時作為搜索路徑;修改一個包的__path__
屬性可以擴展該包所能包含的子包或子模塊。(參考 Packages in Multiple Directories)
例如,在dir目錄下新建一個包pkg(包含一個模塊mod),顯然在包pkg中只能導入一個子模塊mod:
$ mkdir pkg && touch pkg/__init__.py
$ touch pkg/mod.py
$ python
...
>>> import pkg.mod # 可以導入子模塊mod
>>> import pkg.mod_1 # 不能導入子模塊mod_1
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ImportError: No module named mod_1
如果在dir目錄下再新建一個包pkg_1(包含一個模塊mod_1):
$ mkdir pkg_1 && touch pkg_1/__init__.py
$ touch pkg_1/mod_1.py
並且在pkg/__init__.py中修改包pkg的__path__
屬性:
print('before:', __path__)
__path__.append(__path__[0].replace('pkg', 'pkg_1')) # 將“包pkg_1所在路徑”添加到包pkg的__path__屬性中
print('after:', __path__)
此時,在包pkg中就可以導入子模塊mod_1(仿佛子模塊mod_1真的在包pkg中):
$ python
...
>>> import pkg.mod # 可以導入子模塊mod
('before:', ['pkg'])
('after:', ['pkg', 'pkg_1'])
>>> import pkg.mod_1 # 也可以導入子模塊mod_1
2)新增屬性
包的 新增屬性 包括兩部分:靜態的新增屬性和動態的新增屬性。
靜態的新增屬性是指:在__init__.py的頂層(top-level),由賦值語句(如import、=、def和class)創建的屬性。這部分與模塊的新增屬性一致。
動態的新增屬性是指:在執行導入語句后動態添加的新增屬性。具體而言,如果有一個導入語句導入了某個包pkg中的子模塊submod(或子包subpkg),那么被導入的子模塊submod(或子包subpkg)將作為一個屬性,被動態添加到包pkg的新增屬性當中。
以包含模塊mod的包pkg為例:
>>> import pkg
>>> dir(pkg)
['__builtins__', '__doc__', '__file__', '__name__', '__package__', '__path__']
>>> import pkg.mod # 該語句導入了包pkg中的模塊mod
>>> dir(pkg) # mod成為了包pkg的“動態的新增屬性”
['__builtins__', '__doc__', '__file__', '__name__', '__package__', '__path__', 'mod']
3、可導出的公有屬性
在公有屬性方面,包與模塊的行為完全一致,當然別忘了包還有 動態的新增屬性。
4、其他
1)導入語句
加入包的概念以后,導入語句的風格(與僅有模塊時相比)不變,但是語法上有一些細微差異:用“.”來表示包與模塊之間的包含關系;可操作的對象擴充為 包、模塊 和 屬性。
下面是涉及包時,一些典型的導入語句:
import package
import package.module
import package.subpackage
import package.subpackage.module
from packagae import module
from packagae import subpackagae
from packagae import subpackagae.module
或from packagae.subpackagae import module
from packagae.module import attribute
from packagae.subpackagae.module import attribute
2)__init__.py
關於包的__init__.py,有以下幾點總結:
- 一般為空即可
- 有時也可以放置一些初始化代碼,用於在包加載時執行
- 少數情況下,還可以用於定制包的一些屬性(如
__all__
、__path__
等)
五、導入原理
模塊與包的導入原理幾乎完全一致,因此下面以模塊為主進行討論,僅在有顯著差異的地方對包作單獨說明。
1、導入依賴
對於模塊M而言,根據導入語句的不同(指明了模塊M是否在一個包中),可能存在導入依賴的問題:
-
import M
模塊M不在一個包中,因此無導入依賴:直接以“M”為 完整名(fully qualified name)導入模塊M
-
import A.B.M
或者from A.B import M
模塊M在一個子包B中,而子包B又在一個包A中,因此存在導入依賴:會首先以“A”為 完整名 導入包A,接着以“A.B”為 完整名 導入子包B,最后以“A.B.M”為 完整名 導入模塊M。
2、導入過程
一個模塊的導入過程主要分三步:搜索、加載 和 名字綁定。(具體參考 The import statement)
1)搜索
搜索 是整個導入過程的核心,也是最為復雜的一步。對於被導入模塊M,按照先后順序,搜索的處理步驟為:
- 在緩存 sys.modules 中查找模塊M,若找到則直接返回模塊M
- 否則,順序搜索 sys.meta_path,逐個借助其中的 finder 來查找模塊M,若找到則加載后返回模塊M
- 否則,如果模塊M在一個包P中(如
import P.M
),則以P.__path__
為搜索路徑進行查找;如果模塊M不在一個包中(如import M
),則以 sys.path 為搜索路徑進行查找
2)加載
正如 『搜索』 步驟中所述,對於找到的模塊M:如果M在緩存 sys.modules 中,則直接返回;否則,會加載M。
加載 是對模塊的初始化處理,包括以下步驟:
- 設置屬性:包括
__name__
、__file__
、__package__
和__loader__
(對於包,則還有__path__
) - 編譯源碼:將模塊文件(對於包,則是其對應的__init__.py文件)編譯為字節碼(*.pyc),如果字節碼文件已存在且仍然是最新的,則不會重編
- 執行字節碼:執行編譯生成的字節碼(即模塊文件或__init__.py文件中的語句)
有一點值得注意的是,加載不只是發生在導入時,還可以發生在 reload() 時。
3)名字綁定
加載完importee模塊后,作為最后一步,import語句會為 導入的對象 綁定名字,並把這些名字加入到importer模塊的名字空間中。其中,導入的對象 根據導入語句的不同有所差異:
- 如果導入語句為
import obj
,則對象obj可以是包或者模塊 - 如果導入語句為
from package import obj
,則對象obj可以是package的子包、package的屬性或者package的子模塊 - 如果導入語句為
from module import obj
,則對象obj只能是module的屬性
3、更多細節
根據 The import statement 中的描述,以下是導入原理對應的Python偽碼:
import sys
import os.path
def do_import(name):
'''導入'''
parent_pkg_name = name.rpartition('.')[0]
if parent_pkg_name:
parent_pkg = do_import(parent_pkg_name)
else:
parent_pkg = None
return do_find(name, parent_pkg)
def do_find(name, parent_pkg):
'''搜索'''
if not name:
return None
# step 1
if name in sys.modules:
return sys.modules[name]
else:
# step 2
for finder in sys.meta_path:
module = do_load(finder, name, parent_pkg)
if module:
return module
# step 3
src_paths = parent_pkg.__path__ if parent_pkg else sys.path
for path in src_paths:
if path in sys.path_importer_cache:
finder = sys.path_importer_cache[path]
if finder:
module = do_load(finder, name, parent_pkg)
if module:
return module
else:
# handled by an implicit, file-based finder
else:
finder = None
for callable in sys.path_hooks:
try:
finder = callable(path)
break
except ImportError:
continue
if finder:
sys.path_importer_cache[path] = finder
elif os.path.exists(path):
sys.path_importer_cache[path] = None
else:
sys.path_importer_cache[path] = # a finder which always returns None
if finder:
module = do_load(finder, name, parent_pkg)
if module:
return module
raise ImportError
def do_load(finder, name, parent_pkg):
'''加載'''
path = parent_pkg.__path__ if parent_pkg else None
loader = finder.find_module(name, path)
if loader:
return loader.load_module(name)
else:
return None
4、sys.path
正如 『導入過程』 中所述,sys.path是 不在包中的模塊(如import M)的“搜索路徑”。在這種情況下,控制sys.path就能控制模塊的導入過程。
sys.path 是一個路徑名的列表,按照先后順序,其中的路徑主要分為以下四塊:
- 程序主目錄(默認定義):如果是以腳本方式啟動的程序,則為 啟動腳本所在目錄;如果在交互式命令行中,則為 當前目錄。
- PYTHONPATH目錄(可選擴展):以 os.pathsep 分隔的多個目錄名,即環境變量
os.environ['PYTHONPATH']
(類似shell環境變量PATH) - 標准庫目錄(默認定義):Python標准庫所在目錄(與安裝目錄有關)
- .pth文件目錄(可選擴展):以“.pth”為后綴的文件,其中列有一些目錄名(每行一個目錄名),用法參考 site
為了控制sys.path,可以有三種選擇:
- 直接修改sys.path列表
- 使用PYTHONPATH擴展
- 使用.pth文件擴展
六、重新加載
關於導入,還有一點非常關鍵:加載只在第一次導入時發生。這是Python特意設計的,因為加載是個代價高昂的操作。
通常情況下,如果模塊沒有被修改,這正是我們想要的行為;但如果我們修改了某個模塊,重復導入不會重新加載該模塊,從而無法起到更新模塊的作用。有時候我們希望在 運行時(即不終止程序運行的同時),達到即時更新模塊的目的,內建函數 reload() 提供了這種 重新加載 機制。
關鍵字reload
與import
不同:
import
是語句,而reload
是內建函數import
使用 模塊名,而reload
使用 模塊對象(即已被import語句成功導入的模塊)
重新加載(reload(module))有以下幾個特點:
- 會重新編譯和執行模塊文件中的頂層語句
- 會更新模塊的名字空間(字典 M.__dict__):覆蓋相同的名字(舊的有,新的也有),保留缺失的名字(舊的有,新的沒有),添加新增的名字(舊的沒有,新的有)
- 對於由
import M
語句導入的模塊M:調用reload(M)
后,M.x
為 新模塊 的屬性x(因為更新M后,會影響M.x
的求值結果) - 對於由
from M import x
語句導入的屬性x:調用reload(M)
后,x
仍然是 舊模塊 的屬性x(因為更新M后,不會影響x
的求值結果) - 如果在調用
reload(M)
后,重新執行import M
(或者from M import x
)語句,那么M.x
(或者x
)為 新模塊 的屬性x
七、相對導入
嚴格來說,模塊(或包)的導入方式分為兩種:絕對導入 和 相對導入。以上討論的導入方式都稱為 絕對導入,這也是Python2.7的默認導入方式。相對導入是從Python2.5開始引入的,主要用於解決“用戶自定義模塊可能會屏蔽標准庫模塊”的問題(參考 Rationale for Absolute Imports)。
相對導入 使用前導的“.”來指示importee(即被導入模塊或包)與importer(當前導入模塊)之間的相對位置關系。相對導入 只能使用from <> import
風格的導入語句,import <>
風格的導入語句只能用於 絕對導入。(相對導入的更多細節,請參考 PEP 328)
1、導入語句
例如有一個包的布局如下:
pkg/
__init__.py
subpkg1/
__init__.py
modX.py
modY.py
subpkg2/
__init__.py
modZ.py
modA.py
假設當前在文件modX.py或subpkg1/__init__.py中(即當前包為subpkg1),那么下面的導入語句都是相對導入:
from . import modY # 從當前包(subpkg1)中導入模塊modY
from .modY import y # 從當前包的模塊modY中導入屬性y
from ..subpkg2 import modZ # 從當前包的父包(pkg)的包subpkg2中導入模塊modZ
from ..subpkg2.modZ import z # 從當前包的父包的包subpkg2的模塊modZ中導入屬性z
from .. import modA # 從當前包的父包中導入模塊modA
from ..modA import a # 從當前包的父包的模塊modA中導入屬性a
2、導入原理
與絕對導入不同,相對導入的導入原理比較簡單:根據 模塊的__name__
屬性 和 由“.”指示的相對位置關系 來搜索並加載模塊(參考 Relative Imports and __name__)。
3、直接執行
由於相對導入會用到模塊的__name__
屬性,而在直接執行的主模塊中,__name__
值為__main__
(沒有包與模塊的信息),所以在主模塊中:盡量全部使用絕對導入。
如果非要使用相對導入,也可以在頂層包(top-level package)的外部目錄下,以模塊方式執行主模塊:python -m pkg.mod
(假設頂層包為pkg,mod為主模塊,其中使用了相對導入)。(具體參考 PEP 366)