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

5256 次点击
所在节点    Python
27 条回复
ggicci
2019-10-28 21:38:04 +08:00
挺抵制这种行为的,说不上来原因。。。(尴尬)
Kilerd
2019-10-28 22:50:54 +08:00
什么?都 9102 年了,还在用 pip freeze ?

居然还有用 os.system() 来跑 pip install 的
这年头谁还不装个 虚拟环境啊

毫无意义的文章
chinesehuazhou
2019-10-28 23:29:49 +08:00
@Kilerd 大佬,都 9102 年了,怨气还这么重?
GPU
2019-10-29 00:08:21 +08:00
@Kilerd #2 大佬, 不是应该分享一下你的实现方式吗?
conn4575
2019-10-29 01:01:05 +08:00
完全在误导新人好么。。你见过哪种语言用这种奇葩方式导入依赖的?
css3
2019-10-29 08:16:57 +08:00
@Kilerd @ggicci
两位大佬,分享一下你们的解决方法🤝
css3
2019-10-29 08:17:40 +08:00
@conn4575
大佬,分享一下你的处理方法
InkStone
2019-10-29 09:32:59 +08:00
这种时候难道不是该上 docker 么……或者至少也是打包个 venv 吧
Kilerd
2019-10-29 09:54:04 +08:00
首先「在写 Python 项目的时候,我们可能经常会遇到导入模块失败的错误」 这个概念就是错的,为什么会出现先使用再引入的情况呢?
正常开发场景不是先引入包再使用吗?
即便是没有包的情况,都是应该在启动前用 pip 或者类似的工具引入后再启动吧,而不是在运行时进行这样的操作。


其次。os.system 这种直接调用 shell 的代码,基本上都过不了大部分的 code review。
```
try:
import requests
except ImportError:
import os
os.system('pip install requests')
import requests
```
这串代码跑完真的就有 requests 了吗? i doubt that.


第三。try import one except import other 这个场景基本用来做兼容包的问题,这点倒是讲得没有错。但是大部分时候都是因为某某平台上面有一个比较高效的兼容实现,需要优先级高的引入。例如 unix like 环境下的异步库 uvloop

```
try:
import uvloop as asyncio
except:
import asyncio
```


第四再回到 os.system 执行 pip 这点。 问题来了。执行的是哪个 pip 呢?
这点其实就回到了一个大问题
「你写 python 用不用 venv 管理软件 `python3 -m venv` `pyenv` `virtualenv` 」
「你用不用依赖管理软件,pipenv,poetry,pyflow 」

在用了上述任何一个软件来管理 python 环境或者 python 库,那么 os.system 里面的指令就绝对有问题。
而大环境下,绝大部分人(不知道读者们你们在不在这里面)都会使用,那么这篇文件就存在误导性。
0x000007b
2019-10-29 10:14:12 +08:00
emmmmm 很多时候不是没导入库的原因,有时候一些特殊的原因比如环境变量,某个不能读取的字符等等会导致报缺失包或者无法解析的错误,容易误判,这时候会徒增排查难度。
guyeu
2019-10-29 11:12:52 +08:00
这种操作带来的问题绝对比解决的问题多
ggicci
2019-10-29 12:01:28 +08:00
@css3 我一般抛异常,不解决。。。
wd
2019-10-29 12:55:32 +08:00
为了写文章而写文章
Les1ie
2019-10-29 14:40:56 +08:00
除了楼上提到的, 还有点问题
1. 包名是可控的,如果给了一段代码,直接就 autoinstall,那么攻击者可以构造一个恶意的包传到 pypi,运行的时候 autoinstall 就变成肉鸡了

2. 不是所有的包名和 pip install 时候的名字都一样的 比如 opencv smb 等包
krixaar
2019-10-29 17:42:06 +08:00
用这种方法的话,from bs4 import BeautifulSoup 这一行会让 autoinstall 执行什么呢?
xlui
2019-10-29 19:39:13 +08:00
槽点太多,一时没反应过来。

这就是传说中的没有需求就要制造需求吗?巧妙的解决了一个想象中的 Bug ? Python 怎么着你了要这样子黑?
chinesehuazhou
2019-10-29 22:49:52 +08:00
懒得 @人或者怼人了,回应几点问题:
1、没有这样的需求?没有引包失败的情况?---- 这是在真实项目中遇到的(仍在开发中),简单来说,会在很多机器上(操作系统多样)部署同一套类似测试框架的程序,有调度器分发脚本到它们上面执行。多对多的关系。脚本总数会有几千个,由其它团队提供,而它们用到什么三方库,暂不可知。我承认文章不完全能解决这个问题,但它确实存在,完全有去思考的价值。
2、那些机器上的 python、pip 以及源都是一样的。吐槽这点的,难道你用哪个版本自己不知道么?还要人教你怎么区分么?
3、除了练习,没用过虚拟环境,真没有用虚拟环境来管理依赖的习惯。
4、再强调一遍有价值的内容:如何在 Python 中实现自动安装三方库的问题?(同时考虑版本问题)
chinesehuazhou
2019-10-29 23:00:04 +08:00
5、或者使用类似 java 的 maven 方式,来管理依赖?但好像并没有。https://www.zhihu.com/question/20396564/answer/404836350
superrichman
2019-10-30 00:33:30 +08:00
按你的需求来看,要是第三方团队的不同脚本存在依赖同一个库的多个版本问题,单纯的用 pip 就不好解决了,可能还得配合虚拟环境或者 docker。

另外提一下,要是不用虚拟环境,在 linux 环境里普通用户是不能直接 pip install 安装模块的,得考虑加 sudo 或者--user。

有一些模块用 pip 安装的名字可能和实际代码里 import 的名字不相同,你可能要去维护一个字典来识别这些特殊的库。

这些都是头疼的问题。最好还是让第三方团队提供 requirements.txt ,然后在虚拟环境里跑,这样出问题的概率会小一些。
chinesehuazhou
2019-10-30 21:21:41 +08:00
@superrichman 确实是考虑用维护 requirements.txt 的方式。文章里我是考虑话题本身,比考虑这个需求更多,虽然是它诱使我想到题目的话题。可能是我想多了,大家都没有这样的痛点

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

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

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

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

© 2021 V2EX