python中import,模块,包与相对导入(超级详细!)

笔者在CSDN上发现关于这方面的知识过于零碎,分布在各篇博客中,因此整理了一下知识点

一、基本概念

  1. module(模块):一个python文件就是一个模块,例如torch中的nn模块,模块里可以包含类、函数,例如nn.functional类和nn.Conv2d函数,我们之后不区分模块和文件的说法。
  2. package(包):一个包含多个模块并且拥有__init__.py文件的文件夹,例如torch或者torch中的nn;package比module多了一个__path__变量。(在python3中没有__init__.py的文件夹也会被认为是package)
  3. 工作目录:当前工作目录 =执行python文件的命令行路径= os.getcwd() ,cwd 是Current Working Directory的缩写,下图的C:\Users\我的名字叫红\Desktop\import就是命令行路径,D:/anaconda/python.exe是python解释器路径,最后一项是脚本路径

      4. 模块搜索路径 :解释器搜索模块和包的路径,sys.path列表。

       5.路径搜索:“  . ”表示当前目录,“ .. ”表示父目录。“ ./  ”相当于没有,“ ../  ”相当于向上跳一级    到父目录,“<folder>”表示进入文件夹。对于任意路径,从前向后开始解释执行。例如:“ .././data/.././main.py ” 可去掉所有“ ./  ”看作 “ ../data/../main.py”,再执行开头的“ .. ”,进入当前目录的父目录;再执行data,进入父目录下的data文件夹;再执行中间的“ .. ”,进入data文件夹的父目录;最后进入该目录下的main.py

 其他知识点:访问一个子模块、函数、类都可以用" . "来进行

在考虑python设计逻辑的时候一定要想到“嵌套调用”,一个文件可以被其他文件调用

每个程序主要有一个执行的文件,一般称为main.py,后边称之为执行脚本/文件

 在执行一个程序的过程中cwd是不变的(但是可以通过os.chdir(path)修改),整个程序共享一个cwd,也共享一个sys.path

为了方便下面的讲解,我们以这个文件结构为例子,每一个文件里都有函数f(当时起名的时候没起好,不想改了),请注意文件的结构是一棵树,我们要思考的是叶子节点之间如何交流

## 文件结构示意图

- **项目根目录**
  - `A` 文件夹
    - `A`
      - `__init__.py `
      - `A1.py`
      - `A2.py`
    - `B`
      - `B1.py`
    - `A1.py`
  - `B` 文件夹
    - `B1.py`
  - `C1.py` 文件
  - `README.md`

二、读取文件

核心:读取文件的相对路径永远是相对于工作目录而言的(也可以使用绝对路径)

以下均为在根目录下运行代码,右图为根目录,可以顺利读取

一个常见的误区是把相对路径设置为相对于当前执行脚本的,我们在读取同一级别的A2.py时使用"./A2.py"路径会报错

因为open读取文件的相对路径是相对于工作目录(cwd)的,因此在写文件代码的时候,就要事先想好最终的总执行目录是什么,而且用户在运行整个程序的时候也要cd到特定的工作目录。

因为工作目录(cwd)是唯一的,由所有文件共享,所以读取文件的相对路径是唯一的。

三、sys.path

sys.path是一个列表变量,解释器搜索模块和包的路径存放在sys.path列表中,供import使用

sys.path包含三部分内容,是有顺序的,因为搜索路径的过程是按顺序的:

  1. 当前执行脚本的父目录路径,自动添加到列表开头
  2. python的path环境变量
  3. 代码中使用sys.path.append后添加的(可选),append到结尾

 其中第一部分只有一项,当我们执行的脚本(通常是run.py、main.py)调用了其他脚本(通常是utils.py)时,也不会自动添加其他的路径

如下图所示,在根目录下执行脚本,当前脚本的父目录被自动append到sys.path列表中,与工作目录无关

文件A2.py导入同级别的A1.py,在被导入的文件A1.py中执行:

因为sys.path是全局共享的,所以当我们在被导入的A1.py文件中执行sys.path.append时,我们会看到sys.path列表结尾中多了被导入的A1.py文件中添加的“test_import"

四、import

核心:import从sys.path这个列表中按顺序搜索路径,导入的库/模块必须在搜索路径里

我们上文提到sys.path第一项是被执行脚本(通常是run.py、main.py)的父目录路径,因此import部分的代码一定尽量相对于这个父目录路径来写,尤其是被main.py调用的utils.py等模块的import语句。如果实在不能相对于这个父目录路径来写,可以使用sys.path.append(),因为append到列表末尾,因此优先级最低,不会影响其他导入。sys.path.append()的地址是相对于工作目录

import在执行的时候会(1)在sys.modules字典中搜索该模块。(2)如果没搜索到,导入该模块,自动执行被导入模块的代码,并将其添加到sys.modules字典中。我们刚才在第三部分中使用import A1的时候就自动执行了A1模块中的sys.append("test_import")代码。因此python中经常使用

if __name__=="__main__":

来避免执行被导入模块的所有代码,从而只导入需要的class和function。被import的模块作为一个变量,缓存起来,因此只执行一次。

通过" . "来访问的对象可以是函数,例如:torch.nn.functional.relu,也可以是模块,例如:torch.nn,也可以是类,例如: nn.Module

import有两种形式:

(1)import xxx :模块或包,例如import torch,import torch.nn.functional

  (2) from xx import xx :可以是模块、包、函数、类。例如from torch import nn,from torch.nn.functional import relu(函数),from torch.nn import Conv2d(类)

