欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

可能是史上最细的python中import详解

程序员文章站 2024-03-31 21:39:40
以前在使用import的时候经常会因为模块的导入而出现一些问题,以及一些似懂非懂半疑惑半糊涂的问题,索性花了点时间研究了一些python引用的方法,并且动手操作试验了一下,深有感触,特留此文以作总结,...

以前在使用import的时候经常会因为模块的导入而出现一些问题,以及一些似懂非懂半疑惑半糊涂的问题,索性花了点时间研究了一些python引用的方法,并且动手操作试验了一下,深有感触,特留此文以作总结,如有不当之处欢迎评论指正

本文会尽我所能详细描述,字数会比较多,希望各位耐心看完。

首先我觉得应该先了解一下python的引用是怎么引用的

我们首先新建一个python文件demo.py

dir()命令是获取到一个object的所有属性,当传值为空时默认传入的是当前py文件,我们可以通过这个函数来查看py文件的所有属性

双下划线开头结尾的变量是python中一类特殊的变量,称为python的魔法函数,这里简单解释一些常见属性的意义

  • __annotations__:预定义的变量类型
  • __doc__:文档注释,仅包括第一个''' '''内部的注释文档
  • __file__:文件名,根据被引用还是被执行返回绝对路径还是相对路径
  • __name_:文件名,被执行文件该属性为__main__,被引用文件则返回包名

当我们在python文件中写入一些代码后

再次执行后可以发现

['__annotations__', '__builtins__', '__cached__', '__doc__',
 '__file__', '__loader__', '__name__', '__package__', '__spec__', 
'a', 'b', 'c', 'f']

相较于之前,所有的变量、类、函数都被加入py文件的属性值中了,也正是如此才能在之后使用这些已经声明的变量或者函数。

对于以下比较常见的库文件引入方式

同理我们可以比较清晰的看到,当引入了一个python库文件的时候其实也就是在python文件中加入了这个库名字,如果是用 as 关键字进行重命名在文件中也会以重命名之后的名字来做保留

['__annotations__', '__builtins__', '__cached__', '__doc__',
 '__file__', '__loader__', '__name__', '__package__', '__spec__',
 'math','nn','arange']

这时我突然产生了一个疑问,那我们使用的print,dir这两个函数又是在哪里定义的呢,我似乎并没有引用任何的库就直接使用了呢。

这其实是一个相当有趣的问题,不过我们先稍等一下,在后面我会回答这个问题。

我们再来带入一个实际的场景,通常一个较为复杂的项目是多文件协同工作的,其中不乏为了命名空间的统一一致性,为了整体文件结构的清晰有序等多种目的而使用多级目录来合理划分文件关系,梳理整体代码架构。

我们引入如下的文件系统环境(此示意图会在文中适当的位置重复出现以加强记忆)

|—— python
|    |—— demo.py
|    |—— folder1
|        |—— a.py
|        |—— b.py
|    |—— folder2
|        |—— c.py
|        |—— d.py

并且每一个a/b/c/d.py文件中分别定义了f1-f4函数以供调用,示意如下:

然后我们在demo.py中执行以下代码,很显然正确引入没有问题

如果我在a.py中想使用b.py中的f2,我也可以更改并执行,没有任何问题

但如果我想在a.py中使用c.py中的f3显然需要一些别的手段,因为这涉及到了跨文件夹的引用

|—— python
|    |—— demo.py
|    |—— folder1
|        |—— a.py
|        |—— b.py
|    |—— folder2
|        |—— c.py
|        |—— d.py

考虑到文件结构层次,a.py位于目录folder1下,我们希望a.py能够回到上一级目录python下,这样就能再进入folder2/c.py顺利引用了。

很多文件也都是这样做的,加入了一个import sys,sys.path,sys.path.append(".")然后问题似乎就顺利解决了,

不过这种做法为什么可行呢,我们不妨来探究一下这种做法正确执行背后的逻辑

