跳转至

4.3.模块

为什么需要模块

如果你想要编写一些更大的程序,肯定不想定义重复的函数、类,想让代码可以重复使用。随着程序变得越来越大,你可能想要将它分割成几个更易于维护的文件。为了满足这些需要,Python 提供了一个方法可以从文件中获取定义,在脚本或者解释器的一个交互式实例中使用。这样的文件被称为模块。也就是说一个模块就是一个.py文件。

模块搜索路径

Python 怎样知道从何处找到模块文件?

如果你熟悉命令行,那么这个问题对你来说就不难理解。在命令行中执行的任何命令,实际上背后都对应了一个可执行文件。命令行解释器(比如 cmd、powershell 或 bash)会从一个全局的环境变量 PATH 中读取一个有序的列表。这个列表包含了一系列的路径,而命令行解释器,会依次在这些路径里,搜索需要的可执行文件。

Python 搜寻模块文件也遵循了类似的思路。比如,用户在 Python 中尝试导入import foobar,那么

  • 首先,Python 会在内建模块中搜寻 foobar;
  • 若未找到,则 Python 会在当前工作路径(当前脚本所在路径,或者执行 Python 解释器的路径)中搜寻 foobar;
  • 若仍未找到,则 Python 会在环境变量 PYTHONPATH 中指示的路径中搜寻 foobar;
  • 若依旧未能找到,则 Python 会在安装时指定的路径中搜寻 foobar;
  • 若仍旧失败,则 Python 会报错,提示找不到 foobar 这个模块。

添加搜索路径

当我们使用import加载自己编写的 py 代码时,py 文件必须放在 Python 的搜索路径下才可以导入。

添加临时路径——sys.path.append

在 Python 中导入sys模块,sys模块的path变量是一个列表类型的对象,其内容就是搜索路径:

import sys
print(type(sys.path))
sys.path
<class 'list'>
['/home/.../pycharm-2021.1.1/plugins/python/helpers/pycharm_display',
 '/home/.../miniconda3/envs/blog/lib/python39.zip',
 '/home/.../miniconda3/envs/blog/lib/python3.9',
 '/home/.../miniconda3/envs/blog/lib/python3.9/lib-dynload',
 '/home/.../miniconda3/envs/blog/lib/python3.9/site-packages',
 '/home/.../pycharm-2021.1.1/plugins/python/helpers/pycharm_matplotlib_backend',
 '/home/.../miniconda3/envs/blog/lib/python3.9/site-packages/IPython/extensions']

既然是list对象,那么使用append方法可以向列表添加新的元素,假设我们要添加的路径是"C:\Users\Android\Desktop"

import sys
sys.path.append("C:/Users/Android/Desktop")

但这只是临时添加路径,如果重启Python console,添加的路径就会消失。

修改环境变量

大部分Linux用户应该知道如何添加环境变量,而对于 win 10 用户,选择->控制面板\系统和安全\系统->高级系统设置->环境变量,找到Path后选择编辑,添加新的路径。

但这种方法不能适用所有 Python 的功能,因为这并未将要查找的路径添加到 Python 的 Path 系统环境中,在sys.path的列表中将找不到添加在环境变量中的路径。也就是说这样做并没有将路径添加到 Python 的搜索路径中。

添加永久搜索路径——增加.pth文件

在 site-packages 文件夹添加.pth文件,可以实现搜索路径的永久添加。方法如下:

先编辑一个扩展名为.pth的文本文件,在这个文本文件里面写上要添加的路径就可以了,例如:文本文件的文件名是test.pth,其内容如下:

C:\Users\Android\Desktop

接着获取当前 Python 环境的 site-packages 文件夹的路径:

import site
site.getsitepackages()
['/home/.../miniconda3/envs/blog/lib/python3.9/site-packages']

然后将test.pth文件放进/home/.../miniconda3/envs/blog/lib/python3.9/site-packages里面即可。

模块的导入

在 python 用 import 或者 from...import 来导入相应的模块。

导入整个模块:

例如,我们要使用使用sys模块,那么首先要导入该模块:

import sys

导入sys模块后,我们就有了变量sys指向该模块,利用sys这个变量,就可以访问sys模块的所有功能。 也就是说导入整个模块的做法会导致命名空间的修改。

也可以一次性导入多个模块:

import os, sys, time

通过as关键字,可以在导入模块的时候,给模块定义别名:

import sys as system

从某个模块中导入函数:

from somemodule import somefunction

从某个模块中导入多个函数

from somemodule import firstfunc, secondfunc, thirdfunc

将某个模块中的全部函数导入

from somemodule import *

