Python中必须要明白的绝对导入与相对导入

引入

Python的模块导入对于编程来说可以是家常便饭了,几乎每次写代码前都要进行导包。可你真的了解Python怎么导包的吗?为什么我写的程序总是会出现ModuleNotFoundError: No module named XXX?这Python的导包真的是令人头疼,包括我在网上搜的一些文章也都含糊不清地介绍下,本质上并未解决问题。所以写下这篇来解决这无处不在的导包问题。看这篇文章之前请带上以下两个问题:

  • 模块内的__name__是什么,它是怎么变化的?
  • 什么时候用绝对导入,又什么时候该用相对导入?

如果你不能很确定地知道答案,那请认真耐心地看完该篇,相信你一定会有所收获。
在此之前,要先从最基本的知识点说起

导入语句的风格

Python的官方风格指南—PEP 8,在编写导入语句时有几个忠告。总结如下:

  1. 导入总是位于文件的顶部,在任何模块注释和文档字符串之后。
  2. 导入应该根据导入情况来划分。通常有三类:
    • 标准库导入(Python的内置模块)
    • 相关第三方库导入
    • 本地模块导入
  3. 每一组导入应该被空行隔开。

示例如下:

"""
说明文档内容....
"""
import re  # Python的标准库
import sys
				# 空行隔开
import requests  # 安装的第三方库
				# 空行隔开
import my_module  # 自定义模块

