在寫 Python 項目的時候,我們可能經常會遇到導入模塊失敗的錯誤:
ImportError: No module named 'xxx'
或者
ModuleNotFoundError: No module named 'xxx'
導入失敗問題,通常分為兩種:一種是導入自己寫的模塊(即以 .py 為後綴的文件),另一種是導入三方庫。本文主要討論第二種情況。
在編寫代碼的時候,如果我們需要使用某個三方庫(如 requests),但不確定實際運行的環境是否裝了它,那麼可以這樣:
try:
import requests
except ImportError:
import os
os.system('pip install requests')
import requests
這樣寫的效果是,如果找不到 requests 庫,就先安裝,再導入。
在某些開源項目中,我們可能還會看到如下的寫法(以 json 為例):
try:
import simplejson as json
except ImportError:
import json
這樣寫的效果是,優先導入三方庫 simplejson,如果找不到,那就使用內置的標准庫 json。
這種寫法的好處是不需要導入額外的庫,但它有個缺點,即需要保證那兩個庫在使用上是兼容的,如果在標准庫中找不到替代的庫,那就不可行了。
如果真找不到兼容的標准庫,也可以自己寫一個模塊(如 my_json.py),實現想要的東西,然後在 except 語句中導入它。
try:
import simplejson as json
except ImportError:
import my_json as json
以上的思路是針對開發中的項目,但是它有幾個不足:
1、在代碼中對每個可能缺失的三方庫都 pip install,並不可取;
2、某個三方庫無法被標准庫或自己手寫的庫替代,該怎麼辦?
3、已成型的項目,不允許做這些修改怎麼辦?
所以這裡的問題是:有一個項目,想要部署到新的機器上,它涉及很多三方庫,但是機器上都沒有預裝,該怎麼辦?
對於一個合規的項目,按照約定,通常它會包含一個“requirements.txt ”文件 ,記錄了該項目的所有依賴庫及其所需的版本號。這是在項目發布前,使用命令pip freeze > requirements.txt 生成的。
使用命令pip install -r requirements.txt (在該文件所在目錄執行,或在命令中寫全文件的路徑),就能自動把所有的依賴庫給裝上。
但是,如果項目不合規,或者由於其它倒霉的原因,我們沒有這樣的文件,又該如何是好?
一個笨方法就是,把項目跑起來,等它出錯,遇到一個導庫失敗,就手動裝一個,然後再跑一遍項目,遇到導庫失敗就裝一下,如此循環……(此處省略 1 萬句髒話)……
在不修改原有的代碼的情況下,在不需要“requirements.txt”文件的情況下,有沒有辦法自動導入所需要的庫呢?
當然有!先看看效果:
我們以 tornado 為例,第一步操作可看出,我們沒有裝過 tornado,經過第二步操作後,再次導入 tornado 時,程序會幫我們自動下載並安裝好 tornado,所以不再報錯。
autoinstall 是我們手寫的模塊,代碼如下:
# 以下代碼在 python 3.6.1 版本驗證通過
import sys
import os
from importlib import import_module
class AutoInstall():
_loaded = set()
@classmethod
def find_spec(cls, name, path, target=None):
if path is None and name not in cls._loaded:
cls._loaded.add(name)
print("Installing", name)
try:
result = os.system('pip install {}'.format(name))
if result == 0:
return import_module(name)
except Exception as e:
print("Failed", e)
return None
sys.meta_path.append(AutoInstall)
Python 3 的 import 機制在查找過程中,大致順序如下:
其中要注意,sys.meta_path 在不同的 Python 版本中有所差異,比如它在 Python 2 與 Python 3 中差異很大;在較新的 Python 3 版本(3.4+)中,自定義的加載器需要實現find_spec方法,而早期的版本用的則是find_module。
以上代碼是一個自定義的類庫加載器 AutoInstall,可以實現自動導入三方庫的目的。需要說明一下,這種方法會“劫持”所有新導入的庫,破壞原有的導入方式,因此也可能出現一些奇奇怪怪的問題,敬請留意。
sys.meta_path 屬於 Python 探針 的一種運用。探針,即import hook,是 Python 幾乎不受人關注的機制,但它可以做很多事,例如加載網絡上的庫、在導入模塊時對模塊進行修改、自動安裝缺失庫、上傳審計信息、延遲加載等等。