1. 程式人生 > >由淺入深:Python 中如何實現自動匯入缺失的庫?

由淺入深:Python 中如何實現自動匯入缺失的庫?

在寫 Python 專案的時候,我們可能經常會遇到匯入模組失敗的錯誤:ImportError: No module named 'xxx' 或者 ModuleNotFoundError: No module named 'xxx'

匯入失敗問題,通常分為兩種:一種是匯入自己寫的模組(即以 .py 為字尾的檔案),另一種是匯入三方庫。本文主要討論第二種情況,今後有機會,我們再詳細討論其它的相關話題。

解決匯入 Python 庫失敗的問題,其實關鍵是在執行環境中裝上缺失的庫(注意是否是虛擬環境),或者使用恰當的替代方案。這個問題又分為三種情況:

一、單個模組中缺失的庫

在編寫程式碼的時候,如果我們需要使用某個三方庫(如 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)

這段程式碼中使用了sys.meta_path ,我們先列印一下,看看它是個什麼東西?

Python 3 的 import 機制在查詢過程中,大致順序如下:

  • 在 sys.modules 中查詢,它快取了所有已匯入的模組
  • 在 sys.meta_path 中查詢,它支援自定義的載入器
  • 在 sys.path 中查詢,它記錄了一些庫所在的目錄名
  • 若未找到,則丟擲 ImportError 異常

其中要注意,sys.meta_path 在不同的 Python 版本中有所差異,比如它在 Python 2 與 Python 3 中差異很大;在較新的 Python 3 版本(3.4+)中,自定義的載入器需要實現find_spec 方法,而早期的版本用的則是find_module

以上程式碼是一個自定義的類庫載入器 AutoInstall,可以實現自動匯入三方庫的目的。需要說明一下,這種方法會“劫持”所有新匯入的庫,破壞原有的匯入方式,因此也可能出現一些奇奇怪怪的問題,敬請留意。

sys.meta_path 屬於 Python 探針的一種運用。探針,即import hook,是 Python 幾乎不受人關注的機制,但它可以做很多事,例如載入網路上的庫、在匯入模組時對模組進行修改、自動安裝缺失庫、上傳審計資訊、延遲載入等等。

限於篇幅,我們不再詳細展開了。最後小結一下:

  • 可以用 try...except 方式,實現簡單的三方庫匯入或者替換
  • 已知全部缺失的依賴庫時(如 requirements.txt),可以手動安裝
  • 利用 sys.meta_path,可以自動匯入任意的缺失庫

參考資料:

https://github.com/liuchang0812/slides/tree/master/pycon2015cn

http://blog.konghy.cn/2016/10/25/python-import-hook/

https://docs.python.org/3/library/sys.html#sys.meta_path

公眾號【Python貓】, 本號連載優質的系列文章,有喵星哲學貓系列、Python進階系列、好書推薦系列、技術寫作、優質英文推薦與翻譯等等,歡迎關注哦