python模块导入细节

python 模块导入细节

官方手册:https://docs.python.org/3/tutorial/modules.html

可执行文件和模块

python 源代码文件按照功能可以分为两种类型:

  1. 用于执行的可执行程序文件
  2. 不用与执行,仅用于被其它 python 源码文件导入的模块文件

例如文件 a.py 和 b.py 在同一目录下,它们的内容分别是:

# b.py
x="var x in module b"
y=5

# a.py:
import b
import sys
print(b.x)
print(b.y)

a.py 导入其它文件 (b.py) 后,就可以使用 b.py 文件中的属性(如变量、函数等)。这里,a.py 就是可执行文件,b.py 就是模块文件,但模块名为 b,而非 b.py。

python 提供了一些标准库,是预定义好的模块文件,例如上面的 sys 模块。

在此有几个注意点,在后面会详细解释:

  1. 模块 b 的文件名为 b.py,但 import 导入的时候,使用的名称为 b,而非 b.py
  2. a.py 和 b.py 是在同一个目录下的,如果不在同目录下能否导入?
  3. 在 a.py 中访问 b.py 模块中的属性时,使用的是b.xb.y
  4. 上面都是直接以模块名导入的,python 还支持更复杂的包导入方式,例如导入 abc/b.py 时,使用import abc.b。下一篇文章会详细解释包的导入方式

python 模块搜索路径

在 a.py 中导入模块 b 的时候,python 会做一系列的模块文件路径搜索操作:b.py 在哪里?只有找到它才能读取、运行 (装载) 该模块。

在任何一个 python 程序启动时,都会将模块的搜索路径收集到 sys 模块的 path 属性中 (sys.path)。当 python 需要搜索模块文件在何处时,首先搜索内置模块,如果不是内置模块,则搜索 sys.path 中的路径列表,搜索时会从该属性列出的路径中按照从前向后的顺序进行搜索,并且只要找到就立即停止搜索该模块文件(也就是说不会后搜索的同名模块覆盖先搜索的同名模块)。

例如,在 a.py 文件中输出一下这个属性的内容:

# a.py:
import sys
print(sys.path)

结果:

['G:\\pycode', 'C:\\Program Files (x86)\\Python36-32\\python36.zip', 'C:\\Program Files (x86)\\Python36-32\\DLLs', 'C:\\Program Files (x86)\\Python36-32\\lib', 'C:\\Program Files (x86)\\Python36-32', 'C:\\Users\\malong\\AppData\\Roaming\\Python\\Python36\\site-packages', 'C:\\Program Files (x86)\\Python36-32\\lib\\site-packages']

python 模块的搜索路径包括几个方面,按照如下顺序搜索:

  • 程序文件 (a.py) 所在目录,即G:\\pycode
  • 环境变量PYTHONPATH所设置的路径 (如果定义了该环境变量,则从左向右的顺序搜索)
  • 标准库路径
  • .pth 文件中定义的路径

需要注意,上面 sys.path 的结果中,除了.zip是一个文件外,其它的搜索路径全都是目录,也就是从这些目录中搜索模块 X 的文件 X.py 是否存在。

程序所在目录

这个目录是最先搜索的,且是 python 自动搜索的,无需对此进行任何设置。从交互式 python 程序终输出 sys.path 的结果:

>>> sys.path
['', 'C:\\WINDOWS\\system32', 'C:\\Program Files (x86)\\Python36-32\\Lib\\idlelib', 'C:\\Program Files (x86)\\Python36-32\\python36.zip', 'C:\\Program Files (x86)\\Python36-32\\DLLs', 'C:\\Program Files (x86)\\Python36-32\\lib', 'C:\\Program Files (x86)\\Python36-32', 'C:\\Users\\malong\\AppData\\Roaming\\Python\\Python36\\site-packages', 'C:\\Program Files (x86)\\Python36-32\\lib\\site-packages']

