Python:import语句的详细解析(绝对路径导入和相对路径导入)

[an error occurred while processing the directive]

import语句是Python中一个很重要的机制,允许在一个文件中访问另一个文件的函数、类、变量等,本文就将进行详细介绍。

在具体谈论import语句前,首先介绍相关的前置知识——导入的搜索目录。

导入的搜索目录

既然要求在一个文件访问其他文件,那么如何找到其他文件就是一个问题,这由sys包中的path列表决定,它在Python解释器在启动时会自动初始化,并属于整个Python解释器而不是某个文件。

初始化后的sys.path列表由多个元素组成,每个元素都是一个字符串,表示在导入时会依次搜索的路径(除内建模块外,比如math)。

sys.path列表第一个元素会根据Python解释器的启动方式而定:

1、如果是以交互式启动,则会被设置为空字符串,以表示当前工作目录,如例1所示(空字符串永远代表当前工作目录,即使在交互式Python中使用os.chdir函数改变了当前工作目录)。

# 例1

# 命令行

(test) C:\Users\12078\Desktop>python

Python 3.10.14 | packaged by Anaconda, Inc. | (main, May 6 2024, 19:44:50) [MSC v.1916 64 bit (AMD64)] on win32

Type "help", "copyright", "credits" or "license" for more information.

>>> import sys

>>> sys.path

['', 'D:\\Anaconda\\envs\\test\\python310.zip', 'D:\\Anaconda\\envs\\test\\DLLs', 'D:\\Anaconda\\envs\\test\\lib', 'D:\\Anaconda\\envs\\test', 'D:\\Anaconda\\envs\\test\\lib\\site-packages']

2、如果以python -m命令启动,则会被设置当前工作目录,如例2所示(注意其与例1的区别)。

# 例2

# 文件test1.py

import sys

print(sys.path)

# 命令行

(test) C:\Users\12078\Desktop>python -m example.test1

['C:\\Users\\12078\\Desktop', 'D:\\Anaconda\\envs\\test\\python310.zip', 'D:\\Anaconda\\envs\\test\\DLLs', 'D:\\Anaconda\\envs\\test\\lib', 'D:\\Anaconda\\envs\\test', 'D:\\Anaconda\\envs\\test\\lib\\site-packages']

3、如果是以普通的python命令启动(最常见),则会被设置为命令行中脚本文件所在的目录,如例3所示(本文将这种情况为例)。

# 例3

# 文件test1.py

import sys

print(sys.path)

# 命令行

(test) C:\Users\12078\Desktop>python .\example\test1.py

['C:\\Users\\12078\\Desktop\\example', 'D:\\Anaconda\\envs\\test\\python310.zip', 'D:\\Anaconda\\envs\\test\\DLLs', 'D:\\Anaconda\\envs\\test\\lib', 'D:\\Anaconda\\envs\\test', 'D:\\Anaconda\\envs\\test\\lib\\site-packages']

sys.path列表中的其他元素还包括环境变量PYTHONPATH中指定的目录、标准库路径、site-packages目录等。该列表可以在程序执行时被添加、删除元素,就像一个普通的列表那样,但是这是需要谨慎,因为可能出现导入相关的问题。

import语句的语法

下面是Python官方文档中,对于import语句的描述,由于imort语句分为基本import语句和from import语句,下面将分别讨论。

import_stmt ::= "import" module ["as" identifier] ("," module ["as" identifier])*

| "from" relative_module "import" identifier ["as" identifier]

("," identifier ["as" identifier])*

| "from" relative_module "import" "(" identifier ["as" identifier]

("," identifier ["as" identifier])* [","] ")"

| "from" relative_module "import" "*"

module ::= (identifier ".")* identifier

relative_module ::= "."* module | "."+

基本import语句

基本import语句的执行分两步进行,首先会在导入的搜索目录中,根据模块的import路径查找并导入(如果已导入,则不会重复导入),随后在import语句执行位置所在的命名空间定义若干标识符(代表了模块类的实例)。

import路径

一个以.py后缀的文件或包(即目录)可以代表一个模块,对于import路径,路径分隔符不再是斜杠(对于Linux系统)或反斜杠(对于Windows系统)而是点,且对于.py后缀的文件不添加.py后缀。由于是在导入的搜索目录中对import路径进行查找,所以同一个文件,如果在不同的搜索路径下导入,使用的import路径是不同的。

