Python的包(package)和模块(modul)以及相对导入和绝对导入的那些小tricks

代码增加到一定阶段,一定避不开包package和模块module,而这又往往是初学者困惑所在,这里整理下自己踩的坑和一些经验教训。

作者:Su Liang,2018-10-24

  • 参考《Python Cookbook》, David Beazley

Q: 如何设置文件的包(package)和模块(module),并进行模块导入?

假定如下文件结构:
在这里插入图片描述
之所以设置了一个相对复杂的文件结构,是为了接下来做各种测试验证各种导入模式。
以下所有涉及的测试和文件结构,都已上传 - github -,有需要的朋友可以整个clone下来实践下整个代码。
由于逻辑上有点复杂,以下测试需要一定的时间进行研究。

概念1:包package是文件夹, 模块module是.py文件

概念2:绝对导入和相对导入(又分为隐式相对导入和显式相对导入)

  • 绝对导入:基于完整路径导入模块(.py文件),比如from pa.sub import deep
  • 相对导入-显式:基于相对路径符号导入模块,比如在spam.py中写入from . import grok
  • 相对导入-隐式:不采用相对路径符号而以默认相对关系导入,比如在spam.py中写入import grok

这三种方式中,python规则不推荐用隐式相对导入,因为没有路径说明,很可能直接导入了一个跟系统文件重名的模块文件,导致覆盖系统文件,比如import string,这就会覆盖string文件。
相对导入中,.代表本级文件夹,…代表上一级文件夹,比如在deep.py文件中,可以写from .. import grok,其中…代表的就是pa.sub文件夹的路径,而在spam.py文件中可以写from . import isolate,其中.代表的就是pa文件夹
而绝对导入中,每个新的文件夹导入本质上就是导入该文件夹下的__init__文件,且该__init__文件的名称就是该文件夹的名称,这在接下来的测试中可以看到。

概念3:python永远基于__main__函数作为入口,能够导入同层和下层的package和module的名称,绝不可能导入更上一层的package/module。

这里需要说明,一个.py文件有两种加载方式,一种是作为script运行或者在IDE中直接run file,另一种是作为module被import,两种方式完全不同的影响__main__函数入口。当作为script运行时,该文件的自有名称就会被__main__替代掉,而当作为module被import时,该文件的名称就是路径+文件名,两种方式都可以通过print(name)来查看。比如bar.py文件,通常作为module被主程序导入,他的__name__属性就是pb.bar,而如果因某些调试需要直接运行bar.py文件,则他的__name__属性就会被覆盖成__main__来运行作为入口。
同层和下层的解释:比如grok.py文件如果作为__main__的入口,他能够导入同层的sub/sub2包,自然也能导入sub/sub2包下层的deep/ground模块,也能导入同层spam模块,但肯定不能导入pa层/pb层的包以及pb层以下的包/模块。同时需要注意如果直接运行grok.py想要导入同层spam.py模块,使用from . import spam肯定报错,原因是前面解释的不能往上到.所代表的pa层,所以只能用隐式相对导入import spam这种不建议使用的方式,所以也是建议在做模板时,主程序train/test可以放在根目录,其他模块都要分类放在文件夹中,避免隐式相对导入。再次强调这是在假设grok.py作为script直接运行时的讨论,如果grok.py作为module被导入,他是可以通过相对导入/绝对导入导入任意层面的包或者模块,而不受刚才所说的同层和下层的限制。

接下来看实例:

实例1:train.py作为主函数运行