其中第一个''表示的就是程序所在目录。

注意程序所在目录和当前目录是不同的。例如,在 /tmp/ 目录下执行 /pycode 中的 a.py 文件

cd /tmp
python /pycode/a.py

其中 /tmp 为当前目录,而 /pycode 是程序文件 a.py 所在的目录。如果 a.py 中导入 b.py,那么将首先搜索 /pycode,而不是 /tmp。

环境变量 PYTHONPATH

这个变量中可以自定义一系列的模块搜索路径列表,这样可以跨目录搜索 (另一种方式是设置.pth 文件)。但默认情况下这个环境变量是未设置的。

在 windows 下,设置 PYTHONPATH 环境变量的方式:命令行中输入:SystemPropertiesAdvanced-->环境变量-->系统环境变量新建

如果是多个路径,则使用英文格式的分号分隔。以下是临时设置当前命令行窗口的 PYTHONPATH:

set PYTHONPATH='D:\pypath; d:\pypath1'

在 unix 下,设置 PYTHONPATH 环境变量的方式,使用冒号分隔多个路径:另外,必须得 export 导出为环境变量

export PYTHONPATH=/tmp/pypath1:/tmp/pypath2

如果要永久生效,则写入配置文件中:

echo 'export PYTHONPATH=/tmp/pypath1:/tmp/pypath2' >/etc/profile.d/pypth.sh
chmod +x /etc/profile.d/pypth.sh
source /etc/profile.d/pypth.sh

标准库路径

在 Linux 下,标准库的路径一般是在 /usr/lib/pythonXXX/ 下 (XXX 表示 python 版本号),此目录下有些分了子目录。

例如:

['', '/usr/lib/python35.zip', '/usr/lib/python3.5', '/usr/lib/python3.5/plat-x86_64-linux-gnu', '/usr/lib/python3.5/lib-dynload', '/usr/local/lib/python3.5/dist-packages', '/usr/lib/python3/dist-packages']

其中 /usr/lib/python3.5 和其内的几个子目录都是标准库的搜索路径。

注意其中 /usr/lib/python35.zip,它是 ZIP 文件组件,当定义此文件为搜索路径时,将自动解压缩该文件,并从此文件中搜索模块。

Windows 下根据 python 安装位置的不同,标准库的路径不同。如果以默认路径方式安装的 python,则标准库路径为C:\\Program Files (x86)\\Python36-32及其分类的子目录。

.pth 文件自定义路径

可以将自定义的搜索路径放进一个.pth 文件中,每行一个搜索路径。然后将.pth 文件放在 python 安装目录或某个标准库路径内的 sitepackages 目录下即可。

这是一种替换 PYTHONPATH 的友好方式,因为不同操作系统设置环境变量的方式不一样,而以文件的方式记录是所有操作系统都通用的。

例如,windows 下,在 python 安装目录C:\\Program Files (x86)\\Python36-32下新增一个 mypath.pth 文件,内容如下:

d:\pypath1
d:\pypath2

再去输出 sys.path,将可以看到这两个路径已经放进了搜索列表中。

修改搜索路径

除了上面环境变量和.pth 文件,还可以直接修改 sys.path 或者 site.getsitepackages() 的结果。

例如,在 import 导入 sys 模块之后,可以修改 sys.path,向这个列表中添加其它搜索路径,这样之后导入其它模块的时候,也会搜索该路径。

例如:

import sys
sys.path.append('d:\\pypath3')
print(sys.path)

sys.path 的最后一项将是新添加的路径。

导入模块的细节

导入模块时的过程

python 的 import 是在程序运行期间执行的,并非像其它很多语言一样是在编译期间执行。也就是说,import 可以出现在任何地方,只有执行到这个 import 行时,才会执行导入操作。且在 import 某个模块之前,无法访问这个模块的属性。

python 在 import 导入模块时,首先搜索模块的路径,然后编译并执行这个模块文件。虽然概括起来只有两个过程,但实际上很复杂。

前文已经解释了 import 的模块搜索过程,所以这里大概介绍 import 的其它细节。

以前面的 a.py 中导入模块文件 b.py 为例:

import b

import 导入模块时,搜索到模块文件 b.py 后:

1.首先在内存中为每个待导入的模块构建 module 类的实例:模块对象。这个模块对象目前是空对象,这个对象的名称为全局变量 b

注意细节:module 类的对象,变量 b。

输出下它们就知道:

print(b)
print(type(b))

输出结果:

<module 'b' from 'g:\\pycode\\b.py'>
<class 'module'>

因为 b 是全局变量,所以当前程序文件 a.py 中不能重新对全局变量 b 进行赋值,这会使导入的模块 b 被丢弃。例如,下面是错误的:

import b
b=3
print(b.x)   # 已经没有模块 b 了

另外,因为 import 导入时是将模块对象赋值给模块变量,所以模块变量名不能是 python 中的一些关键字,比如 if、for 等,这时会报错。虽然模块文件名可以为 list、keys 等这样的内置函数名,但这会导致这些内置函数不可用,因为根据变量查找的作用域规则,首先查找全局变量,再查找内置作用域。也就是说,模块文件的文件名不能是这些关键字、也不应该是这些内置函数名

  File "g:/pycode/new.py", line 11
    import if
            ^
SyntaxError: invalid syntax

2. 构造空模块实例后,将编译、执行模块文件 b.py,并按照一定的规则将一些结果放进这个模块对象中

注意细节,编译、执行 b.py、将结果保存到模块对象中。

模块第一次被导入的时候,会进行编译,并生成.pyc 字节码文件,然后 python 执行这个 pyc 文件。当模块被再次导入时,如果检查到 pyc 文件的存在,且和源代码文件的上一次修改时间戳 mtime 完全对应 (也就是说,编译后源代码没有进行过修改),则直接装载这个 pyc 文件并执行,不会再进行额外的编译过程。当然,如果修改过源代码,将会重新编译得到新的 pyc 文件。

注意,并非所有的 py 文件都会生成编译得到的 pyc 文件,对于那些只执行一次的程序文件,会将内存中的编译结果在执行完成后直接丢弃 ( 多数时候如此,但仍有例外,比如使用compileall模块可以强制编译成 pyc 文件 ),但模块会将内存中的编译结果持久化到 pyc 文件中。另外,运行字节码 pyc 文件并不会比直接运行 py 文件更快,执行它也一样是一行行地解释、执行,唯一快的地方在于导入装载的时候无需重新编译而已。

执行模块文件 (已完成编译) 的时候,按照一般的执行流程执行:一行一行地、以代码块为单元执行。一般地,模块文件中只用来声明变量、函数等属性,以便提供给导入它的模块使用,而不应该有其他任何操作性的行为,比如 print()操作不应该出现在模块文件中,但这并非强制。

总之,执行完模块文件后,这个模块文件将有一个自己的全局名称空间,在此模块文件中定义的变量、函数等属性,都会记录在此名称空间中。

最后,模块的这些属性都会保存到模块对象中。由于这个模块对象赋值给了模块变量 b,所以通过变量 b 可以访问到这个对象中的属性 (比如变量、函数等),也就是模块文件内定义的全局属性。

只导入一次

假设 a.py 中导入了模块 b 和模块 sys,在 b.py 中也导入了模块 sys,但 python 默认对某个模块只会导入一次,如果 a.py 中先导入 sys,再导入 b,那么导入 b 并执行 b.py 的时候,会发现 sys 已经导入了,不会再去导入 sys。

实际上,python 执行程序的时候,会将所有已经导入的模块放进 sys.module 属性中,这是一个 dict,可以通过下面的方式查看已导入的模块名:

>>> import sys
>>> list(sys.module.keys())

如果某个程序文件中多次使用 import(或 from) 导入同一个模块,虽然不会报错,但实际上还是直接使用内存中已装载好的模块对象。

例如,b.py 中 x=3,导入它之后修改该值,然后再次导入,发现 b.x 并不会发生改变:

import b
print(b.x)   # 3

b.x=33
print(b.x) # 33

import b
print(b.x) # 33

但是 python 提供了 reload 进行多次重复导入的方法,见后文。

使用别名

import 导入时,可以使用as关键字指定一个别名作为模块对象的变量,例如:

import b as bb
bb.x=3
print(bb.x)

这时候模块对象将赋值给变量 bb,而不是 b,b 此时不再是模块对象变量,而仅仅只是模块名。使用别名并不会影响性能,因为它仅仅只是一个赋值过程,只不过是从原来的赋值对象变量 b 变为变量 bb 而已。

from 导入部分属性

import 语句是导入模块中的所有属性,并且访问时需要使用模块变量来引用。例如:

import b
print(b.x)

除了 import,还有一个 from 语句,表示从模块中导入部分指定的属性,且使得可以直接使用这些属性的名称来引用这些属性,而不需要加上模块变量名。例如原来 import 导入时访问变量 x 使用b.x,from 导入时只需使用 x 即可。实际上,from 导入更应该称为属性的再次赋值 (拷贝)。

例如,b.py 中定义了变量 x、y、z,同时定义了函数 f()和 g(),在 a.py 中导入这个模块文件,但只导入 x 变量和 f 函数:

# a.py 文件内容:
from b import x,f

print(x)
f()

# b.py 文件内容:
x=3
y=4
z=5
def f():
print("function f in b.py")

def g():
print("function g in b.py")

注意上面 a.py 中引用模块 b 中属性的方式没有加上b.X,而是直接使用 x 和 f() 来引用。这和 import 是不一样的。至于 from 和 import 导入时的变量名称细节,在下面的内容中会详细解释。

虽然 from 语句只导入模块的部分属性,但实际上仍然会完整地执行整个模块文件。

同样的,from 语句也可以指定导入属性的变量别名,例如,将 b.py 中的属性 x 赋值给 xx,将 y 赋值给 yy:

from b import x as xx,y as yy
print(xx)
print(yy)

from 语句还有一个特殊导入统配符号*,它表示导入模块中的所有属性。

# a.py文件:
from b import *
print(x,y,z)
f()
g()

多数时候,不应该使用from *的方式,因为我们可能会忘记某个模块中有哪些属性拷贝到了当前文件,特别是多个from *时可能会出现属性覆盖的问题。

重载模块:imp.reload()

无论时 import 还是 from,都只导入一次模块,但使用 reload() 可以强制重新装载模块。

reload()是 imp 模块中的一个函数,所以要使用 imp.reload() 之前,必须先导入 imp。

from imp import reload
reload(b)

reload()是一个函数,它的参数是一个已经成功被导入过的模块变量 ( 如果使用了别名,则应该使用别名作为 reload 的参数),也就是说该模块必须在内存中已经有自己的模块对象。

reload() 会重新执行模块文件,并将执行得到的属性完全覆盖到原有的模块对象中。也就是说,reload() 会重新执行模块文件,但不会在内存中建立新的模块对象,所以原有模块对象中的属性可能会被修改

例如,模块文件 b.py 中 x=3,导入 b 模块,修改其值为 33,然后 reload 这个模块,会发现值重新变回了 3。

import b
print(b.x)  # 3

b.x=33
print(b.x) # 33

from imp import reload
reload(b)

print(b.x) # 3

有时候 reload() 很有用,可以让程序无需重启就执行新的代码。例如,在 python 的交互式模式下导入模块 b,然后修改 python 源码,再 reload 导入:

>>> import b
>>> b.x
3

不要关掉交互式解释器,直接修改源代码中的 b=3333

