Python 中 import 的機制與實現

發表於2015-10-30

本文所涉及到的程式碼在github上。

概述

Python 是一門優美簡單、功能強大的動態語言。在剛剛接觸這門語言時,我們會被其優美的格式、簡潔的語法和無窮無盡的類庫所震撼。在真正的將python應用到實際的專案中,你會遇到一些無法避免的問題。最讓人困惑不解的問題有二類,一個 編碼問題,另一個則是引用問題。

本文主要討論關於Python中import的機制與實現、以及介紹一些有意思的Python Hooks。

Python 類庫引入機制

首先,看一個簡單的例子:

現在,考慮一下:

  1. 當我們執行main.py的時候,會發生什麼事情?
  2. 在main.py檔案執行到 import string 的時候,直譯器匯入的string類庫是當前資料夾下的string.py還是系統標準庫的string.py呢?
  3. 如果明確的指明⾃己要引⼊的類庫?

為了搞清楚上面的問題,我們需要了解關於Python類庫引入的機制。

Python的兩種引入機制

Python 提供了二種引入機制:

  1. relative import
  2. absolute import

relative import

relative import 也叫作相對引入,在Python2.5及之前是預設的引入方法。它的使用方法如下:

這種引入方式使用一個點號來標識引入類庫的精確位置。與linux的相對路徑表示相似,一個點表示當前目錄,每多一個點號則代表向上一層目錄。

相對引入,那麼我們需要知道相對什麼來引入。相對引入使用被引入檔案的 __name__ 屬性來決定該檔案在整個包結構的位置。那麼如果檔案的__name__沒有包含任何包的資訊,例如 __name__ 被設定為了__main__,則認為其為‘top level script’,而不管該檔案的位置,這個時候相對引入就沒有引入的參考物。如上面的程式所示,當我們執行 python main.py 時,Python直譯器會丟擲 ValueError: Attempted relative import in non-package的異常。

為了解決這個問題,PEP 0366 — Main module explicit relative imports提出了一個解決方案。允許使用者使用python -m ex2.main的方式,來執行該檔案。在這個方案下,引入了一個新的屬性__package__

absolute import

absolute import 也叫作完全引入,非常類似於Java的引入進位制,在Python2.5被完全實現,但是是需要通過 from __future__ import absolute_import 來開啟該引入進位制。在Python2.6之後以及Python3,完全引用成為Python的預設的引入機制。它的使用方法如下:

要注意的是,需要從包目錄最頂層目錄依次寫下,而不能從中間開始。

在使用該引入方式時,我們碰到比較多的問題就是因為位置原因,Python找不到相應的庫檔案,丟擲ImportError的異常。讓我們看一個完全引用的例子:

我們嘗試著去執行main.py檔案,Python直譯器會丟擲ImportError。那麼我們如何解決這個問題呢?

首先,我們也可以使用前文所述的module的方式去執行程式,通過-m引數來告訴直譯器 __package__ 屬性。如下:

另外,我們還有一個辦法可以解決該問題,在描述之前,我們介紹一個關於Python的非常有用的小知識:Python直譯器會自動將當前工作目錄新增到PYTHONPATH。如下所示,可以看到我們列印出的 sys.path 已經包含了當前工作目錄。

瞭解了Python直譯器的這個特性後,我們就可以解決完全引用的找不到類庫的問題:執行的時候,讓直譯器自動的將類庫的目錄新增到PYTHONPATH中。

我們可以在頂層目錄中新增一個run_ex3.py的檔案,檔案內容和執行結果如下,可以看到Python直譯器正確的執行了ex3.main檔案。

一些實踐經驗

相對引用還是絕對引用?

上面介紹了Python的兩種引用方式,都可以解決引入歧義的問題。那我們應該使用哪一種呢?

先說明一下Python的預設引用方式,在Python2.4及之前,Python只有相對引用這一種方式,在Python2.5中實現了絕對引用,但預設沒有開啟,需要使用者自己指定使用該引用方式。在之後的版本和Python3版本,絕對引用已經成為預設的引用方式。