首先我们了解一下sys.path有什么作用,我们在demo.py中执行

这里我使用的python环境是anaconda3创建的一个python3.7虚拟环境,环境名称为fastreid

['g:\\learner_lu\\code-grammar\\python',
 'c:\\programdata\\anaconda3\\envs\\fastreid\\python37.zip',
 'c:\\programdata\\anaconda3\\envs\\fastreid\\dlls',
 'c:\\programdata\\anaconda3\\envs\\fastreid\\lib', 
 'c:\\programdata\\anaconda3\\envs\\fastreid', 
 'c:\\users\\administrator\\appdata\\roaming\\python\\python37\\site-packages',
 'c:\\programdata\\anaconda3\\envs\\fastreid\\lib\\site-packages']

我们可以观察到sys.path中包含了许多绝对路径,第一个路径似乎是demo.py的所在的文件夹,其他的路径配置过环境变量的的小伙伴想必会觉得很眼熟。

而如果我们选择执行a.py,我们会得到以下结果:

['g:\\learner_lu\\code-grammar\\python\\folder1',
 'c:\\programdata\\anaconda3\\envs\\fastreid\\python37.zip',
 'c:\\programdata\\anaconda3\\envs\\fastreid\\dlls',
 'c:\\programdata\\anaconda3\\envs\\fastreid\\lib',
 'c:\\programdata\\anaconda3\\envs\\fastreid',
 'c:\\users\\administrator\\appdata\\roaming\\python\\python37\\site-packages',
 'c:\\programdata\\anaconda3\\envs\\fastreid\\lib\\site-packages']

唯一的区别就是第一个,也印证了我们的猜想,sys.path中的第一个值是被执行的py文件所在文件夹在操作系统中的绝对路径。

那么现在问题来了,其余的路径是什么呢?

  • c:\\programdata\\anaconda3\\envs\\fastreid\\python37.zip是python的压缩包,解压之后就会被删除,路径无效
  • c:\\programdata\\anaconda3\\envs\\fastreid\\dlls中是所有的.pyd格式的文件,是一种d语言的加密格式,该格式可以被引用但是不能被查看源代码。
  • c:\\programdata\\anaconda3\\envs\\fastreid\\lib中是python自带的一些库,随python一起安装,可以看到我们常见的一些copy.py,glob.py,io.py,os.py
  • c:\\programdata\\anaconda3\\envs\\fastreid是python解释器python.exe所在的目录 ,也是整个py文件被执行时需要启动用来逐行解释语句的文件
  • 剩下两个site-packages则分别是使用pip install / conda install时包的安装位置, 相信对于使用python的小伙伴们来说下载第三方库的操作并不陌生

好了,回到刚才的问题,sys.path中是 被执行的py文件所在文件夹在操作系统中的绝对路径以及使用的python环境下所有的库的目录

|—— python
|    |—— demo.py
|    |—— folder1
|        |—— a.py
|        |—— b.py
|    |—— folder2
|        |—— c.py
|        |—— d.py

到这里已经解决了我的一些疑惑

为什么能 a.py 中能import b ?原来是g:\\learner_lu\\code-grammar\\python\\folder1目录下能找到这个文件

为什么能import os?原来是 c:\\programdata\\anaconda3\\envs\\fastreid\\lib目录下有os.py

为什么能import numpy? 原来是放在xxx\\site-packages下了

所以我们可以总结一下,只要是在sys.path中的路径下能被找到的文件,我们就可以直接使用import引用。

那么之前的问题就很好解释了,a.py的sys.path下找不到c.py,所以我们需要在sys.path中加入能找到c.py的路径,有以下两种方法

虽然可以执行不过显然这种方式比较麻烦,因为你还需要输入目录的绝对路径,这显然不是一个简便的做法。

那么一开始介绍时所使用的sys.path.append(".")又是什么意思呢 ?

