Python基礎:模塊

1、概述

模塊(module)和 包(package)是Python用於組織大型程序的利器。html

模塊 是一個由 變量函數 等基本元素組成的功能單元,設計良好的模塊一般是高內聚、低耦合、可複用、易維護的。 是管理模塊的容器,它具備 可嵌套性:一個包能夠包含模塊和其餘包。從文件系統的視角來看,包就是目錄,模塊就是文件。python

從本質上講,一個模塊就是一個獨立的名字空間(namespace),單純的多個模塊只能構成扁平結構的名字空間集;而包的可嵌套性,使得多個模塊能夠呈現出多層次結構的名字空間樹。linux

2、導入語句

若是要在一個模塊A中使用另外一個模塊B(即訪問模塊B的屬性),則必須首先 導入 模塊B。此時模塊A稱爲導入模塊(即importer),而模塊B稱爲被導入模塊(即importee)。shell

導入語句(import statement)有兩種風格:import <>from <> import。對模塊的導入同時支持這兩種風格。緩存

一、基本語法

1)導入模塊module(重命名爲name)markdown

import module [as name]app

>>> 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)ui

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')

二、推薦風格

如下是在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的名字空間變得不可控(極可能一團糟)。

3、模塊

一、模塊名

一個 模塊 就是一個Python源碼文件。若是文件名爲mod.py,那麼模塊名就是mod。

模塊的導入和使用都是藉助模塊名來完成的,模塊名的命名規則與變量名相同。

二、模塊屬性

模塊屬性 是指在模塊文件的全局做用域內,或者在模塊外部(被其餘模塊導入後)能夠訪問的全部對象名字的集合。這些對象名字構成了模塊的名字空間,這個名字空間其實就是全局名字空間(參考 名字空間與做用域)。

模塊的屬性由兩部分組成:固有屬性新增屬性。能夠經過 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__的值)

三、可導出的公有屬性

在 『導入語句』 中描述的基本語法能夠概括爲三類:

  • import module 在導入模塊(importer)中,能夠經過module.*的形式訪問模塊module中的全部屬性
  • from module import attribute 只能經過名字attribute訪問模塊module中的指定屬性module.attribute
  • from 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__中指定的屬性纔是公有屬性。

四、直接執行

模塊能夠在命令行下被直接執行,以模塊mod(對應文件mod.py)爲例:

1)以腳本方式執行

python mod.py <arguments>

2)以模塊方式執行

python -m mod <arguments>

4、包

一個 就是一個含有__init__.py文件的目錄。

包與模塊之間的包含關係是:一個包能夠包含子包或子模塊,但一個模塊卻不能包含子包和子模塊。

一、包名

與模塊名相似,包名的命名規則也與變量名相同。

此外,須要特別注意的是:若是在同一個目錄下,存在兩個同名的包和模塊,那麼導入時只會識別包,而忽略模塊。(參考 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'>

二、包屬性

包屬性與模塊屬性很是類似,也分爲 固有屬性新增屬性

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']

三、可導出的公有屬性

在公有屬性方面,包與模塊的行爲徹底一致,固然別忘了包還有 動態的新增屬性

四、其餘

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.modulefrom packagae.subpackagae import module
  • from packagae.module import attribute
  • from packagae.subpackagae.module import attribute

2)__init__.py

關於包的__init__.py,有如下幾點總結:

  • 通常爲空便可
  • 有時也能夠放置一些初始化代碼,用於在包加載時執行
  • 少數狀況下,還能夠用於定製包的一些屬性(如__all____path__等)

5、導入原理

模塊與包的導入原理幾乎徹底一致,所以下面以模塊爲主進行討論,僅在有顯著差別的地方對包做單獨說明。

一、導入依賴

對於模塊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。

二、導入過程

一個模塊的導入過程主要分三步:搜索加載名字綁定。(具體參考 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的屬性

三、更多細節

根據 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

四、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文件擴展

6、從新加載

關於導入,還有一點很是關鍵:加載只在第一次導入時發生。這是Python特地設計的,由於加載是個代價高昂的操做。

一般狀況下,若是模塊沒有被修改,這正是咱們想要的行爲;但若是咱們修改了某個模塊,重複導入不會從新加載該模塊,從而沒法起到更新模塊的做用。有時候咱們但願在 運行時(即不終止程序運行的同時),達到即時更新模塊的目的,內建函數 reload() 提供了這種 從新加載 機制。

關鍵字reloadimport不一樣:

  • 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

7、相對導入

嚴格來講,模塊(或包)的導入方式分爲兩種:絕對導入相對導入。以上討論的導入方式都稱爲 絕對導入,這也是Python2.7的默認導入方式。相對導入是從Python2.5開始引入的,主要用於解決「用戶自定義模塊可能會屏蔽標準庫模塊」的問題(參考 Rationale for Absolute Imports)。

相對導入 使用前導的「.」來指示importee(即被導入模塊或包)與importer(當前導入模塊)之間的相對位置關係。相對導入 只能使用from <> import風格的導入語句,import <>風格的導入語句只能用於 絕對導入。(相對導入的更多細節,請參考 PEP 328

一、導入語句

例若有一個包的佈局以下:

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

二、導入原理

與絕對導入不一樣,相對導入的導入原理比較簡單:根據 模塊的__name__屬性 和 由「.」指示的相對位置關係 來搜索並加載模塊(參考 Relative Imports and __name__)。

三、直接執行

因爲相對導入會用到模塊的__name__屬性,而在直接執行的主模塊中,__name__值爲__main__(沒有包與模塊的信息),因此在主模塊中:儘可能所有使用絕對導入。

若是非要使用相對導入,也能夠在頂層包(top-level package)的外部目錄下,以模塊方式執行主模塊:python -m pkg.mod(假設頂層包爲pkg,mod爲主模塊,其中使用了相對導入)。(具體參考 PEP 366

相關文章
相關標籤/搜索