Python 中如何实现自动导入缺失的库?

2019-10-28 20:15:16 +08:00
 chinesehuazhou

在写 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.meta_path 在不同的 Python 版本中有所差异,比如它在 Python 2 与 Python 3 中差异很大;在较新的 Python 3 版本( 3.4+)中,自定义的加载器需要实现find_spec 方法,而早期的版本用的则是find_module

以上代码是一个自定义的类库加载器 AutoInstall,可以实现自动导入三方库的目的。需要说明一下,这种方法会“劫持”所有新导入的库,破坏原有的导入方式,因此也可能出现一些奇奇怪怪的问题,敬请留意。

sys.meta_path 属于 Python 探针的一种运用。探针,即import hook,是 Python 几乎不受人关注的机制,但它可以做很多事,例如加载网络上的库、在导入模块时对模块进行修改、自动安装缺失库、上传审计信息、延迟加载等等。

限于篇幅,我们不再详细展开了。最后小结一下:

参考资料:

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

5457 次点击
所在节点    Python
27 条回复
matsuz
2019-11-04 15:43:13 +08:00
自动导入这个功能并不好实现,Python 不像 Java 那样,你上传了一个 org.example.xxx 的包你就直接导入 org.example.xxx 就行了

比如说在 Python 中,你想用 umsgpack 这个库,你需要 pip install u-msgpack-python 然后 import umsgpack

Python 的包在 pypi 上和你实际导入的名称不一定一样……
frostming
2019-11-05 21:58:56 +08:00
pip install 这种环境搭建阶段的事,放到运行阶段做,是个非常可怕的主意,脏到不行。我列下能想到的不好的地方:
1. 可能需要 sudo 权限
2. 运行结果不可预测,你完全不知道这个脚本跑下去环境会变成什么样
3. 包名可能与导入名不同,这种情况怎么办?
我认为写代码也要有一定审美,而不是为了解决问题而无所不用其极。
应该让脚本提供方也提供 requirements.txt ,或者直接把脚本打成一个 python 包,要用就安装进来。出了什么包不存在的问题,那是脚本提供方的责任。
chinesehuazhou
2019-11-10 14:48:00 +08:00
@frostming 单纯看项目的话,其实我们没有纠结,就是手工维护 requirements.txt 的。
关于运行期自动导入,作为外延的话题,不妨思考一下,出现问题尝试解决下,说不定有办法解决或者绕开呢?
权限问题不难。运行结果不可预测,这可能需要踩些坑才知道。
至于导入包名不同,这不是自动导入时才会遇到的问题,生成依赖文件时也会遇到。pipreqs 和 pigar 这些管理依赖文件的库都遇到了,其实可以借鉴,它们似乎用了穷举法吧?
wzwwzw
2019-11-12 14:09:11 +08:00
你这个解决问题的思路就像,在连接 redis 的时候发现无法连接,直接在本机装一个 redis。
pjntt
2019-11-20 20:38:44 +08:00
这个问题点是在于在布署的时候,缺少 requirements.txt 这个文件,没办法安装运行环境,导致项目无法运行。
楼主的想法是在开发的时候加入一些自动化操作来解决万一缺少 requirements.txt 这个文件也能让项目正常运行的方法。
楼上各位大佬可以针对这个问题讨论一下有没有什么办法解决,运维人员因缺少相应的布署文档有时候很让人抓狂的。
Daletxt
2020-04-23 20:23:41 +08:00
我是维护自己的项目想到这个问题的,平时生产环境都是用一个第三方,安装一个第三方,直到有次手贱想卸载升级一下 python 、anaconda 、包包们,需要重新安装这么多第三方包的,想过自动安装最简单的逻辑是那个 try import except os.system('pip install ...')的,确实有没考虑到的。其实最稳妥的方式还是要维护好 requirements.txt 文件,或者穷举所有包已经他们 pip 安装时的名称,但感觉不够智能,目前先穷举 requirements.txt ,再继续探索下。
chinesehuazhou
2020-04-25 12:59:05 +08:00
@Daletxt 维护 requirements.txt 算是比较主流。我不习惯用虚拟环境,有几个库可以方便维护依赖文件。
Python 依赖库管理哪家强? pip 、pipreqs 、pigar 、pip-tools 、pipdeptree 任君挑选
https://mp.weixin.qq.com/s/bdeUDLihiuwZVmmquRYEug

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/613783

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX