Python基礎:模塊


一、概述

模塊(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.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__中指定的屬性才是公有屬性。

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.modulefrom 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() 提供了這種 重新加載 機制。

關鍵字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

七、相對導入

嚴格來說,模塊(或包)的導入方式分為兩種:絕對導入相對導入。以上討論的導入方式都稱為 絕對導入,這也是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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM