Python的绝对引入和相对引入

Python的绝对引入和相对引入


参考于:https://www.bilibili.com/video/BV1EK411g7Ff

​ 在python中有一些常见的概念,并且这些概念可能会被混淆:

  • 脚本(script):一个python文件,可以直接运行用于实现特定的功能。通常不包含类和函数,只是用来执行。
  • 模块(module):也是一个python文件,通常包含了一些类和函数,用来被其他文件引用,包括被脚本文件引用,或是被其它模块引用。
  • 包(package):一些相关的模块的集合,本质上是一个文件夹,但通常需要一个名为__init__.py的文件(python较高版本也可以不需要该文件),内容可以为空,表明这是一个python的包,而不是一个普通的文件夹。

​ 接着,是一些查看python文件中搜索路径、命名空间和模块的方法:

  • sys.path:其是一个列表,包含了python解释器在导入模块时会搜索的所有目录路径。当尝试导入一个模块时,python就会按照这个列表中提供的路径来顺讯查找这个模块是否在路径下。其会将运行的脚本文件的所在目录添加在该列表的首部,并且其是python解释器的全局变量,会在脚本和各个模块之间共享。因此,python只知道脚本文件所在的位置,而不知道其它文件所在的位置,直接运行某个文件可能会导致其它文件中的引用失效。

  • dir():是一个内置函数,用来返回指定对象或当前局部作用域的有效属性名列表。当不提供任何参数时,就会返回当前局部作用域的名称列表,对于一个文件来说,就会返回其包含的所有的全局变量、函数、类等。可以用来查看一个文件中可用的名称。

  • sys.modules:是一个字典,保存了已经导入到当前会话中的模块,用来查看导入的模块有哪些,也是一个全局的变量,在所有涉及当前程序的python文件的模块都会保存在该字典中。其中键的名称是模块名,根据引用的方式可能只有模块名,也可能带有包名。也就是说同一个模块可能因为引用方式的不同而在该字典中有两个不同的名称,被引用两次。

绝对引入

​ 首先,我们先探索绝对引入,其方式就是import xxxfrom xxx import xxx,使用的路径不包含相对的层级结构,会直接从sys.path下搜索模块。

​ 我们在名为Temp1的文件夹下创建m1.py文件,定义了一个简单的函数,其内容为:

def f1():
    print("this is f1 in m1")

f1()

​ 并且也在Temp1下创建run.py文件,将m1.py导入到本文件中,其内容为:

import m1
m1.f1()

​ 此时的文件结构为:

Temp1
    m1.py
    run.py

​ 然后我们运行run.py文件,得到的结果如下:

E:\WorkSpace\Temp1>python run.py
this is f1 in m1
this is f1 in m1

​ 结果中,有两个this is f1 in m1,说明f1()执行了两次,之所以会出现两次,是因为在import m1时,执行了m1.py中的代码,而其中包含了f1的使用,因此执行了两次。就算使用from m1 import f1,同样也会执行两次,因为在加载一个模块或模块中的东西的时候会先执行整个模块文件中的所有代码,以确保模块能够被正确初始化,并且所有变量、函数、类等都能够被定义和初始化。

​ 解决上述问题的方法就是在m1.py中增加一个判断语句,用来判断当前文件的名称__name__是否为__main__,因为当一个文件作为脚本执行的时候其__name__会为__main__,而当做文件被当做模块引用时,则为模块名。其中,__name__是python中内置的变量,每个python文件都会有一些内置的变量,通过dir()可以查看模块中所有的变量、函数、类,包括内置变量。

​ 我们先将m1.py文件的内容修改为如下:

def f1():
    print("this is f1 in m1")

print(__name__)
if __name__ == "__main__":
    f1()

​ 然后我们执行m1.py

E:\WorkSpace\Temp1>python m1.py
__main__
this is f1 in m1

​ 我们发现m1.py的名称确实为__main__,并且可以执行f1方法。

​ 而我们执行run.py文件得到的结果如下所示:

E:\WorkSpace\Temp1>python run.py
m1
this is f1 in m1