其次,二種引用方式各有利弊。絕對引用程式碼更加清晰明瞭,可以清楚的看到引入的包名和層次,但是,當包名修改的時候,我們需要手動修改所有的引用程式碼。相對引用則比較精簡,不會被包名修改所影響,但是可讀性較差,不如完全引用清晰。

最後,對於兩種引用的方式選擇,還是有爭論的。在PEP8中,Python官方推薦的是絕對引用,詳細理由可以參考這兒

Absolute imports are recommended, as they are usually more readable and tend to be better behaved (or at least give better error messages) if the import system is incorrectly configured (such as when a directory inside a package ends up on sys.path ):

However, explicit relative imports are an acceptable alternative to absolute imports, especially when dealing with complex package layouts where using absolute imports would be unnecessarily verbose:

Standard library code should avoid complex package layouts and always use absolute imports. Implicit relative imports should never be used and have been removed in Python 3.

規範打包釋出

為了別人使用自己程式碼的方便,應該儘量使用規範的包分發機制。為自己的Python包編寫正確的setup.py檔案,新增相應的README.md檔案。對於提供一些可執行命令的包,則可以使用 console_entrypoint 的機制來提供。因為打包和分發不是本文重點,不再詳細敘述,大家可以檢視官方文件。

使用virtualenv管理包依賴

在使用Python的時候,儘量使用virtualenv來管理專案,所有的專案從編寫到執行都在特定的virtualenv中。並且為自己的專案生成正確的依賴描述檔案。

關於virtualenv的用法,可以參考我之前的一篇文章virtualenv教程

Python import實現

Python 提供了 import 語句來實現類庫的引用,下面我們詳細介紹當執行了 import 語句的時候,內部究竟做了些什麼事情。

當我們執行一行  from package import module as mymodule 命令時,Python直譯器會查詢package這個包的module模組,並將該模組作為mymodule引入到當前的工作空間。所以import語句主要是做了二件事:

  1. 查詢相應的module
  2. 載入module到local namespace

下面我們詳細瞭解python是如何查詢模組的。

查詢module的過程

在import的第一個階段,主要是完成了查詢要引入模組的功能,這個查詢的過程如下:

  1. 檢查 sys.modules (儲存了之前import的類庫的快取),如果module被找到,則⾛到第二步。
  2. 檢查 sys.meta_path。meta_path 是一個 list,⾥面儲存著一些 finder 物件,如果找到該module的話,就會返回一個finder物件。
  3. 檢查⼀些隱式的finder物件,不同的python實現有不同的隱式finder,但是都會有 sys.path_hooks, sys.path_importer_cache 以及sys.path。
  4. 丟擲 ImportError。

sys.modules

對於第一步中sys.modules,我們可以開啟Python來實際的檢視一下其內容:

可以看到sys.modules已經儲存了一些包的資訊,由這些資訊,我們就可以直接知道要查詢的包的位置等資訊。

finder、loader和importer

在上文中,我們提到了sys.meta_path中保證了一些finder物件。在python中,不僅定義了finder的概念,還定義了loader和importor的概念。

  • finder的任務是決定自己是否根據名字找到相應的模組,在py2中,finder物件必須實現find_module()方法,在py3中必須要實現find_module()或者find_loader()方法。如果finder可以查詢到模組,則會返回一個loader物件(在py3.4中,修改為返回一個module specs)。
  • loader則是負責載入模組,它必須實現一個load_module()的方法。
  • importer 則指一個物件,實現了finder和loader的方法。因為Python是duck type,只要實現了方法,就可以認為是該類。

sys.meta_path

在Python查詢的時候,如果在sys.modules沒有查詢到,就會依次呼叫sys.meta_path中的finder物件。預設的情況下,sys.meta_path是一個空列表,並沒有任何finder物件。

我們可以向sys.meta_path中新增一些定義的finder,來實現對Python載入模組的修改。比如下例,我們實現了一個會將每次載入包的資訊列印出來的finder。

當我們執行的時候,就可以看到系統載入socket包時所發生的事情。

sys.path hook

