6. 模块

如果您退出 Python 解释器并再次进入,您所做的定义(函数和变量)将会丢失。因此,如果您想编写一个较长的程序,最好使用文本编辑器来准备解释器的输入,并使用该文件作为输入来运行它。这被称为创建脚本。随着程序越来越长,您可能希望将其拆分为多个文件以便于维护。您可能还想使用在多个程序中编写的便利函数,而无需将其定义复制到每个程序中。

为了支持这一点,Python 提供了一种将定义放在文件中并在脚本或解释器的交互实例中使用它们的方法。这样的文件称为模块;模块中的定义可以导入到其他模块或模块(您在顶层执行的脚本和计算器模式下可以访问的变量集合)。

模块是一个包含 Python 定义和语句的文件。文件名是模块名,并附加了后缀 .py。在模块中,模块的名称(作为字符串)可用作全局变量 __name__ 的值。例如,使用您喜欢的文本编辑器在当前目录中创建一个名为 fibo.py 的文件,内容如下

# Fibonacci numbers module

def fib(n):    # write Fibonacci series up to n
    a, b = 0, 1
    while a < n:
        print(a, end=' ')
        a, b = b, a+b
    print()

def fib2(n):   # return Fibonacci series up to n
    result = []
    a, b = 0, 1
    while a < n:
        result.append(a)
        a, b = b, a+b
    return result

现在输入 Python 解释器,并使用以下命令导入此模块

>>> import fibo

这不会将 fibo 中定义的函数的名称直接添加到当前的命名空间(有关更多详细信息,请参见Python 的作用域和命名空间);它只在那里添加了模块名称 fibo。使用模块名称,您可以访问函数

>>> fibo.fib(1000)
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987
>>> fibo.fib2(100)
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]
>>> fibo.__name__
'fibo'

如果您打算经常使用某个函数,您可以将其分配给一个局部名称

>>> fib = fibo.fib
>>> fib(500)
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377

6.1. 更多关于模块

模块可以包含可执行语句以及函数定义。这些语句旨在初始化模块。它们仅在导入语句中首次遇到模块名称时执行。[1] (如果该文件作为脚本执行,它们也会运行。)

每个模块都有自己的私有命名空间,该命名空间用作模块中定义的所有函数的全局命名空间。因此,模块的作者可以使用模块中的全局变量,而不必担心与用户的全局变量发生意外冲突。另一方面,如果您知道自己在做什么,您可以使用与引用其函数相同的表示法 modname.itemname 来访问模块的全局变量。

模块可以导入其他模块。习惯上(但不是必需的)将所有 import 语句放在模块(或脚本,就此而言)的开头。如果导入的模块名称放置在模块的顶层(任何函数或类之外),则会添加到模块的全局命名空间中。

有一个 import 语句的变体,它可以将模块中的名称直接导入到导入模块的命名空间中。例如

>>> from fibo import fib, fib2
>>> fib(500)
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377

这不会在本地命名空间中引入导入来源的模块名称(因此在示例中,未定义 fibo)。

甚至有一个变体可以导入模块定义的所有名称

>>> from fibo import *
>>> fib(500)
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377

这将导入除以下划线 (_) 开头的名称之外的所有名称。在大多数情况下,Python 程序员不会使用此功能,因为它会将一组未知的名称引入解释器,可能会隐藏一些您已经定义的内容。

请注意,通常不赞成从模块或包中导入 *,因为它通常会导致代码可读性差。但是,在交互式会话中为了节省输入是可以接受的。

如果模块名称后跟 as,则 as 后面的名称将直接绑定到导入的模块。

>>> import fibo as fib
>>> fib.fib(500)
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377

这实际上以与 import fibo 相同的方式导入模块,唯一的区别是它作为 fib 可用。

在利用 from 时也可以使用它,效果类似

>>> from fibo import fib as fibonacci
>>> fibonacci(500)
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377

备注

出于效率原因,每个模块在每个解释器会话中仅导入一次。因此,如果您更改了模块,则必须重新启动解释器 - 或者,如果只想以交互方式测试一个模块,请使用 importlib.reload(),例如 import importlib; importlib.reload(modulename)

