一、概述
模塊(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 packageimport package.moduleimport package.subpackageimport package.subpackage.modulefrom packagae import modulefrom packagae import subpackagaefrom packagae import subpackagae.module或from packagae.subpackagae import modulefrom packagae.module import attributefrom 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)