Python import的hook分為二類,一類是上一章節已經描述的meta hook,另一類是 path hook。

當處理sys.path(或者package.path)時,就會呼叫對應的一部分的 Pack hook。Path Hook是通過向sys.path_hooks 中新增一個importer生成器來註冊的。

sys.path_hooks 是由可被呼叫的物件組成,它會順序的檢查以決定他們是否可以處理給定的sys.path的一項。每個物件會使用sys.path項的路徑來作為引數被呼叫。如果它不能處理該路徑,就必須丟擲ImportError,如果可以,則會返回一個importer物件。之後,不會再嘗試其它的sys.path_hooks物件,即使前一個importer出錯了。

詳細可以參考registering-hooks

python import hooks

在介紹完Python的引用機制與一些實現方法後,接下來我們介紹一些關於如何根據自己的需求來擴充套件Python的引用機制。

在開始詳細介紹前,給大家展示一個實用性不高,但是很有意思的例子:讓Python在執行程式碼的時候自動安裝缺失的類庫。我們會實現一個autoinstall的模組,只要import了該模組,就可以開啟該功能。如下所示,我們嘗試引入tornado庫的時候,iPython會提示我們沒有安裝。然後,我們引入了autoinstall,再嘗試引入tornado,iPython就會自動的安裝tornado庫。

這個功能的實現其實很簡單,利用了sys.meta_path。autoinstall的全部程式碼如下:

import hook的重要性

我們為什麼需要Python import的hook呢?使用import的hook可以讓我們做到很多事情,比如說當我們的Python包儲存在一個非標準的檔案中,或者Python程式儲存在網路資料庫中,或者像py2exe一樣將Python程式打包成了一個檔案,我們需要一種方法來正確的解析它們。

其次,我們希望在Python載入類庫的時候,可以額外的做一些事情,比如上傳審計資訊,比如延遲載入,比如自動解決上例的依賴未安裝的問題。

所以,import系統的Hook技術是值的花時間學習的。

如何實現import hooks

Python提供了一些方法,讓我們可以在程式碼中動態的呼叫import。主要有如下幾種:

  1. __import__ : Python的內建函式
  2. imputil : Python的import工具庫,在py2.6被宣告廢棄,py3中徹底移除。
  3. imp : Python2 的一個import庫,py3中移除
  4. importlib : Python3 中最新新增,backport到py2.7,但只有很小的子集(只有一個函式)。

Python2 所有關於import的庫的列表參見Importing Modules。Python3 的可以參考Importing Modules PEP 0302 — New Import Hooks 提案詳細的描述了importlib的目的、用法。

一些Hook示例

Lazy化庫引入

使用Import Hook,我們可以達到Lazy Import的效果,當我們執行import的時候,實際上並沒引入該庫,只有真正的使用這個庫的時候,才會將其引入到當前工作空間。 具體的程式碼可以參考github。 實現的效果如下:

它的實現也很簡單:

Flask 外掛庫統一入口

使用過Flask的同學都知道,Flask的對於外掛提供了統一的入口。比如說我們安裝了Flask_API這個庫,然後我們可以直接 import flask_api 來使用這個庫,同時Flask還允許我們採用 import flask.ext.api 的方式來引用該庫。

這裡Flask就是使用了import 的hook,當引入flask.ext的包時,就自動的引用相應的庫。Flask實現了一個叫ExtensionImporter的類,這個類實現了find_module和load_module程式碼實現如下github

然後在Flask的ext目錄下的__init__.py檔案中,初始化了該Importer。

總結

希望堅持閱讀到本處的你,能明白Python import的用法、實現和改造方法。準備倉促,難免會有錯誤,歡迎大家指正和PR。

本文使用CC-BY-SA協議。

附錄

  1. https://www.python.org/dev/peps/pep-0302/
  2. https://www.python.org/dev/peps/pep-0338/
  3. https://www.python.org/dev/peps/pep-0328/
  4. https://www.python.org/dev/peps/pep-0366/
  5. https://github.com/noahmorrison/limp
  6. https://github.com/mitsuhiko/flask

相關文章