Python模块管理与import导入机制解析

我们在学 python的时候,大多数都是从print("hello,world")开始,这一行代码,敲开了每一位工程师新世界的大门!

然后我们开始学语法、变量、函数、条件控制、数据结构、面向对象,然后迫不及待的与bug过招。在这个过程中,有一位朋友一直默默陪伴着我们,但是我们却从来没有关注过它。他就是我们的 import 兄弟

import os

在我们的代码中,它扮演这不可或缺的角色。但是却很少有人真正懂它。正是因为有了他,python的强大之处才能得以发挥,今天让我们一起好好了解一下它。

1. 模块化编程

在我们真实的项目中,代码量可能达到几十万、几百万行。如果我们把这几十万、几百万行代码都写在一个文件里面。那后果是非常严重的,首先我们的代码文件将会非常大,其次是想通过肉眼去找到我们的代码,难度也是非常大,更别说看懂其中的代码逻辑了。

出现了模块化编程的想法。模块化编程有助于开发者统筹兼顾和分工协作,并提升代码灵活性和可维护性。比如说众多工程师共同开发同一个系统。A做登录功能的逻辑,B做注册功能的逻辑,C做用户管理的逻辑。这些代码是分离的,通过模块化组装的方式把他继承到同一个系统中。

模块化编程:将一个完整系统的代码,拆分成一个一个小模块

举个例子:

(1)非模块化项目:项目里面只有一个py文件 main.py

def login():
    #这里完成登录逻辑的代码编辑
    pass

def register():
    # 这里完成注册逻辑的代码编写
    pass

def user_manage():
    # 这里完成用户管理的代码编写
    pass

if __main__ == "__main__":
    login()
    register()
    user_manage()

(2)模块化的项目:项目中包含 main.pylogin.pyregister.pyuser_manage.py

  • login.py
def login():
    #这里完成登录逻辑的代码编辑
    pass
  • register.py
def register():
    # 这里完成注册逻辑的代码编写
    pass
  • user_manage.py
def user_manage():
    # 这里完成用户管理的代码编写
    pass
  • main.py
from login import login
from register import register
from user_manage import user_manage

if __main__ == "__main__":
    login()
    register()
    user_manage()

其实,Python之所以这么强大,这个特性发挥了很大的作用。我们需要的很多功能不用自己去写,通过导入别人写好的模块,我们可以直接使用,这样可以大大提供效率。比如我们想要写一个爬虫,假设没有模块化的助力,我们需要从0开始编写,http请求、TCP连接、返回处理、底层数据包封装、解析。但是有了模块化,我们只需要一行代码,开箱即用,是不是非常方便呢

import requests

2. Python中的模块

说了模块化编程,那么在Python中,我们的模块到底指的是什么呢?在Python中,我们需要区分几个概念:

  1. 模块:一个后缀为.py的代码文件就是一个模块
  2. 包:一个包含很多.py文件的文件夹。(Python3.3之前要求这个文件夹中必须含有 __init__.py文件)
  3. 库:可能由多个包和模块组成,可以认为是一个完整项目的打包。

3. import语句

3.1 从模块导入



(1)全量导入:

全量导入会将模块内的所有全局变量、函数、类等等全部都导入进来。

全量导入一个模块的所有内容有两种方式:

import xxx

import test