​ 我们可以看到,__name__变为了m1,为m1.py的名称,并且f1方法只执行了一次,即在run.py中执行的,而没有在m1.py中执行。

​ 下面我们来介绍一下python是如何找到m1的,因为我们可以通过import m1,也可以使用from m1 import f1来使用f1,说明python是知道m1在哪里的,才能够将其导入到其它文件中。

​ python是通过sys.path中包含的路径来寻找模块的,我们对run.py文件进行修改,在其中增加上打印sys.path的内容:

import m1
m1.f1()

import sys
print(sys.path)

​ 然后我们运行run.py文件并查看sys.path的内容:

E:\WorkSpace\Temp1> python run.py
m1
this is f1 in m1
['E:\\WorkSpace\\Temp1', 'D:\\Development\\Anaconda\\python36.zip', 'D:\\Development\\Anaconda\\DLLs', 'D:\\Development\\Anaconda\\lib', 'D:\\Development\\Anaconda', 'D:\\Development\\Anaconda\\lib\\site-packages', 'D:\\Development\\Anaconda\\lib\\site-packages\\win32', 'D:\\Development\\Anaconda\\lib\\site-packages\\win32\\lib', 'D:\\Development\\Anaconda\\lib\\site-packages\\Pythonwin']

​ 我们可以看到,sys.path中第一个路径就是run.py所在的目录(注意,该路径为运行的脚本所在的目录,所以当该脚本引用其它模块时,其它模块的第一个路径仍然为脚本的目录,而不是模块的所在目录),然后是所安装的python环境自带的一些路径。因此,python可以找到m1模块,因为m1run.py位于同一目录下。我们直接引用某一个包时,必须保证这个包位于sys.path中某一个路径下,否则python就找不到该包:我们在Temp1下创建一个新的文件夹,名为pkg,并在其中创建一个文件为m2.py,内容为:

import sys
print(f"m2 sys.path is: {sys.path}")
def f2():
    print("this is f2 in pkg->m2")
print(f"m2.py name: {__name__}")

​ 此时的文件结构为:

Temp1
	pkg
		m2.py
    m1.py
    run.py

​ 我们修改run.py的内容,让其直接引用m2

import m1
import m2
m1.f1()


import sys
print(sys.path)

​ 运行run.py

E:\WorkSpace\Temp1>python run.py
m1
Traceback (most recent call last):
  File "run.py", line 2, in <module>
    import m2
ModuleNotFoundError: No module named 'm2'

​ 我们发现,python能够找到m1但找不到m2,尽管m2所在的pkg文件夹与run在同一目录下,即在Temp1下,但是m2本身不在Temp1下,因此python没有找到它。解决的方法有两个:(1)既然pkg是能通过sys.path找到的,可以通过pkg.m2的形式将m2模块引入;(2)既然pkg不在sys.path下,就手动将其加入进去。

​ 修改run.py,使其内容如下:

import m1
import pkg.m2

m1.f1()


import sys
sys.path.append("E:\WorkSpace\Temp1\pkg")
print(sys.path)
import m2

​ 然后我们运行run.py得到内容如下:

E:\WorkSpace\Temp1>python run.py
m1
m2 sys.path is: ['E:\\WorkSpace\\Temp1', 'D:\\Development\\Anaconda\\python36.zip', 'D:\\Development\\Anaconda\\DLLs', 'D:\\Development\\Anaconda\\lib', 'D:\\Development\\Anaconda', 'D:\\Development\\Anaconda\\lib\\site-packages', 'D:\\Development\\Anaconda\\lib\\site-packages\\win32', 'D:\\Development\\Anaconda\\lib\\site-packages\\win32\\lib', 'D:\\Development\\Anaconda\\lib\\site-packages\\Pythonwin']
m2.py name: pkg.m2
this is f1 in m1
['E:\\WorkSpace\\Temp1', 'D:\\Development\\Anaconda\\python36.zip', 'D:\\Development\\Anaconda\\DLLs', 'D:\\Development\\Anaconda\\lib', 'D:\\Development\\Anaconda', 'D:\\Development\\Anaconda\\lib\\site-packages', 'D:\\Development\\Anaconda\\lib\\site-packages\\win32', 'D:\\Development\\Anaconda\\lib\\site-packages\\win32\\lib', 'D:\\Development\\Anaconda\\lib\\site-packages\\Pythonwin', 'E:\\WorkSpace\\Temp1\\pkg']
m2 sys.path is: ['E:\\WorkSpace\\Temp1', 'D:\\Development\\Anaconda\\python36.zip', 'D:\\Development\\Anaconda\\DLLs', 'D:\\Development\\Anaconda\\lib', 'D:\\Development\\Anaconda', 'D:\\Development\\Anaconda\\lib\\site-packages', 'D:\\Development\\Anaconda\\lib\\site-packages\\win32', 'D:\\Development\\Anaconda\\lib\\site-packages\\win32\\lib', 'D:\\Development\\Anaconda\\lib\\site-packages\\Pythonwin', 'E:\\WorkSpace\\Temp1\\pkg']
m2.py name: m2

​ 我们可以看到,m2模块通过上述两种方式都可以引入进去,如果使用第二种方式,我们发现pkg已经在sys.path中了,所以可以直接找到m2。并且我们发现,这两种方式打印出的m2__name__有所不同,使用第一种方式打印的为pkg.m2,而第二种则为m2。模块的名称与是通过sys.path中哪个路径找到的有关系,在第一种方法中,是在E:\WorkSpace\Temp1找到的pkg.m2,所以模块名为pkg.m2,而第二种方法是在E:\WorkSpace\Temp1\pkg中找到的,所以模块名为m2。因为名称的不同,导致同一个包引用了两次。通过sys.modules可以查看当前加载的模块有哪些:

print(sys.modules)
{'builtins': <module 'builtins' (built-in)>, ..., 'pkg.m2': <module 'pkg.m2' from 'E:\\WorkSpace\\Temp1\\pkg\\m2.py'>, 'm2': <module 'm2' from 'E:\\WorkSpace\\Temp1\\pkg\\m2.py'>}

​ 可以看到同一个模块m2被加载了两次,虽然名称不同,但是值都为E:\\WorkSpace\\Temp1\\pkg\\m2.py

相对引入

​ 还是使用上述的文件结构和文件内容,我们继续研究相对引入。相对导入的语法规则是基于 from 语句的,其格式为from xxx import xxx,不能是import xxx。并且会包含.以表示相对的层级结构,每使用一个.就表示进入上一级的文件夹进行寻找,当只有一个.时,表示从当前目录寻找。

​ 我们在pkg下创建m3.py文件,内容为:

def f3():
    print("this is f3 in m3")

​ 目前的文件结构为:

Temp1
	pkg
		m2.py
		m3.py
    m1.py
    run.py

​ 我们将m2.py的内容修改为如下所示,实现对m3的相对引用:

import sys
print(f"m2 sys.path is: {sys.path}")
def f2():
    print("this is f2 in pkg->m2")
print(f"m2.py name: {__name__}")


from .m3 import f3

​ 将run.py的内容修改为如下所示,通过对m2的引用,同时调用f2f3方法:

import pkg.m2 as m2


m2.f2()
m2.f3()

​ 运行run.py

E:\WorkSpace\Temp1>python run.py
m2 sys.path is: ['E:\\WorkSpace\\Temp1', 'D:\\Development\\Anaconda\\python36.zip', 'D:\\Development\\Anaconda\\DLLs', 'D:\\Development\\Anaconda\\lib', 'D:\\Development\\Anaconda', 'D:\\Development\\Anaconda\\lib\\site-packages', 'D:\\Development\\Anaconda\\lib\\site-packages\\win32', 'D:\\Development\\Anaconda\\lib\\site-packages\\win32\\lib', 'D:\\Development\\Anaconda\\lib\\site-packages\\Pythonwin']
m2.py name: pkg.m2
this is f2 in pkg->m2
this is f3 in m3

​ 可以看到run.py可以运行成功,说明m2成功将f3引入到自身,所以run可以通过m2调用f3