模块与包

  • 模块:用于组织Python代码(变量、函数、类)可以理解为就是一个以.py结尾的文件
  • :用于组织模块的,可以理解为多个模块组成的目录,它还有一个__init__.py文件(实际上在Python3.3后,无论有无__init__.py,Python都会将该目录视为包,但为了规范,应该要在包中添加__init__.py
  • 导入模块的本质就是将导入的.py文件内容用解释器执行一遍
  • 导包的本质就是将__init__.py文件内容用解释器执行一遍

特别提醒自定义模块一定不要与Python的标准库重名,也不要与第三方库重名,否则就会出现导包的问题(比如Python的内置模块abctest,如果你写的目录名或是文件名与该名称一致,那么程序如果使用import就会出现问题。)

导包的方式

import XXX[.XX.X....][as X]    # as是起别名
from XXX[.XX.X....] import xxx  
# from XXX import *   # 这种导包的方式是不建议的,是一种非常不好的习惯
应该用 from XXX import xxx1, xxx2, xxx3来代替

绝对导入的问题

为了更好地说明,先创建如下的目录结构

outer   
    │ main1.py
    │  __init__.py
    │
    └─middle
        │  main2.py   
        │  __init__.py
        │
        ├─inner1
        │   module1.py    初始文件内容:print('in the module1')
        │   test1.py
        │    __init__.py
        │  
        └─inner2
            module2.py 
            __init__.py      

其对应在PyCharm的文件结构为:
无
先从最里层的inner1文件开始:
要在test1.py中尝试导入module1 我们一般会这样做:

import module1

实际上在PyCharm是会给出红色警告的。
无
但我们尝试在test.py模块下运行,是可以得到结果的
实际上大可不必管红色警告,因为Python解释器是可以运行的,有强迫症的可以右键父目录,找到Mark Directoy as --> Sources Root。
那么这是一种怎样的导入方式?它是绝对导入
为什么?来看一下sys.path
修改module1.py

import sys
print(sys.path)
print("in the module1")

然后在test1.py中运行,得到运行结果:

['C:\\project_name\\outer\\middle\\inner1', 'C:\\project_name', 'C:\\Python36\\python36.zip', 'C:\\Python36\\DLLs', 'C:\\Python36\\lib', 'C:\\Python36', 'C:\\Python36\\lib\\site-packages', 'C:\\Program Files\\JetBrains\\PyCharm 2018.3.4\\helpers\\pycharm_matplotlib_backend']
in the module1

我们主要关注的是前两个路径,后面的路径其实是一开始配置Python环境变量加入的
第一个路径实际上是当前运行模块的父目录路径
那第二个路径哪来的?我觉得应该是在PyCharm下创建项目时加入的,我在VsCode上试过是没有该路径的。(所以为了程序的可移植性,这个路径当做没有)

首先说明:import 能否导入成功,决定权是在sys.path中。 而日常我们遇到的很多导包问题就出在sys.path[0]中。
模块要想被执行,你其实需要给Python解释器一个完整的路径名。

那什么是绝对导入:就是以sys.path[0]为基准,顺序往里导入的路径就是绝对导入。(注意不能跳,否则报错
知道后,现在让我们尝试在main2.py中导入module1.py, 你应该会这么写:

 from inner1 import module1

ok, 程序正常运行,得到结果, 但细心的你应该要发现sys.path[0]改变了,
变为'C:\\project_name\\outer\\middle',这点非常重要。

test1.py下运行和main2.py运行,同样的module1.pyprint(sys.path()),为什么结果却不一样?
根据前面写的导入模块的本质你就该知道,sys.path[0]是会随着当前执行的模块而改变的。
好,那在main2.py中导入test1.py呢,根据上面的导入方式,你又会这么写:

from inner1 import test1

然后程序一运行,给你一个报错:

ModuleNotFoundError: No module named 'module1'

why? 之前在test1.py下运行不是没有错误吗?当它被导入的时候就给我报错?
因为此时的sys.path[0]已经变了,是'C:\\project_name\\outer\\middle', 该目录下只有main2.py__init__.py,inner1和inner2目录,所以python解释器是找不到module1的,那要怎么解决这个问题呢?
你可能会说,那在sys.path中添加'C:\\project_name\\outer\\middle\\inner1'不就好了,那如果这个文件目录很复杂,导包的情况也有很多,这时你也要一个个的往里添加吗?这显然违背了 python之禅 ,代码不够优美。
那修改一下test1.py的内容,改为

from inner1 import module1

修改之后,程序正常运行,但又会出现一个问题,当你在main1.py中导入test1.py下并运行的时候,程序又会报ModuleNotFoundError: No module named 'inner1' ,这些本质的原因其实都是sys.path[0]改变造成Python解释器找不到路径而造成的。

为了解决以上问题,在此引出相对导入。

相对导入

相对导入分为隐式相对和显式相对,但隐式相对在Python3中已经废弃,所以现在说的相对导入都是显示相对
为了解决以上的问题,需将test1.py文件内容改为

"""
相对导入的.代表当前文件夹的路径, ..则是再往上一层。
"""
from . import module1  

这时你无论是在main1.py 还是在main2.py中导入test1.py模块时,各自都能正常运行。
因为.会随着运行文件改变而改变,当在main1.py运行时,.就代表middle.inner1; 而在main2.py运行时,.就代表inner1

但不要高兴地太早,这时又会出现一个问题,如果在test1.py下直接运行程序
又会给你一个报错:ImportError: cannot import name 'module1'
是路径的问题吗?print(sys.path[0])发现确实是C:\\study\\outer\\middle\\inner1,当前路径下是有module1的。
我们可以发现错误是发生了变化的,但就是给你报错,这又是为啥??

实际上如果是相对导入,就只能导入其顶层模块内部的模块,
当一个模块被直接运行,它自己作为顶层模块,不存在层次结构,所以找不到其他的相对路径。

一句话解释就是如果一个模块有相对导入的语句,它只能被其他模块导入,本身不能直接运行

回到最初问题

为了回答最初的两个问题,需要修改文件的内容,让程序告诉我们答案
其中main2.py

import sys

import inner1.test1

print("main2的__name__--> ", __name__)
print(sys.path[0])

test1.py

from . import module1
print("test1的__name__--> ", __name__)

module1.py

print("module1的__name__--> ", __name__)

现在在main2.py中运行程序,得到结果:

module1的__name__-->  inner1.module1
test1的__name__-->  inner1.test1
main2的__name__-->  __main__
C:\project_name\outer\middle

从中我们可以得到结论:

  • 当一个模块直接运行时,它的__name____main__
  • 当一个模块被调用时,它的完整路径为sys.path[0] + __name__

那什么时候用绝对路径?
当该模块需要调用其内部的子模块时,一般这个模块位于顶级目录下,由它负责项目中各个模块的调用

那什么时候用相对路径?
当该模块在包内部并且希望被其他模块调用时。

到此就模块导入的问题就结束了吗?不,还有问题

__init__.py的作用

为了说明它的作用,各模块的内容修改如下:
module2.py

def foo():
    print("in the module2")

main2.py

from inner2 import module2

module2.foo()

当运行main2.py时,ok,程序正常运行

但这时修改main2.py的内容,换一种调用方式

import inner2

inner2.module2.foo()

这次再次运行main2.py时,程序报错:AttributeError: module 'inner2' has no attribute 'module2'
为什么仅仅导入inner2在调用时就会报错,inner2下不是有module2吗?
其实inner2是一个包,当导入一个包时,实际上是导入__init__.py,但是__init__.py文件没有内容,所以inner2没有module2这个属性。
所以需要在__init__.py文件中添加

from . import module2

这时运行main2.py就没有问题了。

现在我们再来看看Python内置的模块的__init__.py中都写了什么
其中在Lib下的json包的__init__.py内容如下:

from .decoder import JSONDecoder, JSONDecodeError
from .encoder import JSONEncoder
import codecs

所以当导入json时,是可以调用json.encoder下的方法,可以发现__init__.py导入包内的模块时一般采用的是相对导入
再来看一下Lib下的urllib中的__init__.py内容
发现它竟然为空!!!那我们就不能import urllib,再根据urllib.parse调用了, 什么,不相信?
那就试试吧,在inner2下创建test2.py,文件内容

import urllib

data = {'a': '1'}
print(urllib.parse.urlencode(data))

为了更精确地测试,注意不要直接在PyCharm下直接运行,在inner2下打开命令行执行python test2.py,程序报错!!
你还可以再看看其他包下__init__.py文件内容。

OK,到此文章结束,但模块导入的问题还没有结束,以后所遇到的错误就应该运用所学的知识去解决了。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值