图1展示了例3的文件结构,例4给出了几个import路径的示例,假设执行的是test1.py文件(因此'C:\\Users\\12078\\Desktop\\example'是sys.path列表的第一个元素)。

图1 文件结构

# 例4

Package # 这是一个包的import路径

Package.__init__ # 这是一个.py文件的import路径

Package.test2 # 这是一个.py文件的import路径

example.Package # 这是不合法的, 要相对sys.path中的路径

example.Package.test2 # 这是不合法的, 要相对sys.path中的路径

Package.test2.func1 # 这是不合法的, 一个函数不是模块

模块导入

导入一个模块,会执行模块内的所有代码(包也是一种模块,对于这种模块来说是指执行了包中的__init__.py文件),并在sys.modules字典(它和sys.path一样属于整个Python解释器)中以添加模块的import路径为键,模块对象为值,如例5所示,假设执行的是test1.py文件。Python解释器根据import路径避免重复导入,注意这不是绝对路径,所以可能两个同名文件的import路径相同导致第二个导入无法进行,也可能同一个文件因为其具有不同的import路径被重复导入。

# 例5

# 文件test1.py

import sys

print(sys.modules)

import Package.test2

print(sys.modules)

import Package

# 文件__init__.py

def fun():

print("func in __init__.py")

print("This is a package's __init__.py")

# 文件test2.py

def fun1c():

print("func1 in test2.py")

def fun2c():

print("func2 in test2.py")

print("This is test2.py")

# 命令行

(test) C:\Users\12078\Desktop>python .\example\test1.py

{'sys': , 'builtins': , '_frozen_importlib': , '_imp': , '_thread': , '_warnings': , '_weakref': , '_io': , 'marshal': , 'nt': , 'winreg': , '_frozen_importlib_external': , 'time': , 'zipimport': , '_codecs': , 'codecs': , 'encodings.aliases': , 'encodings': , 'encodings.utf_8': , '_codecs_cn': , '_multibytecodec': , 'encodings.gbk': , '_signal': , '_abc': , 'abc': , 'io': , '__main__': , '_stat': , 'stat': , '_collections_abc': , 'genericpath': , '_winapi': , 'ntpath': , 'os.path': , 'os': , '_sitebuiltins': , '_distutils_hack': , 'site': }

This is a package's __init__.py

This is test2.py

{'sys': , 'builtins': , '_frozen_importlib': , '_imp': , '_thread': , '_warnings': , '_weakref': , '_io': , 'marshal': , 'nt': , 'winreg': , '_frozen_importlib_external': , 'time': , 'zipimport': , '_codecs': , 'codecs': , 'encodings.aliases': , 'encodings': , 'encodings.utf_8': , '_codecs_cn': , '_multibytecodec': , 'encodings.gbk': , '_signal': , '_abc': , 'abc': , 'io': , '__main__': , '_stat': , 'stat': , '_collections_abc': , 'genericpath': , '_winapi': , 'ntpath': , 'os.path': , 'os': , '_sitebuiltins': , '_distutils_hack': , 'site': , 'Package': , 'Package.test2': }

可以看出,在test2.py文件(导入)执行前,Package包中的__init__.py文件先(导入)执行了,因为所有import路径上的目录(包)会从浅到深依次导入,最后导入目标模块,从导入后的sys.modules字典中也可以看到Package包比Package.test2模块添加得更早。如果在此之后,再次导入Package,__init__.py文件不会再(导入)执行了,因为Python解释器会避免重复导入。

定义标识符

在import语句执行后,会在当前命名空间定义标识符,引用了被导入的模块,用于访问导入的模块(对于导入的包,指的是其__init__.py文件)中全局命名空间下的函数、类、变量等,需要注意的是导入是针对整个Python解释器而言的,而定义标识符是针对在当前命名空间而言的,两个步骤并不一定会全部完成(如例8的as子句和之后的from import语句所示)。Python解释器中所有导入的模块都会在sys.modules字典中创建键值对,使用它可以访问所有导入的模块(谨慎使用,因为这允许一个文件可以访问另一个文件中导入的模块)。

例6中展示了在当前命名空间定义标识符的过程。

# 例6

# 文件test1.py

import math

import Package.test2 # 首先导入(执行)Package包, 随后导入(执行)Package.test2模块, 最后在全局命名空间定义标识符Package和Package.test2

Package.func() # 可以用标识符调用__init__.py中的函数

Package.test2.func1() # 可以用标识符调用test2.py中的函数

import sys

sys.modules["Package"].func() # 可以用sys.modules字典来调用__init__.py中的函数

sys.modules["Package.test2"].func1() # 也可以用sys.modules字典来调用test2.py中的函数

def ttt():

Package.func() # 局部命名空间可以访问全局命名空间的标识符

ttt() # 调用ttt函数

# 文件__init__.py

def func():

print("func in __init__.py")

print("This is a package's __init__.py")

# 文件test2.py

import sys

print(sys.modules["math"].sqrt(4)) # 使用sys可以让一个文件访问不是在该文件中导入的模块

def func1():

print("func1 in test2.py")

def func2():

print("func2 in test2.py")

print("This is test2.py")

# 命令行

(test) C:\Users\12078\Desktop>python .\example\test1.py

This is a package's __init__.py

2

This is test2.py

func in __init__.py

func1 in test2.py

func in __init__.py

func1 in test2.py

func in __init__.py

控制导入模块的执行

如果希望某些代码在模块导入时不执行,则可以如例7所示,这利用了只有直接执行而非导入执行时,__name__变量才为"__main__"的性质。

# 例7

# 文件test1.py

import Package.test2 # 首先导入(执行)Package包, 随后导入(执行)Package.test2模块, 最后在全局命名空间定义标识符Package和Package.test2

Package.func() # 可以调用__init__.py中的函数了

Package.test2.func1() # 可以调用test2.py中的函数了

def ttt():

Package.func() # 局部命名空间可以访问全局命名空间的标识符

ttt() # 调用ttt函数

# 文件__init__.py

def func():

print("func in __init__.py")

if __name__ == "__main__":

print("This is a package's __init__.py")

# 文件test2.py

def func1():

print("func1 in test2.py")

def func2():

print("func2 in test2.py")

if __name__ == "__main__":

print("This is test2.py")

# 命令行

(test) C:\Users\12078\Desktop>python .\example\test1.py

func in __init__.py

fun1c in test2.py

func in __init__.py

as子句

如果模块的路径很长,则定义的标识符也会很长,此时使用as子句就可以用指定的标识符代表该模块,如例8所示,需要小心这可能会导致标识符的覆盖问题。

# 例8

# 文件test1.py

import sys

import Package.test2 as t2 # 首先导入(执行)Package包, 随后导入(执行)Package.test2模块, 最后在全局命名空间定义标识符t2

# Package.func() 不可以调用__init__.py中的函数, 因为虽然Package包被导入, 但此时没有定义该标识符

sys.modules["Package"].func() # 此时只能用sys.modules字典来调用__init__.py中的函数

# Package.test2.func1() 不可以调用test2.py中的函数,因为虽然Package.test2模块被导入, 但此时没有定义该标识符

t2.func1() # 可以用新的标识符调用test2.py中的函数

sys.modules["Package.test2"].func1() # 也可以用sys.modules字典来调用test2.py中的函数

import Package.test2 # 由于Package包和Package.test2模块已被导入, 这导致直接在全局命名空间定义标识符Package和Package.test2

Package.func() # 用标识符来调用__init__.py中的函数

Package.test2.func1() # 用标识符来调用test2.py中的函数

# 文件__init__.py

def func():

print("func in __init__.py")

if __name__ == "__main__":

print("This is a package's __init__.py")

# 文件test2.py

def func1():

print("func1 in test2.py")

def func2():

print("func2 in test2.py")

if __name__ == "__main__":

print("This is test2.py")

# 命令行

(test) C:\Users\12078\Desktop>python .\example\test1.py

func in __init__.py

func1 in test2.py

func1 in test2.py

func in __init__.py

func1 in test2.py

多重import语句

多个import语句可以合并为同一个语句,当语句包含多个子句(由逗号分隔)时这两个步骤将对每个子句分别执行,如同这些子句被分成独立的import语句一样,例9重写了例8。

# 例9

# 文件test1.py

import Package.test2 as t2, Package.test2 # 首先导入(执行)Package包, 随后导入(执行)Package.test2模块, 接着在全局命名空间定义标识符t2, 最后在全局命名空间定义标识符Package和Package.test2