一般而言,不推荐导入模块内的所有公开标志符(没有前缀`_`的标志符)。因为导入模块会修改命名空间,而通常你不知道模块定义了哪些符号,是否与当前的命名空间有重名的符号。重名的标志符会被重新绑定值,代码便会出错。

模块重载

出于性能考虑,每个模块在每个 Python 解释器会话中只导入一遍。 因此,如果模块的代码修改了,要重新导入,需要重启解释器;如果想交互式地测试一个模块,或者动态地加载一个模块,则需要使用以下方法重新导入模块:

  • Python 2.x:
reload(modulename)
  • Python 2.x 至 Python3.3:
import imp
imp.reload(modulename)
  • Python3.4+:
import importlib
importlib.reload(modulename)

注意

只能重载模块,不能单独重载模块中的某个函数、类、变量。

另外,Spyder IDE提供了UMR(user module reloader)功能,可以自动重载被修改的模块。

__name__

和 Python 中的其它对象一样,Python 模块也包含一些双下划线开头和结尾的特殊变量。对于模块来说,最重要的特殊变量是__name__,这代表模块的名字。每当解释器执行脚本,它就会为该脚本赋予一个名字:

  • 如果脚本作为主程序,该脚本的__name__变量被定义为"__main__"
  • 对于被import进主程序的模块来说,__name__则被定义为脚本的文件名(base filename)

因此,我们可以用如下的形式,在模块代码中定义一些测试代码:

if __name__ == "__main__":
    ...

当脚本被直接运行时,测试代码就会被运行;当脚本作为模块被导入,测试代码不会被运行。

作用域

在一个模块中,我们可能会定义很多函数和变量,但有的函数和变量我们希望给别人使用,有的函数和变量我们希望仅仅在模块内部使用(隐藏代码内部逻辑),在Python中,这是通过_前缀来实现的。

正常的函数和变量名是公开的(public),可以被直接引用,比如:abcx123PI等。

类似__xxx__这样的变量是特殊变量,可以被直接引用,但是有特殊用途,例如模块的文档注释可以用特殊变量__doc__访问,__author__变量用来定义模块作者。我们自己的变量名称一般不要用双下划线开头和结尾。

类似_xxx__xxx这样的函数或变量是非公开的(private),不应该被直接引用,比如_abc__abc等。之所以说私有函数和变量“不应该”被直接引用,而不是“不能”被直接引用,是因为 Python 并没有一种方法可以完全限制访问私有变量,但是,从编程习惯上不应该引用私有变量。

“编译的” Python 文件——pyc 文件

装载大量文本文件的速度是很慢的,LaTeX 用户对此应该有所体会。为了应对大量模块被导入的场景,Python 也采用了类似 LaTeX 的解决方案:先将模块编译成容易装载的文件,并在__pycache__目录下以 module.*version*.pyc 的名字缓存这些文件(相当于 LaTeX 的 dump 格式文件 .fmt)。

这里的版本编制了编译后文件的格式。它通常会包含 Python 的版本号。例如,在 CPython 3.3 版中,spam.py 编译后的版本将缓存为 __pycache__/spam.cpython-33.pyc。这种命名约定允许来自不同发布和不同版本的编译的模块同时存在。

当 Python 编译好模块之后,下次载入时,Python 就会读取相应的 .pyc 文件,而不是 .py 文件。装载 .pyc 文件会比装载 .py 文件更快。

Python 会检查源文件与编译版的修改日期以确定它是否过期并需要重新编译。 这是完全自动化的过程。同时,.pyc文件是跨平台的,所以同一个库可以在不同架构的系统之间共享。

Python 不检查在两个不同环境中的缓存。首先,它会永远重新编译而且不会存储直接从命令行加载的模块。其次,如果没有源模块它不会检查缓存。若要支持没有源文件(只有编译版)的发布,编译后的模块必须在源目录下,并且必须没有源文件的模块。

部分高级技巧:

  • 为了减少一个编译模块的大小,可以在 Python 命令行中使用 -O 或者 -OO-O 参数删除了断言语句,-OO 参数删除了断言语句和 doc 字符串。

    因为某些程序依赖于这些变量的可用性,你应该只在确定无误的场合使用这一选项。 “优化的” 模块有一个 .pyo 后缀而不是 .pyc 后缀。未来的版本可能会改变优化的效果。

  • 来自 .pyc 文件或 .pyo 文件中的程序不会比来自 .py 文件的运行更快. .pyc.pyo 文件只是在加载的时候更快一些。

  • compileall 模块可以为指定目录中的所有模块创建 .pyc 文件(或者使用 -O 参数创建 .pyo 文件)。

  • PEP 3147 中有很多关这一部分内容的细节,并且包含了一个描述如何加载模块的流程图。