6.1.1. 将模块作为脚本执行

当您使用以下命令运行 Python 模块时

python fibo.py <arguments>

模块中的代码将被执行,就像您导入它一样,但是 __name__ 设置为 "__main__"。这意味着通过在模块末尾添加此代码

if __name__ == "__main__":
    import sys
    fib(int(sys.argv[1]))

您可以使该文件既可以用作脚本,又可以用作可导入的模块,因为仅当模块作为“主”文件执行时,解析命令行的代码才会运行

$ python fibo.py 50
0 1 1 2 3 5 8 13 21 34

如果导入模块,则不会运行该代码

>>> import fibo
>>>

这通常用于为模块提供方便的用户界面,或用于测试目的(将模块作为脚本运行会执行测试套件)。

6.1.2. 模块搜索路径

当导入名为 spam 的模块时,解释器首先搜索具有该名称的内置模块。这些模块名称在 sys.builtin_module_names 中列出。如果找不到,则会在变量 sys.path 给定的目录列表中搜索名为 spam.py 的文件。sys.path 从以下位置初始化

  • 包含输入脚本的目录(或者未指定文件时的当前目录)。

  • PYTHONPATH(目录名称列表,与 shell 变量 PATH 的语法相同)。

  • 与安装相关的默认值(按照惯例,包括一个由 site 模块处理的 site-packages 目录)。

更多详细信息请参见 sys.path 模块搜索路径的初始化

备注

在支持符号链接的文件系统上,输入脚本所在的目录是在符号链接被跟踪后计算的。换句话说,包含符号链接的目录不会添加到模块搜索路径中。

初始化后,Python 程序可以修改 sys.path。正在运行的脚本所在的目录放置在搜索路径的开头,位于标准库路径之前。这意味着将加载该目录中的脚本,而不是库目录中同名的模块。除非有意进行替换,否则这是一个错误。有关更多信息,请参见 标准模块 部分。

6.1.3. “已编译”的 Python 文件

为了加快模块的加载速度,Python 会将每个模块的编译版本缓存到 __pycache__ 目录下,并命名为 module.version.pyc,其中 version 编码了编译文件的格式;它通常包含 Python 的版本号。例如,在 CPython 3.3 版本中,spam.py 的编译版本会缓存为 __pycache__/spam.cpython-33.pyc。这种命名约定允许来自不同发行版和不同 Python 版本的编译模块共存。

Python 会检查源代码的修改日期与编译版本,以确定它是否已过期并需要重新编译。这是一个完全自动的过程。此外,编译后的模块是平台独立的,因此同一个库可以在具有不同架构的系统之间共享。

在两种情况下,Python 不会检查缓存。首先,它总是重新编译,并且不会存储从命令行直接加载的模块的结果。其次,如果没有源代码模块,它不会检查缓存。为了支持非源代码(仅编译)发行版,编译后的模块必须位于源目录中,并且不能有源代码模块。

给专家的提示

  • 你可以在 Python 命令中使用 -O-OO 开关来减小编译模块的大小。-O 开关删除 assert 语句,-OO 开关删除 assert 语句和 __doc__ 字符串。由于某些程序可能依赖于这些信息,因此只有当你清楚自己在做什么时才应该使用此选项。“优化”的模块具有 opt- 标签,并且通常较小。未来的版本可能会更改优化的效果。

  • .pyc 文件读取程序时,程序的运行速度并不比从 .py 文件读取程序时快;.pyc 文件唯一的优点是加载速度更快。

  • 模块 compileall 可以为目录中的所有模块创建 .pyc 文件。

  • 关于此过程的更多详细信息,包括决策流程图,请参阅 PEP 3147

6.2. 标准模块

Python 附带一个标准模块库,该库在单独的文档 Python 库参考(以下简称“库参考”)中进行了描述。一些模块内置于解释器中;这些模块提供对不属于语言核心,但仍然内置的操作的访问,或者是为了提高效率,或者是为了提供对操作系统原语(如系统调用)的访问。此类模块的集合是一个配置选项,它也取决于底层平台。例如,winreg 模块仅在 Windows 系统上提供。有一个特定的模块值得关注:sys,它内置于每个 Python 解释器中。变量 sys.ps1sys.ps2 定义用作主提示符和辅助提示符的字符串。