|—— python
|    |—— demo.py
|    |—— folder1
|        |—— a.py
|        |—— b.py
|    |—— folder2
|        |—— c.py
|        |—— d.py

.表示的是当前目录,也就是现在你所在的目录。比如我现在处于g:\learner_lu\code-grammar\python,我想要执行a.py可以通过相对路径访问,python folder1/a.py, 而sys.path.append(".") 就是加入了当前所在的g:\learner_lu\code-grammar\python路径,与sys.path.append("g:\learner_lu\code-grammar\python")完全一致。

但如果我现在更进一步,我现在处于g:\learner_lu\code-grammar\python\folder1,那么我想直接运行a.py则可以直接python a.py,这时候的sys.path.append(".")相当于sys.path.append("g:\learner_lu\code-grammar\python\folder1")

也就是说,sys.path.append(".")其实就是在sys.path中加入当前所在的目录的绝对路径,会随着你cd进入或者退出到其他目录而发生改变。

这确实是一种解决办法,省去了繁琐的绝对路径的输入而把当前目录(通常是整个项目最外层的根目录)加入,这样可以直接在文件中引入任意文件的位置。这种做法在许多项目中也都有应用。

使用这种做法时一定要注意你所在的目录最好是根目录,这样能引用到所有文件,如果不在也可以通过sys.path.append('..')等方式得到一个更大的引用范围

还有一种引用的用法也十分常见,就是文件之间通过相对引用.,..,....等文件、函数相互引用,比如还是之前的文件结构

|—— python
|    |—— demo.py
|    |—— folder1
|        |—— a.py
|        |—— b.py
|    |—— folder2
|        |—— c.py
|        |—— d.py

我重写了a.py的f1函数,它调用了来自b.py的f2以及c.py中的f3, 然后我想在demo.py中使用f1函数,这种情况比较常见,那我们应该怎么做呢?

首先我改写a.py,采用相对引用导入f2,f3

其次我改写demo.py

看起来合情合理,但是运行demo.py时却报错了。

traceback (most recent call last):
  file "g:\learner_lu\code-grammar\python\demo.py", line 3, in <module>
    from folder1.a import f1
  file "g:\learner_lu\code-grammar\python\folder1\a.py", line 4, in <module>
    from ..folder2.c import f3
valueerror: attempted relative import beyond top-level package

报错原因是“在*包之外进行相对引用”,说的云里雾里没太明白什么意思。

那我们运行a.py来先试试看a.py有没有问题?

这时候更奇怪的事情发生了,报错的位置甚至提前了,第一次至少from .b import f2没有报错,现在居然也报错了?

traceback (most recent call last):
  file "g:\learner_lu\code-grammar\python\folder1\a.py", line 3, in <module>
    from .b import f2
importerror: attempted relative import with no known parent package

报错原因是“尝试在没有已知父包的情况下进行相对导入”,明明我用from b import f2来导入没有问题呀,a.py和b.py同目录我导入加一个同目录的.又有什么问题呢 ?这些报错又都是什么意思呢?

这时我们不妨先停一下,我们先来分析一下python是如何进行相对引用的导入模块的。

通过开头的dir() 函数我们可以看到每一个py文件都有一个__name__属性

  • 当这个文件被执行时,这个属性的值为"__main__ "
  • 当它作为模块被引用时,它的属性的值是当前执行的文件到它的相对路径

|—— python
|    |—— demo.py
|    |—— folder1
|        |—— a.py
|        |—— b.py
|    |—— folder2
|        |—— c.py
|        |—— d.py

还是原来的文件目录,我们输出不同情况下c.py的__name__属性

而相对引用的原理也就在此,根据模块的__name__值和使用的.,..等相对路径来搜索文件。

所以说回到刚才的问题,为什么a.py和b.py同目录下使用from .b import f2的相对引用失败了呢?

