首页   注册   登录
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python 学习手册
Python Cookbook
Python 基础教程
Python Sites
PyPI - Python Package Index
http://www.simple-is-better.com/
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
V2EX  ›  Python

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

  •  
  •   chinesehuazhou · 41 天前 · 1271 次点击
    这是一个创建于 41 天前的主题,其中的信息可能已经有所发展或是发生改变。

    在写 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

    25 回复  |  直到 2019-11-20 20:38:44 +08:00
        1
    ggicci   41 天前
    挺抵制这种行为的,说不上来原因。。。(尴尬)
        2
    Kilerd   41 天前   ♥ 3
    什么?都 9102 年了,还在用 pip freeze ?

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

    毫无意义的文章
        3
    chinesehuazhou   41 天前
    @Kilerd 大佬,都 9102 年了,怨气还这么重?
        4
    GPU   41 天前
    @Kilerd #2 大佬, 不是应该分享一下你的实现方式吗?
        5
    conn4575   41 天前 via Android
    完全在误导新人好么。。你见过哪种语言用这种奇葩方式导入依赖的?
        6
    css3   41 天前 via iPhone
    @Kilerd @ggicci
    两位大佬,分享一下你们的解决方法🤝
        7
    css3   41 天前 via iPhone
    @conn4575
    大佬,分享一下你的处理方法
        8
    InkStone   41 天前
    这种时候难道不是该上 docker 么……或者至少也是打包个 venv 吧
        9
    Kilerd   41 天前   ♥ 3
    首先「在写 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 里面的指令就绝对有问题。
    而大环境下,绝大部分人(不知道读者们你们在不在这里面)都会使用,那么这篇文件就存在误导性。
        10
    0x000007b   41 天前
    emmmmm 很多时候不是没导入库的原因,有时候一些特殊的原因比如环境变量,某个不能读取的字符等等会导致报缺失包或者无法解析的错误,容易误判,这时候会徒增排查难度。
        11
    guyeu   41 天前
    这种操作带来的问题绝对比解决的问题多
        12
    ggicci   41 天前
    @css3 我一般抛异常,不解决。。。
        13
    wd   41 天前 via iPhone
    为了写文章而写文章
        14
    Les1ie   40 天前
    除了楼上提到的, 还有点问题
    1. 包名是可控的,如果给了一段代码,直接就 autoinstall,那么攻击者可以构造一个恶意的包传到 pypi,运行的时候 autoinstall 就变成肉鸡了

    2. 不是所有的包名和 pip install 时候的名字都一样的 比如 opencv smb 等包
        15
    krixaar   40 天前
    用这种方法的话,from bs4 import BeautifulSoup 这一行会让 autoinstall 执行什么呢?
        16
    xlui   40 天前
    槽点太多,一时没反应过来。

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

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

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

    这些都是头疼的问题。最好还是让第三方团队提供 requirements.txt ,然后在虚拟环境里跑,这样出问题的概率会小一些。
        20
    chinesehuazhou   39 天前
    @superrichman 确实是考虑用维护 requirements.txt 的方式。文章里我是考虑话题本身,比考虑这个需求更多,虽然是它诱使我想到题目的话题。可能是我想多了,大家都没有这样的痛点
        21
    matsuz   34 天前
    自动导入这个功能并不好实现,Python 不像 Java 那样,你上传了一个 org.example.xxx 的包你就直接导入 org.example.xxx 就行了

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

    Python 的包在 pypi 上和你实际导入的名称不一定一样……
        22
    frostming   33 天前
    pip install 这种环境搭建阶段的事,放到运行阶段做,是个非常可怕的主意,脏到不行。我列下能想到的不好的地方:
    1. 可能需要 sudo 权限
    2. 运行结果不可预测,你完全不知道这个脚本跑下去环境会变成什么样
    3. 包名可能与导入名不同,这种情况怎么办?
    我认为写代码也要有一定审美,而不是为了解决问题而无所不用其极。
    应该让脚本提供方也提供 requirements.txt ,或者直接把脚本打成一个 python 包,要用就安装进来。出了什么包不存在的问题,那是脚本提供方的责任。
        23
    chinesehuazhou   28 天前
    @frostming 单纯看项目的话,其实我们没有纠结,就是手工维护 requirements.txt 的。
    关于运行期自动导入,作为外延的话题,不妨思考一下,出现问题尝试解决下,说不定有办法解决或者绕开呢?
    权限问题不难。运行结果不可预测,这可能需要踩些坑才知道。
    至于导入包名不同,这不是自动导入时才会遇到的问题,生成依赖文件时也会遇到。pipreqs 和 pigar 这些管理依赖文件的库都遇到了,其实可以借鉴,它们似乎用了穷举法吧?
        24
    wzwwzw   26 天前
    你这个解决问题的思路就像,在连接 redis 的时候发现无法连接,直接在本机装一个 redis。
        25
    pjntt   18 天前
    这个问题点是在于在布署的时候,缺少 requirements.txt 这个文件,没办法安装运行环境,导致项目无法运行。
    楼主的想法是在开发的时候加入一些自动化操作来解决万一缺少 requirements.txt 这个文件也能让项目正常运行的方法。
    楼上各位大佬可以针对这个问题讨论一下有没有什么办法解决,运维人员因缺少相应的布署文档有时候很让人抓狂的。
    关于   ·   FAQ   ·   API   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   4353 人在线   最高记录 5043   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.3 · 27ms · UTC 06:04 · PVG 14:04 · LAX 22:04 · JFK 01:04
    ♥ Do have faith in what you're doing.