import仅仅是把模块添加到命名空间里了,但并没有添加到sys.path中,因此下面的代码搜不到nn

import package

import 一个package(例如torch)会自动执行__init__.py的代码,但不会加载其他代码。在A/A1.py中import A package,执行了print的代码,如下图

此时我们执行A.A1.f(),会报错,这是因为A1并没有被加载到A包下面

有两种解决方案:

  1. 添加import A.A1,这样A1被加载到A包下
  2. 在__init__.py中添加from . import A1。导入A1后,通过A就可以访问到A1了。这也是常用的库中的方式,不必import torch.nn,直接import torch就可以直接使用torch.nn。这样做的缺点是刚刚导入torch的时候需要花很长时间初始化。

第二种解决方案的另一种写法:__all__=["A1.py"] ,变量__all__指定了初始化导入的模块,不希望用户导入其他模块。但是尽管A2.py没有被__all__包括在内,用户依然能使用”from A import A2“导入它

这里解释一下为什么torch.nn模块下既有函数(Conv2d)又有其他模块 (functional):nn模块也有__init__.py,在init代码中导入了nn模块下的某些子模块中的函数,因此nn可以直接调用函数

torch.nn源码如下:nn from .modules import *(包含Conv,loss等等部分)

点相对导入

最令人困惑的是使用" . "和" .. "的相对导入(这里简称为点相对导入),这也是package里最常使用的导入方式,因为一个package里各个模块的相对位置是长期固定的。首先我们来看同一package内的两个同级别模块A1和A2,这里的“ . ”指的是相对于该模块的路径而言。例如:torch.nn.functional,在functional.py中执行from . import Conv2d,也就是导入了当前工作目录(nn)下的nn.Conv2d;执行from .. import nn.Conv2d,也就是导入了父目录下的torch.nn.Conv2d。由于".."和“.”对应的模块名是唯一的,所以不用指明,此时".."后添加模块名相当于自动在中间补上一个点,例如from ..nn  import Conv2d,起到了两个作用,相当于(1)访问到torch(2)from torch.nn import import Conv2d

我们一般会在A2中直接执行import A1,没有问题,因为sys.path中有A2的父目录,也就是A1的父目录。但是执行from . import A1并运行A2的时候报错:

ImportError: attempted relative import with no known parent package

这个问题是因为,我们压根就不应该直接执行package里的文件,如果我们在这个package之外调用A2.py,虽然from . import A1这段代码执行了,但不会报错。至于为什么,参考资料2里有详细说明。我们可以简单认为,当在package外使用package时,会自动将" . "前补充上package的名字,而在package里执行时,文件不知道" . "前补充什么名字,因此报错“no known parent "

那如果我们非要直接执行A2.py呢?把from . import A1暂时换成import A1就好了。另一种解决方案是按模块方式来执行该代码:python -m 模块路径。

我们再来看看其他情况作为练习:

刚才的两个文件同属于一个包下同一级别,结构如下左图,我们来看看不同包下同一级别的情况,见右图,使用B1.py访问A包下的A1.py

            

这时直接执行以下三种代码会遇到相同的错误:attempted relative import with no known parent package

这时无论我们添加sys.path.append(".")("..")("./A")("./A/A")都无济于事,因为说到底,这类点相对导入就和sys.path没什么关系,只和“包”有关

于是我们尝试另一种形式的相对导入,上下两种方法都可以,借此,我们能访问到./A/A下的文件,

但是注意不能同时使用,因为有重名的问题,from A.A的时候程序会优先从"./A"展开搜索,由于找不到"./A"下的A.A,会报错:

更换二者顺序后如上图,同样报错:

这里建议只添加根目录(工作目录)sys.path.append("."),然后全部使用相对于根目录的地址,避免名字空间的重复。

五、总结

可以把文件的结构视作一颗树,如果你想使用main.py的父目录(sys.path的第一项)访问到尽可能多的模块,那main.py最好在根节点下一级,能够通过A.B.C.D的方式访问到所有模块。package内可以用相对导入;如果只有一个被执行文件main.py,而且package是你自己私有的话,那package内部也可以根据相对main.py的位置进行导入子模块。

文件的结构也可以比作国家-地区-个人的层级查找,两国之间的两个人(python脚本)沟通必须要依赖于国家(上级文件夹),上级能访问同级和下级(使用点“ . "或者直接import),而个人是没有权力要求国家的,个人的通讯权力仅限于自己的sys.path,必须使用sys.path.append来赋予其访问权限。当国家需要个人的时候(main.py调用utils.py),个人也就有了与最顶层的国家共享sys.path的权力,因此借用这个根也就可以访问其他国的人。

总结:读取文件是相对于工作目录,sys.path包含脚本(main.py)的父目录,import使用sys.path或者" . "的相对导入方式。工作目录和sys.path是在整个程序中共享的。

为了正确读取文件,需要写代码的时候想好工作目录是什么,并cd到正确的工作目录下执行python文件

为了正确import模块,需要相对于main.py的路径写import语句

为了正确sys.append,需要相对于工作目录写相对地址

本人编程水平不高,如果有什么错误欢迎指正。下面是几个优秀的链接,在次推荐

参考资料:

1. 模块管理-预备知识-CSDNPython入门技能树

2.【python】关于import你需要知道的一切!一个视频足够了_哔哩哔哩_bilibili

3.【Python】`__init__.py` 文件详解_python init文件-CSDN博客

4.【Python】python包相对导入问题及解决方案_cython attempted relative import with no known par-CSDN博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值