t2.func1() # 用新的标识符调用test2.py中的函数

Package.func()

Package.test2.func1()

# 文件__init__.py

def func():

print("func in __init__.py")

if __name__ == "__main__":

print("This is a package's __init__.py")

# 文件test2.py

def func1():

print("func1 in test2.py")

def func2():

print("func2 in test2.py")

if __name__ == "__main__":

print("This is test2.py")

# 命令行

(test) C:\Users\12078\Desktop>python .\example\test1.py

fun1c in test2.py

func in __init__.py

fun1c in test2.py

删除导入的模块

如果想完全删除导入的模块,其实也就是删除只想其的所有引用,每个被导入的模块有两处引用,来自import语句引入的标识符和sys.modules字典中的键值对,所以可以直接使用del语句删除import语句引入的标识符,然后del语句删除sys.modules字典中的值(这之后允许再次导入,因为解释器是根据sys.modules中的import路径决定是否能重新导入),只有删除了这两个引用,才能完全删除导入的模块,如例10所示。

# 例10

# 命令行

(test) C:\Users\12078\Desktop>python

>>> import Package

This is a package's __init__.py

>>> import sys

>>> sys.getrefcount(Package) # getrefcount函数可以得到一个对象的引用个数

3 # 三处引用分别来自于Packge标识符、sys.modules字典和getrefcount函数执行时临时添加的

>>> del sys.modules["Package"]

>>> sys.getrefcount(Package)

2

>>> del Package

from import语句

首先按照简单的情况分析,即from后跟着的路径可以像之前一样是一个普通的import路径(为区别,称为绝对路径)。在文末会对相对路径导入进行讨论,这是from import语句特有的。

import路径

与基本import语句一样,一个以.py后缀的文件或包(即目录)可以代表一个模块,对于import路径,路径分隔符不再是斜杠(对于Linux系统)或反斜杠(对于Windows系统)而是点,且对于.py后缀的文件不添加.py后缀。

模块导入

根据import路径的不同,from import语句会有不同的效果,如import路径代表了一个包,则import后的必须是一个包(目录)内(注意不是__init__.py文件中)的模块;如import路径代表了一个.py文件,则import后的必须是该文件内的全局命名空间下的函数、类、变量等。

与基本import语句相同,所有import路径上的目录(包)会从浅到深依次导入(执行),对于import路径代表了一个包,包内的模块是最后导入(执行)的。所有导入的模块都会在sys.modules字典(它和sys.path一样属于整个Python解释器)中以添加模块的import路径为键,模块对象为值。

定义标识符

如果import后的是一个包内的模块,会在当前命名空间定义标识符,引用了import后被导入的模块(注意,虽然所有import路径上的目录(包)会从浅到深依次导入,但不会定义标识符),用于访问导入的模块(对于导入的包,指的是其__init__.py文件)中全局命名空间下的函数、类、变量等;如果import路径代表了一个.py文件,则import后的函数、类、变量标识符会被引入,如例11所示。如果import后是*,代表包内的所有模块或py文件内全局命名空间的所有标识符

# 例11

# 文件test1.py

from Package import test2 # 首先导入(执行)Package包, 随后导入(执行)Package.test2模块, 最后在全局命名空间定义标识符test2

# 上面的导入的效果就像是 import Package.test2 as test2, 导入(执行)了Package包但没有定义Package标识符

# Package.func() 不可以调用__init__.py中的函数, 因为虽然Package包被导入, 但此时没有定义该标识符

import sys

sys.modules["Package"].func() # 此时只能用sys.modules字典来调用__init__.py中的函数

from Package.test2 import func1 # 在全局命名空间定义标识符func1

from Package.test2 import func2 # 在全局命名空间定义标识符func2

func1()

func2()

# 文件__init__.py

def func():

print("func in __init__.py")

if __name__ == "__main__":

print("This is a package's __init__.py")

# 文件test2.py

def func1():

print("func1 in test2.py")

def func2():

print("func2 in test2.py")

if __name__ == "__main__":

print("This is test2.py")

# 命令行

(test) C:\Users\12078\Desktop\example>python test1.py

​func in __init__.py

func1 in test2.py

func2 in test2.py

as子句