print(dir(test)
# 查看导入了什么
# 发现我们定义的Hello类、hello函数、name变量全部都被导入进来了
# 并且还有一些其他的东西
['Hello', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'hello', 'name']

test.hello()
print(test.name)
# 上面方法需求使用:模块名.变量名(方法名)引用,因为只是引入模块整体,并没有把模块里面的内容单独引入

from xxx import *

from test import *

hello()
Hello()
print(name)
# 以上可以直接使用,因为 from test import * 已经将test模块的所有内容单独导入进来。

(2)局部导入

from xxx import xxx

from test import hello  # 只从test模块中导入hello函数,别的不导入
from test import hello, Hello  # 导入多个

hello()

给导入的内容设置别名:from xxx import xxx as yyy

from test import hello as hello_func  # 从test模块中导入hello函数,并且设置别名为:hello_func

hello_func()

3.2 从python包中导入


|——test_package
    |——__init__.py
    |——test.py
    |——test2.py
|——main.py

main.py:

大家猜猜,下面这两段代码能运行吗?

import test_package

test_package.test.hello()

Traceback (most recent call last):
  File "/app/util-python/python-module/main.py", line 8, in <module>
    test_package.test.hello()
AttributeError: module 'test_package' has no attribute 'test'
from test_package import *

test.hello()

Traceback (most recent call last):
  File "/app/util-python/python-module/main.py", line 10, in <module>
    test.hello()
AttributeError: module 'test' has no attribute 'hello'

看到这里,可能大家脸上都出现了三个问号???为什么导入模块的方法放在导入包这里不好使了呢?

其实啊,我们的python在导入一个模块的时候,会把我们的模块.py文件执行一遍,然后生成一个模块对象,模块中我们定义的函数、变量、类会添加到这个模块对象的属性里面:这就是为什么我们可以通过test.hello(),因为hello()是test的一个属性.

那导入包的时候呢?我们知道python的包本质上是一个文件夹,文件夹是不能被编译执行的。那为什么还能import呢?实际上,我们在import一个包的时候,执行的是这个包里面的 __init__.py文件,也可以理解为导入包的时候,只是导入了__init__.py

因为我们__init__.py是空的,所以我们导入了个寂寞。

(1)通过导入模块的方式:

from test_package import test
test.hello()

from test_package.test import hello
hello()

(2)通过添加__all__属性:

__init__.py文件: 添加 __all__ 属性

__all__ = ["test", "test2"]

main.py文件:

from test_package import *

# 通过在 __init__.py中定义了 __all__属性,在导入的时候,可以把该属性列出的模块全部导入 
test.hello()
test2.hello2()
__all__ 是针对模块公开接口的一种约定,以提供了”白名单“的形式暴露接口。如果定义了__all__,其他文件中使用from xxx import *导入该文件时,只会导入 __all__ 列出的成员
__all__ 仅对于使用from module import * 这种情况适用。

4. 动态导入

上面我们介绍了比较主流的import语句导入,其实在python中还有其他的导入方式

4.1 __import__()

__import__() 函数可用于导入模块。其实当我们使用import导入Python模块的时候,默认调用的是__import__()函数。直接使用该函数的情况很少见,一般用于动态加载模块。

__import__(name, globals=None, locals=None, fromlist=(), level=0)参数:

  • name:要导入的模块名,可使用变量
  • globals和locals:通常使用默认值。使用给定的globals和locals变量来决定如何在一个包上下文中解析name
  • fromlist:指定要导入的子模块名或对象名,它们会按名称从模块导入
  • leve:指定导入模块的方式。level为0则绝对导入; level为正值则表示相对于调用import ()的模块目录,要搜索的父目录数
os_obj = __import__("os")

print(os_obj.getcwd())
os_obj = __import__("test_package.test")

os_obj.test.hello()

4.2 importlib

importlib 是 Python 中的一个标准库,importlib 能提供的功能非常全面

import importlib
myos=importlib.import_module("os")
myos.getcwd()

4.3 一个使用场景

一个定时任务的场景,数据库存了许多这样的定时任务:cmdb.tasks.get_host_info,它表示的是调用cmdb包下的tasks模块下的get_host_info函数。我们怎么实现,通过这种格式的字符串,去调用相应的函数呢?

大家可以思考思考怎么做。

def exec_task(task_name):
    if not task_name:
        return -1, "task name must not None"
    try:
        module_name = task_name.rsplit(".", 1)[0]
        method_name = task_name.rsplit(".", 1)[1]

        # 动态导入tasks模块
        module_obj = __import__(module_name)
        if not hasattr(module_obj, method_name):
            return -1, "function not found"
    except:
        return -1, "has Error"

    task = getattr(module_obj, method_name)
    task()

5. 搜索路径

不知道大家有没有思考过这样一个问题,当我们导入一个模块或者导入一个包的时候,python是去哪里寻找这个模块的呢?

Python搜索模块的路径是由四部分构成的:

  • 程序的主目录、
  • PATHONPATH目录
  • 标准链接库目录
  • .pth文件的目录,

这四部分的路径都存储在sys.path 列表中。

pth文件:pth文件用于添加额外的sys.path即python检索路径,一般在github上下载的程序包会有一个setup.py,执行该文件会在(当前python环境下的site-packages文件夹生成)一个.pth文件
import sys
print(sys.path)

[
    '/app/util-python/python-module',    # 当前程序所在的目录 
    '/usr/lib/python37.zip', 
    '/usr/lib/python3.7', 
    '/usr/lib/python3.7/lib-dynload', 
    '/app/python-virtualenv/aioms-env/lib/python3.7/site-packages'  # 下载的第三方包目录
]

当我们的 导入一个包的时候,python解释器依次在这些目录搜索。如果这些目录中没有找到,程序就会报错

假设有一个文件:/app/test_pack.py

我们现在的程序路径为:/app/util-python/python-module/main.py

import test_pack

Traceback (most recent call last):
  File "/app/util-python/python-module/main.py", line 5, in <module>
    import test_pack
ModuleNotFoundError: No module named 'test_pack'

没有任何意外,报错了,找不到这个包

import sys
sys.path.append('/app')

import test_pack

test_pack.hello()
# 测试导入自定义路径

print(sys.path)
[
    '/app/util-python/python-module',
    '/usr/lib/python37.zip', 
    '/usr/lib/python3.7', 
    '/usr/lib/python3.7/lib-dynload', 
    '/app/python-virtualenv/aioms-env/lib/python3.7/site-packages', 
    '/app' # 我们发现多了一个搜索目录,它在这个目录下找到了我们的test_pack.py
]
所以:模块与包的搜索路径不是固定不变的,我们可以自定义它,当然上面的方法只是暂时的

所以,当我们程序出现这个错误的时候:ModuleNotFoundError: No module named 'test_pack'

问题排查两部曲:

  1. 查看下载安装的包的路径在什么地方
  2. 使用sys.path看看,下载完成的包在不在这里面

6.相对导入与绝对导入

假设包结构:

main.py
packageA/
    __init__.py
    moduleA.py
    moduleB.py
packageB/
    __init__.py
    moduleA.py
    moduleB.py

对于packageA/moduleA.py

绝对导入:所有的模块import都从“根节点”开始。根节点的位置由sys.path中的路径决定

from packageA import moduleB

相对导入:只关心相对自己当前目录的模块位置

  • . 当前目录同级查找
  • .. 当前目录上级查找
from . import moduleB

7. 交叉引用(导入循环)

什么叫做交叉导入,就是两个包互相导入

main.py
package/
    __init__.py
    moduleA.py
    moduleB.py
from . import moduleB

这里不想拓展太多了,推荐大家都使用绝对导入就完事了!免得给自己挖坑

moduleA.py:

from moduleB import hello_b

moduleB.py:

from moduleA import hello_a

在这里面:模块A调用了模块B的某方法、而模块B也调用了模块A的某方法,这个就叫交叉导入

首先我们看看这样会导致什么问题

ImportError: cannot import name 'hello_b' from 'moduleB'

这样将会抛出异常。异常的原因是:在导入的时候,moduleA需要访问moduleB的hello_b,但是没有hello_b还没有初始化完成。所以就会抛出异常

通常来说:这是由于大型的Python工程中,架构设计不当,出现的模块间相互引用的情况

解决办法:

  1. 架构优化,解除相互引用的关系
  2. import语句放置在模块的最后
  3. import语句放置在函数中

8. 安装第三方包

什么是第三方包呢?在编程的圈子里面流行着这么一句话:“不要重复造轮子!”。这里的轮子其实就是我们的第三方包。当我们想要制造一辆小汽车,我们直接使用现有的零件拼装起来就行。就不需要再从0开始,造轮子、发动机等等。因为别人已经造好了。

在计算机行业里面,别人造好了轮子,总要有地方存起来,可以让其他用户看到和使用。这就需要一个权威的三方机构去管理这些轮子。

PyPI就是这么一个角色, PyPI(Python Package Index)是python官方的第三方库的仓库,所有人都可以下载第三方库或上传自己开发的库到PyPI。

那怎么去管理这些三方包呢?市面上有很多方法。但是使用最广泛的是pip

pip 是一个现代的,通用的 Python 包管理工具,该工具提供了对Python 包的查找、下载、安装、卸载的功能

7.1 pip 安装

# 查看pip版本,可以判断pip是否安装
pip -v

一般情况下,我们从官网上下载的python安装以后,会自带这个工具,我们无需过多操心,但是,如果很不巧,你的Python版本下恰好没有pip这个工具,怎么办呢?

对于linux用户:

$ curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py   # 下载安装脚本
$ sudo python get-pip.py    # 运行安装脚本
sudo apt-get install python-pip

window用户类似,只需要去官网上下载pip安装包,再使用python安装即可

7.2 安装第三方包

pip install Django  # 安装最新版本
pip install Django==1.0.4  # 指定版本
pip install Django>=1.0.1  # 指定最小版本
pip install Django<=1.0.1  # 指定最大版本

由于 PyPI镜像源是国外的,有时候下载会非常缓慢,这个时候我们可以使用国内镜像源

pip install -i https://pypi.tuna.tsinghua.edu.cn/simple Django

国内镜像源地址:

7.3 其他操作

pip uninstall Django  # 卸载已安装的库
pip install --upgrade Djano==2.0.1  # 升级已安装的包
pip list  # 列出已安装的库
pip freeze > requirements.txt  # 将当前的项目依赖导出的文本文件中
pip install -r requirements.txt  # 根据上面导出的文本文件里面的依赖进行安装

发布于 2021-11-26 10:25