​ 但是,我们不能直接运行m2.py

E:\WorkSpace\Temp1>python pkg/m2.py
m2 sys.path is: ['E:\\WorkSpace\\Temp1\\pkg', 'D:\\Development\\Anaconda\\python36.zip', 'D:\\Development\\Anaconda\\DLLs', 'D:\\Development\\Anaconda\\lib', 'D:\\Development\\Anaconda', 'D:\\Development\\Anaconda\\lib\\site-packages', 'D:\\Development\\Anaconda\\lib\\site-packages\\win32', 'D:\\Development\\Anaconda\\lib\\site-packages\\win32\\lib', 'D:\\Development\\Anaconda\\lib\\site-packages\\Pythonwin']
m2.py name: __main__
Traceback (most recent call last):
  File "pkg/m2.py", line 8, in <module>
    from .m3 import f3
ModuleNotFoundError: No module named '__main__.m3'; '__main__' is not a package

​ 我们可以看到,在引入m3的时候发生了错误,因为在相对引用时,会通过模块的名称,即__name__来寻找包和模块。当一个文件作为脚本运行时,其名称为__main__,而from .m3 import f3则是说在__main__下寻找f3,所以会报错 ModuleNotFoundError: No module named '__main__.m3'; '__main__' is not a package。而作为模块被引用时,其名称为pkg.m2,因此可以通过pkg.m3找到该模块。

​ 解决上述问题的方式就是修改m2的名称,将其修改为pkg.xxx,以告诉python当前的路径为pkg,至于xxx则可以任意,因为python会在当前模块所在的包中寻找,所以和当前模块的名称没有关系。但是只修改名称,python只知道可以通过pkg寻找到m3,但是不知道pkg在哪里,我们可以看到上述输出中,sys.path中第一个路径为E:\\WorkSpace\\Temp1\\pkg,而不是E:\\WorkSpace\\Temp1,并且python只会在路径下寻找包,所以尽管有Temp1\\pkg,但python仍然不知道在哪里可以寻找到pkg,所以还需要将E:\\WorkSpace\\Temp1加入到sys.path中。因此,将m2.py的内容修改为:

import sys
print(f"m2 sys.path is: {sys.path}")
def f2():
    print("this is f2 in pkg->m2")
print(f"m2.py name: {__name__}")

__name__ = "pkg.abc"
sys.path.append("E:\\WorkSpace\\Temp1")
from .m3 import f3

​ 运行m2.py

E:\WorkSpace\Temp1>python pkg/m2.py
m2 sys.path is: ['E:\\WorkSpace\\Temp1\\pkg', 'D:\\Development\\Anaconda\\python36.zip', 'D:\\Development\\Anaconda\\DLLs', 'D:\\Development\\Anaconda\\lib', 'D:\\Development\\Anaconda', 'D:\\Development\\Anaconda\\lib\\site-packages', 'D:\\Development\\Anaconda\\lib\\site-packages\\win32', 'D:\\Development\\Anaconda\\lib\\site-packages\\win32\\lib', 'D:\\Development\\Anaconda\\lib\\site-packages\\Pythonwin']

​ 可以看到,可以成功运行。接着,我么你将sys.path中的E:\\WorkSpace\\Temp1去掉,看是否还能正常运行:

import sys
print(f"m2 sys.path is: {sys.path}")
def f2():
    print("this is f2 in pkg->m2")
print(f"m2.py name: {__name__}")

__name__ = "pkg.abc"
# sys.path.append("E:\\WorkSpace\\Temp1")
from .m3 import f3
E:\WorkSpace\Temp1>python pkg/m2.py
m2 sys.path is: ['E:\\WorkSpace\\Temp1\\pkg', 'D:\\Development\\Anaconda\\python36.zip', 'D:\\Development\\Anaconda\\DLLs', 'D:\\Development\\Anaconda\\lib', 'D:\\Development\\Anaconda', 'D:\\Development\\Anaconda\\lib\\site-packages', 'D:\\Development\\Anaconda\\lib\\site-packages\\win32', 'D:\\Development\\Anaconda\\lib\\site-packages\\win32\\lib', 'D:\\Development\\Anaconda\\lib\\site-packages\\Pythonwin']
m2.py name: __main__
Traceback (most recent call last):
  File "pkg/m2.py", line 9, in <module>
    from .m3 import f3