同目录没问题,相对引用使用.b也没有问题,问题出现在当你执行a.py时,它的包名,也就是__name__ 的值是"__main__",无法通过“__main__"来找到其他文件。也就是说如果a.py并不直接运行,而只是以模块的方式和b.py进行相对引用是没有问题的,例如:

因为这是a.py现在没有被执行,它的的__name__属性是被执行的demo.py到a.py的相对路径,也就是folder1.a,而我们执行demo.py时首先加入sys.path中的就是 demo.py所在的文件夹,也就是g:\learner_lu\code-grammar\python,它使用.b进行相对引用也就是先寻找一个folder1的文件夹,在其中再试图找到一个b.py的文件,可以找到该文件,所以正确执行

那如果我就是想通过相对引来引用b.py并且执行a.py呢?那么依照原理,我们首先更改a的"__name__" 属性,其次我们还需要能找到folder1这个文件夹,因为执行a.py是在sys.path中加入的是 g:\learner_lu\code-grammar\python\folder1它无法找到自己的.,方法如下

正确执行没有问题,多说一句,其中__name__的值不一定非要是"folder1.a",因为反正是取a的同级目录,那么a的包名是什么其实无关紧要,改为"folder1.asd","folder1.pql"等都可以,但是只能有一个.,多个会被认为是多层的目录结构,寻找的位置就错误了。

不过这种直接改写name的方式图一乐也就好了, 能直接引用完全没必要画蛇添足

所以回到刚才的第二个报错信息,使用from .b import f2时报错,“尝试在没有已知父包的情况下进行相对导入”:

就是因为你是直接运行的a.py,它的包名__name__值是__main__,你又怎么能通过它来找相对引用的文件的位置呢? 而我们运行demo.py时这条引用没有报错是因为这时候a.py的__name__值为folder1.a,可以通过相对引用关系找到folder.b。

然后是刚才的另一个问题,我运行的是demo.py,a.py/b.py/c.py之间通过包名采用相互引用问题在哪里呢?看起来他们的__name__似乎没什么问题啊?

demo.py直接引用a没有问题,问题在于a.py中from ..folder2.c import f3

|—— python
|    |—— demo.py
|    |—— folder1
|        |—— a.py
|        |—— b.py
|    |—— folder2
|        |—— c.py
|        |—— d.py

运行 demo.py时a.py的包名为folder1.a,你试图通过..来进入其上级目录,其所在目录folder1的上级目录是python,而执行demo.py时首先加入sys.path的是g:\learner_lu\code-grammar\python目录无法通过其找到同级目录python,需要更高一级目录引用。

所以其报错信息:“在*包之外进行相对引用”,就是说现在a.py的包名不够我搜索到它的上级目录,folder1.a仅仅够我执行.,如果你想执行..那么你的包名至少要是python.folder1.a或者更长

有两种解决方法:

1.提高demo.py的目录等级,和python处于同一级下,即文件系统更改为

|—— demo.py
|—— python
|    |—— folder1
|        |—— a.py
|        |—— b.py
|    |—— folder2
|        |—— c.py
|        |—— d.py

在demo.py中使用from python.folder1.a import f1,提高demo.py位置相当于扩大a包的名字,由原来的folder1.a变为现在python.folder1.a,可以使用..来跳至python目录搜索folder2

2.原目录不变,更改a.py引用c.py的间接引用为直接引用

demo.py不变

我在初学python的时候就被告诉说python的引用多级目录之间就直接用.就可以了,这就是python的语法,并没有思考它背后的原理。

其实我们使用.来连接目录与目录,目录与文件,最后写出来的import python.folder1.a其实就是这个文件的包名,python也就是通过查找包名来引入模块的。

现在可以来回答一下开头提出的问题,print函数在哪里被引用了呢?我似乎并没有引入任何模块就直接使用了呢。

python中有一些函数叫做内置函数,观察开头所提到的py文件的属性,有一个属性叫做'__builtins__',这个属性包含了所有内置函数,我们可以通过print(dir(__builtins__))进一步查看

