1. 程式人生 > >python 模組匯入詳解

python 模組匯入詳解

本文不討論 Python 的匯入機制(底層實現細節),僅討論模組與包,以及匯入語句相關的概念。通常,匯入模組都是使用如下語句:

import ...
import ... as ...
from ... import ...
from ... import ... as ...

 

一般情況下,使用以上語句匯入模組已經夠用的。但是在一些特殊場景中,可能還需要其他的匯入方式。例如 Python 還提供了 __import__ 內建函式和 importlib 模組來實現動態匯入。動態匯入的好處是可以延遲模組的載入,僅在用到模組時才支援匯入動作。

運用 __import__ 函式和 importlib 模組固然能夠實現模組的延遲載入,但其不足之處是,在任何需要用到指定模組的地方都要實現一遍同樣的匯入語句,這樣是不便於維護且非常麻煩的。如果能夠在頂層實現惰性匯入,則是一個更好的選擇,這也是本文最終要討論的點。

在討論一些高階用法之前,首先需要了解一下模組與包的概念。

模組與包

模組 可以理解為是 Python 可以載入並執行的程式碼檔案,程式碼檔案不僅可以是 .py 檔案,還可以是 .so 等其他型別的檔案。Python 只有一個 模組 物件型態,而且所有模組都是這個型態。為了便於組織多個模組並提供一個模組層次結構的命名,Python 提供了 包 的概念。

可以簡單的將包看作是一個檔案系統的目錄,將模組看作是目錄中的程式碼檔案(注意,不能完全地這樣認為,因為包和模組並非僅來自檔案系統,還可以來自壓縮檔案、網路等)。類似於檔案系統的目錄結構,包被分級組織起來,而且包本身也可以包含子包和常規模組。

包其實可以看作是一種特殊的模組。例如常規包(下面會介紹常規包的概念)的目錄中需要包含 __init__.py 檔案,當包被匯入時,該檔案的頂層程式碼被隱式執行,就如同模組匯入時頂層程式碼被執行,該檔案就像是包的程式碼一樣。所以 包是一種特殊的模組。需要記住的是,所有的包都是模組,但不是所有的模組都是包。包中子包和模組都有 __path__

 屬性,具體地說,任何包含 __path__ 屬性的模組被認為是包。所有的模組都有一個名稱,類似於標準屬性訪問語法,子包與他們父包的名字之間用點隔開。

Python 定義了兩種型別的包,即 常規包 和 名稱空間包。常規包是存在於 Python 3.2 及更早版本中的傳統包。常規包即包含 __init__.py 檔案的目錄。當匯入一個常規包時,__init__.py 檔案被隱式執行,而且它定義的物件被繫結到包名稱空間中的名稱。 __init__.py 檔案能包含其他任何模組能夠包含的相同的 Python 程式碼,而且在匯入時,Python 將給模組增加一些額外的屬性。

從 Python 3.3 開始,Python 引入了 名稱空間包 的概念。名稱空間包是不同檔案集的複合,每個檔案集給父包貢獻一個子包,所有的包中都不需要包含 __init__.py 檔案。檔案集可以存於檔案系統的不同位置。檔案集的查詢包含匯入過程中 Python 搜尋的壓縮檔案,網路或者其他地方。名稱空間包可以但也可以不與檔案系統的物件直接對應,它們可以是真實的模組但沒有具體的表述。更新關於名稱空間包的說明可以參考 PEP 420

名稱空間包的 __path__ 屬性與常規包不同,其使用自定義的迭代器型別,遍歷所有包含該命令空間包的路徑。如果他們父包的路徑(或者高階包的 sys.path )改變,它將在下次試圖匯入時在該包中自動重新搜尋包部分。

如有如下目錄結構:

.
├── bar-package
│   └── nsp
│       └── bar.py
└── foo-package
    └── nsp
        └── foo.py

 

則 nsp 即可以是一個名稱空間包,以下是測試程式碼(記得用 Python 3.3 及更高版本執行測試):

import sys
sys.path.extend(['foo-package', 'bar-package'])

import nsp
import nsp.bar
import nsp.foo

print(nsp.__path__)

# 輸出:
# _NamespacePath(['foo-package/nsp', 'bar-package/nsp'])

 

名稱空間包具有如下特性:

  • 1、優先順序最低,在已有版本所有的 import 規則之後
  • 2、包中不必再包含 __init__.py 檔案
  • 3、可以匯入並組織目錄分散的程式碼
  • 4、依賴於 sys.path 中從左到右的搜尋順序

__import__

__import__ 函式可用於匯入模組,import 語句也會呼叫函式。其定義為:

__import__(name[, globals[, locals[, fromlist[, level]]]])

 

引數介紹:

  • name (required): 被載入 module 的名稱
  • globals (optional): 包含全域性變數的字典,該選項很少使用,採用預設值 global()
  • locals (optional): 包含區域性變數的字典,內部標準實現未用到該變數,採用預設值 - local()
  • fromlist (Optional): 被匯入的 submodule 名稱
  • level (Optional): 匯入路徑選項,Python 2 中預設為 -1,表示同時支援 absolute import 和 relative import。Python 3 中預設為 0,表示僅支援 absolute import。如果大於 0,則表示相對匯入的父目錄的級數,即 1 類似於 ‘.’,2 類似於 ‘..’。