在train.py运行后输出见右边,具体解释下每个输出。
在这里插入图片描述

  1. 首先输出this is train.py: _ _main__ ,这句输出是通过train.py中打印__name__得到,说明__name__ = __main__
  2. 然后输出this is pa __init__: pa,这句输出是通过在pa文件夹下__init__文件中事先写了一句print打印__init__文件的__name__属性得到,说明from pa import spam这句会先执行pa下的__init__文件,然后才执行spam.py,且__init__文件的名称就是pa
  3. 然后输出this is spam.py: pa.spam ,这句输出是spam文件打印自己的__name__属性,如下,也证明此时spam的__name__属性是由包名+文件名的组合
  4. 然后输出this is grok.py: pa.grok,这句输出是spam文件调用grok模块后grok文件的输出
    在这里插入图片描述
  5. 然后输出this is pb __init__: pb,这条是在grok.py文件中调用pb.bar,其中pb的__init__文件的print输出。
  6. 然后输出this is bar.py: pb.bar,这条是bar.py本身print __name__的输出
  7. 然后输出this is sub __init__ pa.sub,这条是bar.py内进行的相对导入from pa.sub import deep,先执行pa的__init__,然后执行deep本身的打印
  8. 然后输出this is deep:pa.sub.deep,就是deep.py本身的print __name__
  9. 最后输出this is isolate2: pa.isolate2,这是deep.py执行的相对导入from .. import isolate2

实例2:grok.py作为主程序运行(调试目的)

在实例1的基础上微调了几个模块内的代码,如果你运行过实例1了,需要手动微调代码才能复现实例2的内容

如果直接运行grok.py文件,可以看到grok.py的__name__属性输出是__main__
在这里插入图片描述
输出this is grok.py:__main__ 是grok.py的__name__被打印出来输出
输出this is isolate2: isolate2 是导入isolate2模块成功后,isolate2的__name__输出。
输出this is sub __init__: sub 是调用sub.deep模块成功后,sub的__init__打印输出
输出thi is deep: sub.deep 是调用sub.deep模块成功后,deep的打印输出。
可见这种情况下grok可以调用同层的模块和同层package以及package之下的模块。

在这里插入图片描述这次运行没有报错,但不会执行from . import isolate2 是因为此时__main__的入口在grok.py文件处,它是看不到更上层的包的,所以.所代表的pa包他无法访问,也就无法导入isolate2模块。

在这里插入图片描述这次运行报错,因为grok.py作为__main__函数,看不到更上层的包,肯定无法访问pb和bar.

以上几个实验详细的代码输入可以查看github上的源码:通过实验,可以完整验证绝对导入/相对导入/导入执行过程/__main__函数命名方式,希望能够帮到你解惑。

总结一下:

  1. 包和模块的概念:
    包package是文件夹, 模块module是.py文件。
    包是用来存放模块的,最终导入的永远是模块。
    包可以通过__init__文件的创建转化为模块,此时该模块等价于__init__文件
    模块可以是一个模块(aaa.py),也可以是一个模块链(aaa.bbb.ccc.py)

  2. 绝对导入和相对导入的概念:
    绝对导入:基于完整路径导入模块(.py文件)
    显式相对导入:采用. /..作为相对路径符号导入
    隐式相对导入:在默认相对路径相同的情况下直接导入

  3. 永远把sys.path或者__main__函数作为模块调用入口,永远只能导入这两个调用入口的同级或下级模块,永远不能导入更上一级的模块

  4. 模块的导入过程拆解:
    从总体上来说,在python中导入一个模块分两步,分别寻找两个调用入口:
    (1) 步骤1,从sys.path的路径入口查看同级和下级目录是否有该模块(mod.py),或者有该模块链:
    如果有则直接导入,如果没有则走第二步
    (2) 步骤2,从__main__函数入口查看同级和下级目录是否有该模块或者有该模块链:
    如果有则直接导入,如果没有则报错

  5. 处理模块导入的基本原则:
    (1)主函数文件尽可能放在根目录,这样调用各模块都可以直接用同级向下的逻辑和写法
    (2)模块文件的相互导入,可以采用显式相对导入的写法,在被主函数文件调用时不会报错,但单独调试该模块文件(即作为main运行)是不被允许的。
    (3)其他类似与example/test类型的主函数文件,如果需要在子文件夹中运行,则需要把主文件夹加入sys.path
    ,可以把这部分代码放在根目录的requirement.py文件中,在需要时运行,但注意这个添加是一次性的,关闭系统后就会被python清楚,下一次还需要再添加,添加代码如下:
    import sys, os
    rootpath = os.path.dirname(__file__)
    sys.path.insert(0, rootpath)

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值