>>> import sys
>>> sys.ps1
'>>> '
>>> sys.ps2
'... '
>>> sys.ps1 = 'C> '
C> print('Yuck!')
Yuck!
C>

这两个变量仅当解释器处于交互模式时才定义。

变量 sys.path 是一个字符串列表,它确定解释器搜索模块的路径。它被初始化为从环境变量 PYTHONPATH 获取的默认路径,如果未设置 PYTHONPATH,则从内置默认路径获取。你可以使用标准列表操作来修改它。

>>> import sys
>>> sys.path.append('/ufs/guido/lib/python')

6.3. dir() 函数

内置函数 dir() 用于查找模块定义了哪些名称。它返回一个排序的字符串列表。

>>> import fibo, sys
>>> dir(fibo)
['__name__', 'fib', 'fib2']
>>> dir(sys)  
['__breakpointhook__', '__displayhook__', '__doc__', '__excepthook__',
 '__interactivehook__', '__loader__', '__name__', '__package__', '__spec__',
 '__stderr__', '__stdin__', '__stdout__', '__unraisablehook__',
 '_clear_type_cache', '_current_frames', '_debugmallocstats', '_framework',
 '_getframe', '_git', '_home', '_xoptions', 'abiflags', 'addaudithook',
 'api_version', 'argv', 'audit', 'base_exec_prefix', 'base_prefix',
 'breakpointhook', 'builtin_module_names', 'byteorder', 'call_tracing',
 'callstats', 'copyright', 'displayhook', 'dont_write_bytecode', 'exc_info',
 'excepthook', 'exec_prefix', 'executable', 'exit', 'flags', 'float_info',
 'float_repr_style', 'get_asyncgen_hooks', 'get_coroutine_origin_tracking_depth',
 'getallocatedblocks', 'getdefaultencoding', 'getdlopenflags',
 'getfilesystemencodeerrors', 'getfilesystemencoding', 'getprofile',
 'getrecursionlimit', 'getrefcount', 'getsizeof', 'getswitchinterval',
 'gettrace', 'hash_info', 'hexversion', 'implementation', 'int_info',
 'intern', 'is_finalizing', 'last_traceback', 'last_type', 'last_value',
 'maxsize', 'maxunicode', 'meta_path', 'modules', 'path', 'path_hooks',
 'path_importer_cache', 'platform', 'prefix', 'ps1', 'ps2', 'pycache_prefix',
 'set_asyncgen_hooks', 'set_coroutine_origin_tracking_depth', 'setdlopenflags',
 'setprofile', 'setrecursionlimit', 'setswitchinterval', 'settrace', 'stderr',
 'stdin', 'stdout', 'thread_info', 'unraisablehook', 'version', 'version_info',
 'warnoptions']

不带参数,dir() 列出你当前定义的名称。

>>> a = [1, 2, 3, 4, 5]
>>> import fibo
>>> fib = fibo.fib
>>> dir()
['__builtins__', '__name__', 'a', 'fib', 'fibo', 'sys']

请注意,它列出了所有类型的名称:变量、模块、函数等。

dir() 不会列出内置函数和变量的名称。如果你想要这些名称的列表,它们在标准模块 builtins 中定义。