这部分的与基本import语句相同。

多重from import语句

多个from import语句可以合并为同一个语句,当语句包含多个子句(由逗号分隔,如果用可选的小括号包围时结尾可以有多余的逗号)时如同这些子句被分成独立的import语句一样,例12重写了例11。

​# 例12

# 文件test1.py

from Package.test2 import func1, func2 # 首先导入(执行)Package包, 随后导入(执行)Package.test2模块, 最后在全局命名空间定义标识符func1和func2

# from Package.test2 import (func1, func2) 等价表示

# from Package.test2 import (func1, func2,) 等价表示

func1()

func2()

# 文件__init__.py

def func():

print("func in __init__.py")

if __name__ == "__main__":

print("This is a package's __init__.py")

# 文件test2.py

def func1():

print("func1 in test2.py")

def func2():

print("func2 in test2.py")

if __name__ == "__main__":

print("This is test2.py")

# 命令行

(test) C:\Users\12078\Desktop\example>python test1.py

​func in __init__.py

func1 in test2.py

func2 in test2.py

删除导入的模块

这部分的与基本import语句相同。

相对路径导入

相对导入是一个难点,本节将详细解释。

首先忘掉sys.path列表,相对路径导入并不是在导入的搜索目录中进行搜索。我们将前面带有若干点.的路径和只由若干点组成的路径称为相对路径。

当一个模块还未完全被导入(执行)时,其实就能在sys.modules中看到它了,如例13所示。

​# 例13

# 文件test1.py

from Package.test2 import func1, func2 # 首先导入(执行)Package包, 随后导入(执行)Package.test2模块, 最后在全局命名空间定义标识符func1和func2

# 文件__init__.py

def func():

print("func in __init__.py")

if __name__ == "__main__":

print("This is a package's __init__.py")

# 文件test2.py

import sys

print(sys.modules)

def func1():

print("func1 in test2.py")

def func2():

print("func2 in test2.py")

if __name__ == "__main__":

print("This is test2.py")

# 命令行

(test) C:\Users\12078\Desktop\example>python test1.py

这和相对路径导入有什么关系呢?其实相对导入是通过sys.modules中本模块的import路径反查绝对路径来查找的。有一点需要注意,绝对路径的层级不能高于本模块的import路径的第一级。这也许听起来令人费解,例14展示了一个具体的例子。

​# 例14

# 文件test1.py

from Package.test2 import func1, func2 # 首先导入(执行)Package包, 随后导入(执行)Package.test2模块, 最后在全局命名空间定义标识符func1和func2

# 文件__init__.py

def func():

print("func in __init__.py")

if __name__ == "__main__":

print("This is a package's __init__.py")

# 文件test2.py

import sys

print(sys.modules)

print(sys.path)

del sys.path[0]

print(sys.path)

from .test3 import *

from . import test3

# from ..Package.test3 import * 报错:ImportError: attempted relative import beyond top-level package

# from .. import Package 报错:ImportError: attempted relative import beyond top-level package

print(sys.modules)

def func1():

print("func1 in test2.py")

def func2():

print("func2 in test2.py")

if __name__ == "__main__":

print("This is test2.py")

# 文件test3.py(与test2.py一起在Package中)

def func3():

print("func3 in test3.py")

# 命令行

(test) C:\Users\12078\Desktop\example>python test1.py

{'sys': , 'builtins': , '_frozen_importlib': , '_imp': , '_thread': , '_warnings': , '_weakref': , '_io': , 'marshal': , 'nt': , 'winreg': , '_frozen_importlib_external': , 'time': , 'zipimport': , '_codecs': , 'codecs': , 'encodings.aliases': , 'encodings': , 'encodings.utf_8': , '_codecs_cn': , '_multibytecodec': , 'encodings.gbk': , '_signal': , 'encodings.latin_1': , '_abc': , 'abc': , 'io': , '__main__': , '_stat': , 'stat': , '_collections_abc': , 'genericpath': , 'ntpath': , 'os.path': , 'os': , '_sitebuiltins': , '_locale': , '_bootlocale': , '_distutils_hack': , 'types': , 'importlib._bootstrap': , 'importlib._bootstrap_external': , 'warnings': , 'importlib': , 'importlib.machinery': , '_heapq': , 'heapq': , 'itertools': , 'keyword': , '_operator': , 'operator': , 'reprlib': , '_collections': , 'collections': , 'collections.abc': , '_functools': , 'functools': , 'contextlib': , 'enum': , '_sre': , 'sre_constants': , 'sre_parse': , 'sre_compile': , 'copyreg': , 're': , 'typing.io': , 'typing.re': , 'typing': , 'importlib.abc': , 'importlib.util': , 'mpl_toolkits': , 'sphinxcontrib': , 'zope': , 'site': , 'Package': , 'Package.test2': }