>>> from imp import reload
>>> reload(b)
<module 'b' from 'G:\pycode\b.py'>
>>> b.x
3333

但正因为 reload() 重载模块会改变原始的值,这可能是很危险的行为,一定要清楚地知道它是在干什么。

导入模块时的变量名称细节

import 导入的变量

import 导入时,模块对象中的属性有自己的名称空间,然后将整个模块对象赋值给模块变量。

例如,在 a.py 中导入 b:

import b
print(b.x)

这个过程唯一和当前文件 a.py 作用域有关的就是模块对象变量 b,b.py 中声明的属性和当前文件无任何关系。无论是访问还是修改,都是直接修改这个模块对象自身作用域中的值。所以,只要模块变量 b 不出现冲突问题,可以放心地修改模块 b 中的属性。

另一方面,因为每个进程都有自己的内存空间,所以在 a.py、c.py 中都导入 b 时,a.py 中修改 b 的属性值不会影响 c.py 中导入的属性,a.py 和 c.py 中模块对象所保存的属性都是执行 b.py 后得到的,它们相互独立

from 导入的变量

from 导入模块时,会先执行完模块文件,然后将指定的部分属性重新赋值给当前程序文件的同名全局变量。

例如,在模块文件 b.py 中定义了 x、y、z 变量和 f()、g() 函数:

# b.py:
x=3
y=4
b=5
def f():
    print("function f in b.py")

def g():
print("function g in b.py")

当在 a.py 中导入 b 模块时,如果只导入 x、y 和 f():

# a.pyfrom b import x, y, f

实际上的行为是构造模块对象后,将这个模块对象对应的名称空间中的属性 x、y 和 f 重新赋值给 a.py 中的变量 x、y 和 f,然后丢弃整个模块对象以及整个名称空间。换句话说,b 不再是一个有效的模块变量 (所以和 import 不一样),来自 b 的 x,y,z,f 和 g 也都被丢弃

这里有几个细节,需要详细解释清楚,只有理解了才能搞清楚它们是怎么生效的。

假设现在模块文件 b.py 的内容为,并且 a.py 中导入 x,y,f 属性:

# b.py:
x=3
y=[1,2]
z=5
def f():
    print("function f in b.py")

def g():
print("function g in b.py")

# a.py:
from b import x,y,f

首先在执行模块文件 b.py 时,会构造好自己的模块对象,并且模块对象有自己的名称空间 (作用域),模块对象构造完成后,它的名称空间大致如下:

然后 python 会在 a.py 的全局作用域内创建和导入属性同名的全局变量 x,y 和 f,并且通过赋值的方式将模块的属性赋值给这些全局变量,也就是:

x = b.x
y = b.y
f = b.f

上面的 b 只是用来演示,实际上变量 b 是不存在的。

赋值完成后,我们和构造的整个模块对象就失去联系了,因为没有变量 b 去引用这个对象。但需要注意,这个对象并没有被删除,仅仅只是我们无法通过 b 去找到它。

所以,现在的示意图如下:

因为是赋值的方式传值的,所以在 a.py 中修改这几个变量的值时,是直接在模块对象作用域内修改的:对于不可变对象,将在此作用域内创建新对象,对于可变对象,将直接修改原始对象的值。

另一方面,由于模块对象一直保留在内存中,下次继续导入时,将直接使用该模块对象。对于 import 和 from,是直接使用该已存在的模块对象,对于 reload,是覆盖此模块对象。

例如,在 a.py 中修改不可变对象 x 和可变对象 y,之后 import 或 from 时,可变对象的值都会随之改变,因为它们使用的都是原来的模块对象:

from b import x,y

x=33
y[0]=333

from b import x,y
print((x,y)) # 输出 (3, [333, 2])

import b
print((b.x,b.y)) # 输出 (3, [333, 2])

from 导入时,由于 b 不再是模块变量,所以无法再使用 reload(b) 去重载对象。如果想要重载,只能先 import,再 reload:

from b import x,y
...CODE...

# 想要重载 b
import b
from imp import reload
reload(b)

查看模块中的属性

内置函数 dir 可用于列出某模块中定义了哪些属性 (全局名称空间)。完整的说明见help(dir)

import b
dir(b)

输出结果:

['__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__', 'f', 'g', 'x', 'y', 'z']

可见,模块的属性中除了自己定义的属性外,还有一些内置的属性,比如上面以__开头和结尾的属性。

如果 dir() 不给任何参数,则输出当前环境下定义的名称属性:

>>> import b
>>> x=3
>>> aaa=333
>>> dir()
['__annotations__', '__builtins__', '__doc__', '__loader__', '__name__', '__package__', '__spec__', 'aaa', 'b', 'x']

每个属性都对应一个对象,例如 x 对应的是 int 对象,b 对应的是 module 对象:

>>> type(x)
<class 'int'>
>>> type(b)
<class 'module'>

既然是对象,那么它们都会有自己的属性。例如:

>>> dir(x)
['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__', '__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__', '__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__', '__getnewargs__', '__gt__', '__hash__', '__index__', '__init__', '__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__', '__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__', '__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__', '__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__', '__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__', 'bit_length', 'conjugate', 'denominator', 'from_bytes', 'imag', 'numerator', 'real', 'to_bytes']

所以,也可以直接 dir 某个模块内的属性:

import b
dir(b.x)
dir(b.__name__)

dir() 不会列出内置的函数和变量,如果想要输出内置的函数和变量,可以去标准模块 builtins 中查看,因为它们定义在此模块中:

import builtins
dir(buildins)

除了内置 dir()函数可以获取属性列表 ( 名称空间),对象的__dict__属性也可以获取对象的属性字典 (名称空间),它们的结果不完全一样。详细说明参见dir() 和 __dict__ 属性区别

总的来说,获取对象 M 中一个自定义的属性age,有以下几种方法:

M.age
M.__dict__['age']
sys.modules['M'].age
getattr(M,'age')

有妙用的 __name__ 属性

前面说了,py 文件分两种:用于执行的程序文件和用于导入的模块文件。当直接使用python a.py的时候表示 a.py 是用于执行的程序文件,通过 import/from 方式导入的 py 文件是模块文件。

__name__属性用来区分 py 文件是程序文件还是模块文件:

  • 当文件是程序文件的时候,该属性被设置为__main__
  • 当文件是模块文件的时候 (也就是被导入时),该属性被设置为自身模块名

换句话说,__main__表示的是当前执行程序文件的默认模块名,想必学过其他支持包功能的语言的人很容易理解:程序都需要一个入口,入口程序所在的包就是 main 包,在 main 包中导入其它包来组织整个程序。python 也是如此,只不过它是隐式自动设置的。

对于 python 来说,因为隐式自动设置,该属性就有了特殊妙用:直接在模块文件中通过if __name__ == "__main__"来判断,然后写属于执行程序的代码,如果直接用 python 执行这个文件,说明这个文件是程序文件,于是会执行属于 if 代码块的代码,如果是被导入,则是模块文件,if 代码块中的代码不会被执行。

显然,这是 python 中非常方便的单元测试方式。

例如,写一个模块文件,里面包含一个函数,用来求给定序列的最大值和最小值:

def minmax(func,*args):
    res = args[0]
    for arg in args[1:]:
        if func(arg,res):
            res = arg
    return res

def lessthan(x,y): return x < y
def greatethan(x,y): return x > y

# 测试代码
if name == "main":
print(minmax(lessthan,3,6,2,1,4,5))
print(minmax(greatethan,3,6,2,1,4,5))

 

转载请注明出处:https://www.cnblogs.com/f-ck-need-u/p/9955485.html

如果觉得文章不错,不妨给个打赏,写作不易,各位的支持,能激发和鼓励我更大的写作热情。谢谢!