>>> import builtins
>>> dir(builtins)  
['ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException',
 'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning',
 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError',
 'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning',
 'EOFError', 'Ellipsis', 'EnvironmentError', 'Exception', 'False',
 'FileExistsError', 'FileNotFoundError', 'FloatingPointError',
 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError',
 'ImportWarning', 'IndentationError', 'IndexError', 'InterruptedError',
 'IsADirectoryError', 'KeyError', 'KeyboardInterrupt', 'LookupError',
 'MemoryError', 'NameError', 'None', 'NotADirectoryError', 'NotImplemented',
 'NotImplementedError', 'OSError', 'OverflowError',
 'PendingDeprecationWarning', 'PermissionError', 'ProcessLookupError',
 'ReferenceError', 'ResourceWarning', 'RuntimeError', 'RuntimeWarning',
 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError',
 'SystemExit', 'TabError', 'TimeoutError', 'True', 'TypeError',
 'UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError',
 'UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning',
 'ValueError', 'Warning', 'ZeroDivisionError', '_', '__build_class__',
 '__debug__', '__doc__', '__import__', '__name__', '__package__', 'abs',
 'all', 'any', 'ascii', 'bin', 'bool', 'bytearray', 'bytes', 'callable',
 'chr', 'classmethod', 'compile', 'complex', 'copyright', 'credits',
 'delattr', 'dict', 'dir', 'divmod', 'enumerate', 'eval', 'exec', 'exit',
 'filter', 'float', 'format', 'frozenset', 'getattr', 'globals', 'hasattr',
 'hash', 'help', 'hex', 'id', 'input', 'int', 'isinstance', 'issubclass',
 'iter', 'len', 'license', 'list', 'locals', 'map', 'max', 'memoryview',
 'min', 'next', 'object', 'oct', 'open', 'ord', 'pow', 'print', 'property',
 'quit', 'range', 'repr', 'reversed', 'round', 'set', 'setattr', 'slice',
 'sorted', 'staticmethod', 'str', 'sum', 'super', 'tuple', 'type', 'vars',
 'zip']

6.4.

包是一种通过使用“带点的模块名称”来构造 Python 模块命名空间的方法。例如,模块名称 A.B 指定一个名为 B 的子模块,该子模块位于名为 A 的包中。正如使用模块可以避免不同模块的作者担心彼此的全局变量名称一样,使用带点的模块名称可以避免 NumPy 或 Pillow 等多模块包的作者担心彼此的模块名称。

假设你想设计一个模块集合(一个“包”),用于统一处理声音文件和声音数据。存在许多不同的声音文件格式(通常通过其扩展名来识别,例如:.wav.aiff.au),因此你可能需要创建和维护一个不断增长的模块集合,用于在各种文件格式之间进行转换。你可能还想对声音数据执行许多不同的操作(例如混音、添加回声、应用均衡器函数、创建人工立体声效果),因此你还将编写源源不断的模块来执行这些操作。以下是你的包的可能结构(以分层文件系统的形式表达):

sound/                          Top-level package
      __init__.py               Initialize the sound package
      formats/                  Subpackage for file format conversions
              __init__.py
              wavread.py
              wavwrite.py
              aiffread.py
              aiffwrite.py
              auread.py
              auwrite.py
              ...
      effects/                  Subpackage for sound effects
              __init__.py
              echo.py
              surround.py
              reverse.py
              ...
      filters/                  Subpackage for filters
              __init__.py
              equalizer.py
              vocoder.py
              karaoke.py
              ...

导入包时,Python 会在 sys.path 上的目录中搜索包子目录。

需要 __init__.py 文件来使 Python 将包含该文件的目录视为包(除非使用命名空间包,这是一个相对高级的功能)。这可以防止具有通用名称(例如 string)的目录无意中隐藏模块搜索路径上稍后出现的有效模块。在最简单的情况下,__init__.py 可以只是一个空文件,但它也可以执行包的初始化代码或设置 __all__ 变量,稍后会介绍。

包的用户可以从包中导入单个模块,例如

import sound.effects.echo

这将加载子模块 sound.effects.echo。必须使用其完整名称引用它。

sound.effects.echo.echofilter(input, output, delay=0.7, atten=4)

导入子模块的另一种方法是

from sound.effects import echo

这也加载子模块 echo,并使其在没有包前缀的情况下可用,因此可以使用如下方式使用它

echo.echofilter(input, output, delay=0.7, atten=4)

另一种变体是直接导入所需的函数或变量

from sound.effects.echo import echofilter

同样,这会加载子模块 echo,但这使其函数 echofilter() 直接可用

echofilter(input, output, delay=0.7, atten=4)