['C:\\Users\\12078\\Desktop\\example', 'D:\\Anaconda\\python39.zip', 'D:\\Anaconda\\DLLs', 'D:\\Anaconda\\lib', 'D:\\Anaconda', 'D:\\Anaconda\\lib\\site-packages', 'D:\\Anaconda\\lib\\site-packages\\win32', 'D:\\Anaconda\\lib\\site-packages\\win32\\lib', 'D:\\Anaconda\\lib\\site-packages\\Pythonwin']

['D:\\Anaconda\\python39.zip', 'D:\\Anaconda\\DLLs', 'D:\\Anaconda\\lib', 'D:\\Anaconda', 'D:\\Anaconda\\lib\\site-packages', 'D:\\Anaconda\\lib\\site-packages\\win32', 'D:\\Anaconda\\lib\\site-packages\\win32\\lib', 'D:\\Anaconda\\lib\\site-packages\\Pythonwin']

{'sys': , 'builtins': , '_frozen_importlib': , '_imp': , '_thread': , '_warnings': , '_weakref': , '_io': , 'marshal': , 'nt': , 'winreg': , '_frozen_importlib_external': , 'time': , 'zipimport': , '_codecs': , 'codecs': , 'encodings.aliases': , 'encodings': , 'encodings.utf_8': , '_codecs_cn': , '_multibytecodec': , 'encodings.gbk': , '_signal': , 'encodings.latin_1': , '_abc': , 'abc': , 'io': , '__main__': , '_stat': , 'stat': , '_collections_abc': , 'genericpath': , 'ntpath': , 'os.path': , 'os': , '_sitebuiltins': , '_locale': , '_bootlocale': , '_distutils_hack': , 'types': , 'importlib._bootstrap': , 'importlib._bootstrap_external': , 'warnings': , 'importlib': , 'importlib.machinery': , '_heapq': , 'heapq': , 'itertools': , 'keyword': , '_operator': , 'operator': , 'reprlib': , '_collections': , 'collections': , 'collections.abc': , '_functools': , 'functools': , 'contextlib': , 'enum': , '_sre': , 'sre_constants': , 'sre_parse': , 'sre_compile': , 'copyreg': , 're': , 'typing.io': , 'typing.re': , 'typing': , 'importlib.abc': , 'importlib.util': , 'mpl_toolkits': , 'sphinxcontrib': , 'zope': , 'site': , 'Package': , 'Package.test2': , 'Package.test3': }

import路径前的一个点表示当前模块的包(该例中指Package目录)内,而两个点表示所处的包的父包(该例中指example目录)内,以此类推,只使用一个点表示表示当前模块的包,而两个点表示当前模块所处的包的父包,以此类推。

文件一中使用from import语句和绝对路径导入了test2.py文件,在test2.py文件中,首先将sys.path中的第一个元素删除(这是为了确保相对导入与sys.path无关),随后使用了相对路径导入,其中前两种是正确的,而后两种是错误的,因为它们使用两个点访问了..(该例中指example目录),而test2.py的import路径是Package.test2,两个点已经高于Package目录了。 这就意味着,绝对导入在某种情况下是错误的,比如在例14中,直接执行test2.py文件是错误的(python test2.py),因为此时test2模块的import路径将会是test2而一个点已经高于它了。

这看起来很神奇,test2.py文件是否能使相对路径导入同目录的其他文件,取决于其运行方式。这也决定了相对导入是不可能单独使用的,它需要知道当前模块的import路径。

例14最后的结果显示,test3.py文件被导入后的import路径是Package.test3,这其实也是直接通过test2.py的import路径推断出来的,而与sys.path无关。

[an error occurred while processing the directive]
Copyright © 2088 1990年世界杯_世界杯球赛 - xqgjmy.com All Rights Reserved.
友情链接