使用示例:

# import spam
spam = __import__('spam')

# import spam.ham
spam = __import__('spam.ham')

# from spam.ham import eggs, sausage as saus
_temp = __import__('spam.ham', fromlist=['eggs', 'sausage'])
eggs = _temp.eggs
saus = _temp.sausage

 

模組快取

在執行模組匯入時,Python 的匯入系統會首先嚐試從 sys.modules 查詢。sys.modules 中是所有已匯入模組的一個快取,包括中間路徑。即,假如 foo.bar.baz 被匯入,那麼,sys.modules 將包含進入 foo,foo.bar 和 foo.bar.baz 模組的快取。其實一個 dict 型別,每個鍵都有自己的值,對應相應的模組物件。

匯入過程中,首先在 sys.modules 中查詢模組名稱,如果存在,則返回該模組並結束匯入過程。如果未找到模組名稱,Python 將繼續搜尋模組(從 sys.path 中查詢並載入)。sys.modules 是可寫的,刪除一個鍵會使指定模組的快取實現,下次匯入時又將重新搜尋指定的模組,這類似於模組的 reload。

需要注意的是,如果保持模組物件引用,並使 sys.modules 中快取失效,然後再重新匯入指定的模組,則這兩個模組物件是不相同的。而相比之下,importlib.reload() 重新載入模組時,會使用相同的模組物件,並通過重新執行模組程式碼簡單地重新初始化模組內容。

imp 與 importlib 模組

imp 模組提供了一些 import 語句內部實現的介面。例如模組查詢(find_module)、模組載入(load_module)等等(模組的匯入過程會包含模組查詢、載入、快取等步驟)。可以用該模組來簡單實現內建的 __import__ 函式功能:

import imp
import sys

def __import__(name, globals=None, locals=None, fromlist=None):
    # 首先從快取中查詢
    try:
        return sys.modules[name]
    except KeyError:
        pass

    # 如果模組快取中沒有,則開始從 sys.path 中查詢模組
    fp, pathname, description = imp.find_module(name)

    # 如何找到模組則將其載入
    try:
        return imp.load_module(name, fp, pathname, description)
    finally:
        if fp:
            fp.close()

 

importlib 模組在 python 2.7 被建立,並且僅包含一個函式:

importlib.import_module(name, package=None)

 

這個函式是對 __import__ 的封裝,以用於更加便捷的動態匯入模組。例如用其實現相對匯入:

import importlib

# 類似於 'from . import b'
b = importlib.import_module('.b', __package__)

 

從 python 3 開始,內建的 reload 函式被移到了 imp 模組中。而從 Python 3.4 開始,imp 模組被否決,不再建議使用,其包含的功能被移到了 importlib 模組下。即從 Python 3.4 開始,importlib 模組是之前 imp 模組和 importlib 模組的合集。

惰性匯入

前邊介紹的大部分內容都是為實現惰性匯入做鋪墊,其他的小部分內容僅是延伸而已(就是隨便多介紹了點內容)。惰性匯入即延遲模組匯入,在真正用到模組時才執行模組的匯入動作,如果模組不被使用則匯入動作永遠不會發生。

惰性匯入的需求還是很常見的。一般推薦模組僅在頂層匯入,而有時候在頂層匯入模組並非最好的選擇。比如,一個模組僅在一個函式或者類方法中用到時,則可使用區域性匯入(在區域性作用域中執行匯入),使得僅在函式或方法被執行時才匯入模組,這樣可以避免在頂層名字空間中引入模組變數。再比如,在我工作所負責的專案中,需要用到 pandas 包,而 pandas 包匯入了會佔用一些記憶體(不是很多,但也不算少,幾十兆的樣子),所以當不會用到 pandas 包時,我們希望他不被匯入。我們自己實現的一些包在載入時會很耗時(因為要讀取配置等等,在匯入時就會耗時幾秒到十幾秒的樣子),所以也極其需要惰性匯入的特性。

下面是惰性匯入的簡單實現,可供參考:

import sys
from types import ModuleType


class LazyModuleType(ModuleType):

    @property
    def _mod(self):
        name = super(LazyModuleType, self).__getattribute__("__name__")
        if name not in sys.modules:
            __import__(name)
        return sys.modules[name]

    def __getattribute__(self, name):
        if name == "_mod":
            return super(LazyModuleType, self).__getattribute__(name)

        try:
            return self._mod.__getattribute__(name)
        except AttributeError:
            return super(LazyModuleType, self).__getattribute__(name)

    def __setattr__(self, name, value):
        self._mod.__setattr__(name, value)


def lazy_import(name, package=None):
    if name.startswith('.'):
        if not package:
            raise TypeError("relative imports require the 'package' argument")
        level = 0
        for character in name:
            if character != '.':
                break
            level += 1

        if not hasattr(package, 'rindex'):
            raise ValueError("'package' not set to a string")
        dot = len(package)
        for _ in range(level, 1, -1):
            try:
                dot = package.rindex('.', 0, dot)
            except ValueError:
                raise ValueError("attempted relative import beyond top-level "
                                 "package")

        name = "{}.{}".format(package[:dot], name[level:])

    return LazyModuleType(name)

 

參考資料