ModuleNotFoundError: No module named 'pkg'

​ 可以看到,python已经找不到pkg在哪里了。所以,在脚本中基本上不会使用到相对引用。

​ 在相对引用时,还会遇到一个问题就是beyond top-level package,就是通过相对引用时,每加一个.就会往上找一层,那么python能够向上找多少层取决于模块的名称有多少层。例如,如果模块名为pkg.m4,那么它最多找到pkg,因为再往上就不知道其父目录是什么了。

​ 我们在pkg下创建一个文件夹名为subpkg,并在下面创建一个文件,名为m4.py,其内容为:

from ..m3 import f3
print(__name__)

​ 此时的文件结构为:

Temp1
	pkg
		m2.py
		m3.py
		subpkg
			  m4.py
    m1.py
    run.py

​ 我们修改run.py的内容,使其如下:

from pkg.subpkg.m4 import f3

f3()

​ 然后运行run.py

E:\WorkSpace\Temp1>python run.py
pkg.subpkg.m4
this is f3 in m3

​ 因为我们是通过pkg.subpkg找到的m4,所以其模块名为pkg.subpkg.m4,而m3又在pkg下,所以m4可以找到m3

​ 我们将m4的内容修改为以下内容:

from ...m1 import f1
print(__name__)

​ 将run的内容修改为以下内容:

from pkg.subpkg.m4 import f1

f1()

​ 然后运行run.py

E:\WorkSpace\Temp1>python run.py
Traceback (most recent call last):
  File "run.py", line 1, in <module>
    from pkg.subpkg.m4 import f1
  File "E:\WorkSpace\Temp1\pkg\subpkg\m4.py", line 1, in <module>
    from ...m1 import f1
ValueError: attempted relative import beyond top-level package

​ 然后我们就看到了报错信息beyond top-level package,因为最高级已经是pkg了,再往上找已经不知道是什么了。

​ 那么解决上述问题的方法就是使用绝对引用,如果担心所要寻找的包的路径不在sys.path下,可以手动加入。

### 如何在 Python 中通过绝对路径导入模块 在 Python 中,可以通过调整 `sys.path` 来实现基于绝对路径的模块导入。以下是具体方法: #### 使用 `sys.path.append()` 添加自定义路径 为了能够从指定的绝对路径导入模块,可以先将该路径添加到 `sys.path` 列表中[^2]。这样,Python 解释器会在执行 `import` 或者 `from ... import` 语句时自动搜索这些路径。 ```python import sys import os # 将目标模块所在目录加入到 sys.path 中 module_path = "/absolute/path/to/your/module" if module_path not in sys.path: sys.path.append(module_path) # 正常导入模块 import your_module_name ``` 上述代码片段展示了如何动态修改解释器的导入路径列表。注意,这里假设 `/absolute/path/to/your/module` 是实际存在的文件夹,并且其中包含了名为 `your_module_name.py` 的 Python 文件[^3]。 #### 使用 `importlib.util.spec_from_file_location()` 另一种更灵活的方式是利用标准库中的 `importlib` 模块来加载特定位置上的模块。这种方法不需要更改全局变量 `sys.path` 即可完成单次操作。 ```python import importlib.util spec = importlib.util.spec_from_file_location("module.name", "/path/to/file.py") foo = importlib.util.module_from_spec(spec) spec.loader.exec_module(foo) foo.MyClass() ``` 此方式适合于那些仅需临时访问外部脚本而不希望影响整个项目的场景[^1]。 #### 注意事项 当尝试从非标准位置导入模块时,请确认所涉及的所有相对依赖项也都能被正确解析;否则可能引发 ImportError 错误。此外,频繁改动 `sys.path` 可能带来维护困难以及潜在的安全隐患,因此建议谨慎使用这种技术手段。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值