请注意,当使用 from package import item 时,item 可以是包的子模块(或子包),也可以是包中定义的其他名称,如函数、类或变量。import 语句首先测试 item 是否在包中定义;如果未定义,它会假定它是一个模块并尝试加载它。如果找不到,则会引发 ImportError 异常。

相反,当使用像 import item.subitem.subsubitem 这样的语法时,除了最后一个之外,每个 item 都必须是一个包;最后一个 item 可以是一个模块或一个包,但不能是前一个 item 中定义的类、函数或变量。

6.4.1. 从包导入 *

现在,当用户编写 from sound.effects import * 时会发生什么?理想情况下,人们希望这会以某种方式进入文件系统,找到包中存在的子模块,并将它们全部导入。这可能需要很长时间,并且导入子模块可能会产生不希望的副作用,这些副作用只应在显式导入子模块时发生。

唯一的解决方案是包作者提供包的显式索引。 import 语句使用以下约定:如果包的 __init__.py 代码定义了一个名为 __all__ 的列表,则当遇到 from package import * 时,该列表将被视为应该导入的模块名称列表。 包作者有责任在发布新版本的包时保持此列表的最新状态。 如果他们认为从他们的包导入 * 没有用处,包作者也可以决定不支持它。 例如,文件 sound/effects/__init__.py 可能包含以下代码

__all__ = ["echo", "surround", "reverse"]

这将意味着 from sound.effects import * 将导入 sound.effects 包的三个命名的子模块。

请注意,子模块可能会被本地定义的名称遮蔽。 例如,如果你在 sound/effects/__init__.py 文件中添加一个 reverse 函数,则 from sound.effects import * 将只导入两个子模块 echosurround,而不会导入 reverse 子模块,因为它被本地定义的 reverse 函数遮蔽了。

__all__ = [
    "echo",      # refers to the 'echo.py' file
    "surround",  # refers to the 'surround.py' file
    "reverse",   # !!! refers to the 'reverse' function now !!!
]

def reverse(msg: str):  # <-- this name shadows the 'reverse.py' submodule
    return msg[::-1]    #     in the case of a 'from sound.effects import *'

如果没有定义 __all__,则语句 from sound.effects import * 不会 将包 sound.effects 的所有子模块导入到当前命名空间;它仅确保包 sound.effects 已被导入(可能会运行 __init__.py 中的任何初始化代码),然后导入包中定义的任何名称。这包括 __init__.py 定义的任何名称(和显式加载的子模块)。它还包括先前 import 语句显式加载的包的任何子模块。考虑以下代码

import sound.effects.echo
import sound.effects.surround
from sound.effects import *

在此示例中,当执行 from...import 语句时,echosurround 模块会在当前命名空间中导入,因为它们是在 sound.effects 包中定义的。(当定义了 __all__ 时,这也有效。)

尽管某些模块被设计为在使用 import * 时仅导出符合特定模式的名称,但在生产代码中仍然被认为是糟糕的做法。

请记住,使用 from package import specific_submodule 没有什么问题! 实际上,除非导入的模块需要使用来自不同包的同名子模块,否则这是推荐的表示法。

6.4.2. 包内引用

当包被结构化为子包时(如示例中的 sound 包),你可以使用绝对导入来引用兄弟包的子模块。例如,如果模块 sound.filters.vocoder 需要使用 sound.effects 包中的 echo 模块,则可以使用 from sound.effects import echo

你也可以使用 import 语句的 from module import name 形式编写相对导入。 这些导入使用前导点来指示相对导入中涉及的当前包和父包。 例如,从 surround 模块中,你可能会使用

from . import echo
from .. import formats
from ..filters import equalizer

请注意,相对导入是基于当前模块的名称。 由于主模块的名称始终为 "__main__",因此旨在用作 Python 应用程序主模块的模块必须始终使用绝对导入。

6.4.3. 多个目录中的包

包支持一个特殊的属性,__path__。 在执行该文件中的代码之前,它被初始化为包含保存包的 __init__.py 的目录名称的字符串的序列。 可以修改此变量;这样做会影响将来对包中包含的模块和子包的搜索。

虽然此功能并不常用,但可以用来扩展在包中找到的模块集。

脚注