评论

收藏

[python] python模块化包管理与import机制

编程语言 编程语言 发布于:2021-11-15 17:14 | 阅读数:531 | 评论:0

我们在学 python的时候,大多数都是从print("hello,world")开始,这一行代码,敲开了每一位工程师新世界的大门!
然后我们开始学语法、变量、函数、条件控制、数据结构、面向对象,然后迫不及待的与bug过招。在这个过程中,有一位朋友一直默默陪伴着我们,但是我们却从来没有关注过它。他就是我们的 import 兄弟os在我们的代码中,它扮演这不可或缺的角色。但是却很少有人真正懂它。正是因为有了他,python的强大之处才能得以发挥,今天让我们一起好好了解一下它。
1. 模块化编程
在我们真实的项目中,代码量可能达到几十万、几百万行。如果我们把这几十万、几百万行代码都写在一个文件里面。那后果是非常严重的,首先我们的代码文件将会非常大,其次是想通过肉眼去找到我们的代码,难度也是非常大,更别说看懂其中的代码逻辑了。
出现了模块化编程的想法。模块化编程有助于开发者统筹兼顾和分工协作,并提升代码灵活性和可维护性。比如说众多工程师共同开发同一个系统。A做登录功能的逻辑,B做注册功能的逻辑,C做用户管理的逻辑。这些代码是分离的,通过模块化组装的方式把他继承到同一个系统中。
模块化编程:将一个完整系统的代码,拆分成一个一个小模块
举个例子:
(1)非模块化项目:项目里面只有一个py文件  main.py
login():
  #这里完成登录逻辑的代码编辑
  pass
def register():
  # 这里完成注册逻辑的代码编写
  pass
def user_manage():
  # 这里完成用户管理的代码编写
  pass
if __main__ == "__main__":
  login()
  register()
  user_manage()
(2)模块化的项目:项目中包含 main.py、login.py、register.py、user_manage.py

  • login.py
    login():
      #这里完成登录逻辑的代码编辑
      pass
  • register.py
    register():
      # 这里完成注册逻辑的代码编写
      pass
  • user_manage.py
    user_manage():
      # 这里完成用户管理的代码编写
      pass
  • main.py
    login import login
    from register import register
    from user_manage import user_manage
    if __main__ == "__main__":
      login()
      register()
      user_manage()
其实,Python之所以这么强大,这个特性发挥了很大的作用。我们需要的很多功能不用自己去写,通过导入别人写好的模块,我们可以直接使用,这样可以大大提供效率。比如我们想要写一个爬虫,假设没有模块化的助力,我们需要从0开始编写,http请求、TCP连接、返回处理、底层数据包封装、解析。但是有了模块化,我们只需要一行代码,开箱即用,是不是非常方便呢
requests
2. Python中的模块
说了模块化编程,那么在Python中,我们的模块到底指的是什么呢?在Python中,我们需要区分几个概念:

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

3.1 从模块导入
DSC0000.png
DSC0001.png

(1)全量导入:
全量导入会将模块内的所有全局变量、函数、类等等全部都导入进来。
全量导入一个模块的所有内容有两种方式:
import xxx
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 <em>
test import *
hello()
Hello()
print(name)
# 以上可以直接使用,因为 from test import * 已经将test模块的所有内容单独导入进来。
(2)局部导入
from xxx import xxx
test import hello  # 只从test模块中导入hello函数,别的不导入
from test import hello, Hello  # 导入多个
hello()
给导入的内容设置别名:from xxx import xxx as yyy
test import hello as hello_func  # 从test模块中导入hello函数,并且设置别名为:hello_func
hello_func()
3.2 从python包中导入
DSC0002.png
|——test_package
  |——__init__.py
  |——test.py
  |——test2.py
|——main.py
main.py:
大家猜猜,下面这两段代码能运行吗?
test_package
test_package.test.hello()
Traceback (most recent call last):
  File "/app/util-python/python-module/main.py", line 8, in &lt;module&gt;
  test_package.test.hello()
AttributeError: module 'test_package' has no attribute 'test'
test_package import *
test.hello()
Traceback (most recent call last):
  File "/app/util-python/python-module/main.py", line 10, in &lt;module&gt;
  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)通过导入模块的方式:
test_package import test
test.hello()
from test_package.test import hello
hello()
(2)通过添加__all__属性:
__init__.py文件: 添加  __all__ 属性
__all__ = ["test", "test2"]
main.py文件:
test_package import *
# 通过在 __init__.py中定义了 __all__属性,在导入的时候,可以把该属性列出的模块全部导入 
test.hello()
test2.hello2()
__all__ 是针对模块公开接口的一种约定,以提供了”白名单“的形式暴露接口。如果定义了__all__,其他文件中使用from xxx import </em>导入该文件时,只会导入 __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 ()的模块目录,要搜索的父目录数
_obj = __import__("os")
print(os_obj.getcwd())
_obj = __import__("test_package.test")
os_obj.test.hello()
4.2 importlib
importlib 是 Python 中的一个标准库,importlib 能提供的功能非常全面
importlib
myos=importlib.import_module("os")
myos.getcwd()
4.3 一个使用场景
一个定时任务的场景,数据库存了许多这样的定时任务:cmdb.tasks.get_host_info,它表示的是调用cmdb包下的tasks模块下的get_host_info函数。我们怎么实现,通过这种格式的字符串,去调用相应的函数呢?
大家可以思考思考怎么做。
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文件
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
test_pack
Traceback (most recent call last):
  File "/app/util-python/python-module/main.py", line 5, in &lt;module&gt;
  import test_pack
ModuleNotFoundError: No module named 'test_pack'
没有任何意外,报错了,找不到这个包
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'
问题排查两部曲:

  • 查看下载安装的包的路径在什么地方
  • 使用sys.path看看,下载完成的包在不在这里面
6.相对导入与绝对导入
假设包结构:
.py
packageA/
  __init__.py
  moduleA.py
  moduleB.py
packageB/
  __init__.py
  moduleA.py
  moduleB.py
对于packageA/moduleA.py:
绝对导入:所有的模块import都从“根节点”开始。根节点的位置由sys.path中的路径决定
packageA import moduleB
相对导入:只关心相对自己当前目录的模块位置

  • . 当前目录同级查找
  • .. 当前目录上级查找
. import moduleB
7. 交叉引用(导入循环)
什么叫做交叉导入,就是两个包互相导入
.py
package/
  __init__.py
  moduleA.py
  moduleB.py
. import moduleB
这里不想拓展太多了,推荐大家都使用绝对导入就完事了!免得给自己挖坑
moduleA.py:
moduleB import hello_b
moduleB.py:
moduleA import hello_a
在这里面:模块A调用了模块B的某方法、而模块B也调用了模块A的某方法,这个就叫交叉导入
首先我们看看这样会导致什么问题
: cannot import name 'hello_b' from 'moduleB'
这样将会抛出异常。异常的原因是:在导入的时候,moduleA需要访问moduleB的hello_b,但是没有hello_b还没有初始化完成。所以就会抛出异常
通常来说:这是由于大型的Python工程中,架构设计不当,出现的模块间相互引用的情况
解决办法:

  • 架构优化,解除相互引用的关系
  • import语句放置在模块的最后
  • 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  # 运行安装脚本
apt-get install python-pip
window用户类似,只需要去官网上下载pip安装包,再使用python安装即可

7.2 安装第三方包
install Django  # 安装最新版本
pip install Django==1.0.4  # 指定版本
pip install Django&gt;=1.0.1  # 指定最小版本
pip install Django&lt;=1.0.1  # 指定最大版本
由于 PyPI镜像源是国外的,有时候下载会非常缓慢,这个时候我们可以使用国内镜像源
install -i https://pypi.tuna.tsinghua.edu.cn/simple Django
国内镜像源地址:

  • 清华:https://pypi.tuna.tsinghua.edu.cn/simple
  • 阿里云:http://mirrors.aliyun.com/pypi/simple/
  • 豆瓣:http://pypi.douban.com/simple/
  • ......还有其他可自行搜索

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

关注小编公众号:偷偷学习,卷死他们
</div>
    
    <div id="asideoffset"></div>

关注下面的标签,发现更多相似文章