['arithmeticerror', 'assertionerror', 'attributeerror'.......,
abs,any,chr,help,set,round,sum,tuple,list,zip,min,max,str....
print,next,object,pow,quit,dir,.....]

众多我们或熟悉或陌生的内置函数,这些函数并不像其他的py文件需要被导入,python是用c语言来实现的,这些内置函数也是。你可以在[这里](https://hg.python.org/cpython/file/937fa81500e2/python/bltinmodule.c)找到所有内置函数的代码实现,在[这里](https://hg.python.org/cpython/file/937fa81500e2/python/bltinmodule.c#l1567)找到print函数的源代码实现,他们是用c语言完成的,他们引用的头文件也都可以在c:\programdata\anaconda3\envs\fastreid\include中到找到对应的源代码,例如code.h,eval.h,但是没有对应的c源文件,本身已经被编译链接成python.exe文件了。关于cpython和python的关系见文末。

当我们使用一个函数时,python首先会查找它是否在该py文件中被引入了,如果没有找到那么就会到内置函数中去查找,再找不到就会报错。

那如果对于模块引用又是怎么查找的呢,优先级是怎么样的呢?

比如我们可以定义一个print函数,他就会覆盖内置函数print被调用

现在我们在c.py的同目录下创建一个copy.py文件,那我们引用import copy时引用的是我定义的copy.py还是python环境中的'c:\\programdata\\anaconda3\\envs\\fastreid\\lib'下的copy.py呢?

现在的目录结构如下:

|—— python
|    |—— demo.py
|    |—— folder1
|        |—— a.py
|        |—— b.py
|    |—— folder2
|        |—— c.py
|        |—— copy.py
|        |—— d.py

我在copy.py中定义一个函数deepcopy,它的作用仅仅输出一句话

而原'c:\\programdata\\anaconda3\\envs\\fastreid\\lib'中的copy.py中的同名函数deepcopy的作用是对变量进行深拷贝,全新的复制了一份变量,他们具有不同的地址,相同的值。

现在我在c.py中import copy,那么它会引用哪个呢?

可以看到引用了同目录下的copy.py而不是python环境中的。

这似乎说明了引用的原则 : 优先引用执行文件的目录下的文件。

这时我又产生了疑问,那如果我像之前一样,把copy.py的目录加入sys.path中,不在同目录下引用也是一样的么?

|—— python
|    |—— demo.py
|    |—— folder1
|        |—— a.py
|        |—— b.py
|    |—— folder2
|        |—— c.py
|        |—— copy.py
|        |—— d.py

我在demo.py中把folder2 的目录路径加入sys.path,直接使用import copy引用

很奇怪,居然没有输出,这说明它调用的是python环境中的copy.py对“123”进行了深拷贝。

哦我发现了,我使用的是sys.path.append,在列表结尾加入,也就是说现在的sys.path是这样的

['g:\\learner_lu\\code-grammar\\python',
 'c:\\programdata\\anaconda3\\envs\\fastreid\\python37.zip',
 'c:\\programdata\\anaconda3\\envs\\fastreid\\dlls',
 'c:\\programdata\\anaconda3\\envs\\fastreid\\lib',
 'c:\\programdata\\anaconda3\\envs\\fastreid',
 'c:\\users\\administrator\\appdata\\roaming\\python\\python37\\site-packages',
 'c:\\programdata\\anaconda3\\envs\\fastreid\\lib\\site-packages',
 './folder2']

那我要是在列表的头部插入呢?也就是把sys.path变为

['./folder2',
 'g:\\learner_lu\\code-grammar\\python',
 'c:\\programdata\\anaconda3\\envs\\fastreid\\python37.zip',
 'c:\\programdata\\anaconda3\\envs\\fastreid\\dlls',
 'c:\\programdata\\anaconda3\\envs\\fastreid\\lib',
 'c:\\programdata\\anaconda3\\envs\\fastreid',
 'c:\\users\\administrator\\appdata\\roaming\\python\\python37\\site-packages',
 'c:\\programdata\\anaconda3\\envs\\fastreid\\lib\\site-packages']

它又引用了我们定义的copy.py了 !

似乎我们离真相更近了,它是按照sys.path的顺序来搜索的,如果找到了就不会继续往下搜索了,也就是返回第一个找到的结果

现在我们验证一下猜想,我们再尝试一个引用一个math

|—— python
|    |—— demo.py
|    |—— folder1
|        |—— a.py
|        |—— b.py
|    |—— folder2
|        |—— c.py
|        |—— copy.py
|        |—— d.py
|        |—— math.py

新建文件math.py,定义变量pi="123",然后在c.py中引用!

咦?为什么还是原先的值,不是在同目录下我应该被优先引用么?为什么copy.py行你又犯病了?

经过仔细检查才发现,原来math是一个内置的库,与内置函数类似,也是由c语言编写。

我们使用的sys,time,math等模块都是内置库,他们需要被显式的通过import xxx引用,并且他们的引用的优先级要高于所有的其他的模块,也就是sys.path中文件只有在与内置库文件不同名是才会被引用,否则引用的一定是内置库模块

现在引用的顺序已经呼之欲出了

  • 首先检查是否是内置库,即在sys.builtin_module_names中搜索,返回第一个找到的结果
  • 其次顺序在sys.path中搜索,排在前面的优先被找到,返回第一个找到的结果

这就需要注意文件的命名规范以及函数作用域等等,除函数重载外尽量避免使用相同的命名,否则后引入的会覆盖先引入的,例如:

谁后引入使用哪个,这也警示我们在自己编写代码时一定要注意文件、函数、变量的命名规范,一旦错引乱引重载覆盖等都是不易察觉的。

我们又会发现,在demo.py中一个一个引入似乎有些太麻烦了,如果demo2、demo3、demo4都需要引入那大段大段地引入,哪怕是复制粘贴显然也不是我们想要的。能不能有方便一点的方式来引入模块呢?

我们可以在folder1下新建一个__init__.py文件,目录结构如下:

|—— python
|    |—— demo.py
|    |—— folder1
|        |—— __init__.py
|        |—— a.py
|        |—— b.py
|    |—— folder2
|        |—— c.py
|        |—— d.py

如今python3中已不需要显式的定义__init__.py文件来将某文件夹作为包来使用,不过我们可以在其中加入一些东西以方便我们使用,加入如下代码

这样我们可以直接在demo.py中引用

这里的__init__.py的作用就是在引用folder1 时会优先执行这个初始化文件,把folder1作为包来将其下所有的引用集合起来,这样只需要from folder1 import *就可以引用所有的定义了。

除此之外还能在一些文件中遇到__all__这种变量,其实我们在使用from xxx import *的时候就是调用了__all__这个变量,它默认包含所有子文件,属性是一个字符串组成的列表。当然我们可以显式的定义这个变量以达到一些效果,比如:

|—— python
|    |—— demo.py
|    |—— folder1
|        |—— __init__.py
|        |—— a.py
|        |—— b.py
-

我们在a.py下加入几个函数

使用dir()查看demo.py中引入的变量

可以看到__init__.py调用了a,b, 又继续import * 调用a.py b.py中所有的函数 ,所有函数都可以使用

但如果我们在a.py中加入__all__的限制

fx这个函数就会被抛弃,不会被引用。这种方式通常被用来剔除当前文件中的一些私有基类以及基函数,不希望被引用。

类似的我们也可以在__init__.py中使用

这样在demo.py中只能使用约束在__all__中的函数或变量,可以手动区分命名空间,防止污染

需要注意的是,只有在使用from xxx import *这种引用方式时才涉及到__all__,其他的引用方式设置此变量都对引用无影响。

总结

到此这篇关于python的import详解就介绍到这了,最后祝大家天天进步!!,更多相关python中import详解内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!