淺談 Python 的模塊導入

淺談 Python 的模塊導入

本文不討論 Python 的導入機制(底層實現細節),僅討論模塊與包,以及導入語句相關的概念。一般,導入模塊都是使用以下語句:html

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

通常狀況下,使用以上語句導入模塊已經夠用的。可是在一些特殊場景中,可能還須要其餘的導入方式。例如 Python 還提供了 __import__ 內建函數和 importlib 模塊來實現動態導入。動態導入的好處是能夠延遲模塊的加載,僅在用到模塊時才支持導入動做。python

運用 __import__ 函數和 importlib 模塊當然可以實現模塊的延遲加載,但其不足之處是,在任何須要用到指定模塊的地方都要實現一遍一樣的導入語句,這樣是不便於維護且很是麻煩的。若是可以在頂層實現惰性導入,則是一個更好的選擇,這也是本文最終要討論的點。git

在討論一些高級用法以前,首先須要瞭解一下模塊與包的概念。github

模塊與包

模塊 能夠理解爲是 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 420ui

命名空間包的 __path__ 屬性與常規包不一樣,其使用自定義的迭代器類型,遍歷全部包含該命令空間包的路徑。若是他們父包的路徑(或者高階包的 sys.path )改變,它將在下次試圖導入時在該包中自動從新搜索包部分。spa

若有以下目錄結構:

.
├── 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'])

命名空間包具備以下特性:

  • 一、優先級最低,在已有版本全部的 import 規則以後
  • 二、包中沒必要再包含 __init__.py 文件
  • 三、能夠導入並組織目錄分散的代碼
  • 四、依賴於 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)

參考資料

相關文章
相關標籤/搜索