1.2.5. 代码重用:脚本和模块

目前为止,我们在解释器中输入了所有指令。对于更长的指令集,我们需要改变思路,将代码写在文本文件(使用文本编辑器)中,这些文件我们称之为脚本模块。使用您喜欢的文本编辑器(只要它提供 Python 语法高亮),或您可能正在使用的 Scientific Python Suite 附带的编辑器。

1.2.5.1. 脚本

提示

让我们先写一个脚本,它是一个包含一系列指令的文件,这些指令每次调用脚本时都会执行。例如,指令可以从解释器中复制粘贴(但要注意遵守缩进规则!)。

Python 文件的扩展名为 .py。在一个名为 test.py 的文件中编写或复制粘贴以下几行。

message = "Hello how are you?"
for word in message.split():
print(word)

提示

现在让我们在 Ipython 解释器中交互式地执行脚本,这可能是科学计算中使用脚本最常见的方式。

注意

在 Ipython 中,执行脚本的语法是 %run script.py。例如,

In [1]: %run test.py
Hello
how
are
you?
In [2]: message
Out[2]: 'Hello how are you?'

脚本已执行。此外,脚本中定义的变量(例如 message)现在可以在解释器的命名空间中使用。

提示

其他解释器也提供执行脚本的功能(例如,在普通 Python 解释器中使用 execfile 等)。

也可以在 shell 终端(Linux/Mac 控制台或 cmd Windows 控制台)中执行脚本,以独立程序的形式执行。例如,如果我们位于与 test.py 文件相同的目录下,我们可以在控制台中执行:

$ python test.py
Hello
how
are
you?

提示

独立脚本也可以接受命令行参数

file.py

import sys
print(sys.argv)
$ python file.py test arguments
['file.py', 'test', 'arguments']

警告

不要自己实现选项解析。使用专门的模块,例如 argparse

1.2.5.2. 从模块中导入对象

In [3]: import os
In [4]: os
Out[4]: <module 'os' (frozen)>
In [5]: os.listdir('.')
Out[5]:
['systemd-private-adec3d1727fe4cd39ffbe00d42109bc0-systemd-logind.service-b9Rzjz',
'profile_aigtsgcb',
'profile_mmurb8al',
'profile_xvonz9yf',
'profile_4hs_w_mh',
'dotnet-diagnostic-1659-25920-socket',
'clr-debug-pipe-1659-25920-out',
'profile_lgnuvwyt',
'profile_xcrx5vq8',
'profile_r8j35hgb',
'systemd-private-adec3d1727fe4cd39ffbe00d42109bc0-chrony.service-fI5AKM',
'systemd-private-adec3d1727fe4cd39ffbe00d42109bc0-haveged.service-mIZITo',
'snap-private-tmp',
'profile_4ikxwrj1',
'profile_rts6jo7t',
'profile_sxoh34un',
'.Test-unix',
'profile_ho8psflt',
'profile_czgm0fx3',
'profile_fgaa3812',
'profile_31ggph82',
'profile_ecrzkms3',
'profile_4oyg93xr',
'dotnet-diagnostic-606-982-socket',
'profile_1_9wxfx4',
'profile_aq6t3dxq',
'profile_pulh7uyz',
'profile_5_tt0l9w',
'clr-debug-pipe-606-982-out',
'.X11-unix',
'.ICE-unix',
'.font-unix',
'profile_h7s5rvrk',
'profile_qsp3_q4p',
'profile_o23tpc6n',
'profile_wkzbriqc',
'clr-debug-pipe-606-982-in',
'profile_agvc7v4m',
'systemd-private-adec3d1727fe4cd39ffbe00d42109bc0-systemd-resolved.service-LZYFSu',
'profile_kcc2n4g7',
'clr-debug-pipe-1641-25689-out',
'profile_9nz6obms',
'profile_z53xjajo',
'profile_ewil8srw',
'profile_bkzd2sj_',
'clr-debug-pipe-1641-25689-in',
'profile_zk2rhhsn',
'profile_aq3hxf0g',
'profile_pu0vk1xu',
'profile_dk_b2g1k',
'profile_9fsj2k75',
'profile_0m71mlo2',
'clr-debug-pipe-1659-25920-in',
'profile_q3_da92m',
'.XIM-unix',
'profile_o6gr3f53',
'profile_ud5som2r',
'www-data-temp-aspnet-0',
'profile_0h7ljo86',
'profile_4r0wr_uf',
'profile_evxijf16',
'profile_m23qdbuk',
'profile_kxp0ehvn',
'profile__2tb6hf4',
'profile_iatzm6x3',
'profile_r3xscs1p',
'profile_tk9omgbb',
'profile_37leef3_',
'profile_om7err07',
'profile_t7peg56y',
'profile_30z2ds56',
'profile_nnurehu6',
'profile__y38edp6',
'dotnet-diagnostic-1641-25689-socket',
'profile_4esqldpc',
'profile_az_xm5hv',
'profile_zzggzu6j',
'profile_j48t_pkc',
'profile_sjaajzbs']

还有

In [6]: from os import listdir

导入简写

In [7]: import numpy as np

警告

from os import *

这被称为星号导入,请不要使用它

  • 使代码更难阅读和理解:符号来自哪里?

  • 无法通过上下文和名称来猜测功能(提示:os.name 是操作系统的名称),也无法利用制表符自动完成功能。

  • 限制了您可以使用的变量名:os.name 可能会覆盖 name,反之亦然。

  • 可能会导致模块之间的名称冲突。

  • 使代码无法静态检查未定义的符号。

提示

因此,模块是按层次结构组织代码的良好方式。实际上,我们将要使用的所有科学计算工具都是模块。

>>> import numpy as np # data arrays
>>> np.linspace(0, 10, 6)
array([ 0., 2., 4., 6., 8., 10.])
>>> import scipy as sp # scientific computing

1.2.5.3. 创建模块

提示

如果我们想要编写更大、组织更完善的程序(与简单的脚本相比),其中定义了一些对象(变量、函数、类),并且我们希望多次重复使用这些对象,那么我们必须创建自己的模块

让我们创建一个名为 demo 的模块,包含在文件 demo.py

"A demo module."
def print_b():
"Prints b."
print("b")
def print_a():
"Prints a."
print("a")
c = 2
d = 2

提示

在这个文件中,我们定义了两个函数 print_aprint_b。假设我们想从解释器中调用 print_a 函数。我们可以将文件作为脚本执行,但由于我们只需要访问 print_a 函数,因此我们更倾向于将其作为模块导入。语法如下所示。

In [8]: import demo
In [9]: demo.print_a()
a
In [10]: demo.print_b()
b

导入模块后,可以使用 module.object 语法访问其对象。不要忘记在对象名称之前加上模块名称,否则 Python 将无法识别指令。

内省

In [11]: demo?
Type: module
Base Class: <type 'module'>
String Form: <module 'demo' from 'demo.py'>
Namespace: Interactive
File: /home/varoquau/Projects/Python_talks/scipy_2009_tutorial/source/demo.py
Docstring:
A demo module.
In [12]: who
demo
In [13]: whos
Variable Type Data/Info
------------------------------
demo module <module 'demo' from 'demo.py'>
In [14]: dir(demo)
Out[14]:
['__builtins__',
'__doc__',
'__file__',
'__name__',
'__package__',
'c',
'd',
'print_a',
'print_b']
In [15]: demo.<TAB>
demo.c demo.print_a demo.py
demo.d demo.print_b demo.pyc

将模块中的对象导入主命名空间

In [16]: from demo import print_a, print_b
In [17]: whos
Variable Type Data/Info
--------------------------------
demo module <module 'demo' from 'demo.py'>
print_a function <function print_a at 0xb7421534>
print_b function <function print_b at 0xb74214c4>
In [18]: print_a()
a

警告

模块缓存

模块会被缓存:如果您修改了 demo.py,并在旧会话中重新导入它,您将获得旧的模块。

解决方案

In [10]: importlib.reload(demo)

1.2.5.4. ‘__main__’ 和模块加载

提示

有时我们希望代码在直接运行模块时执行,但在模块被另一个模块导入时不执行。 if __name__ == '__main__' 允许我们检查模块是否正在被直接运行。

文件 demo2.py

def print_b():
"Prints b."
print("b")
def print_a():
"Prints a."
print("a")
# print_b() runs on import
print_b()
if __name__ == "__main__":
# print_a() is only executed when the module is run directly.
print_a()

导入它

In [19]: import demo2
b
In [20]: import demo2

运行它

In [21]: %run demo2
b
a

1.2.5.5. 脚本还是模块?如何组织你的代码

注意

经验法则

  • 多次调用的指令集应写入函数中,以提高代码的可重用性。

  • 从多个脚本调用的函数(或其他代码段)应写入模块中,以便仅在不同的脚本中导入该模块(不要将您的函数复制粘贴到不同的脚本中!)。

模块是如何找到和导入的

当执行 import mymodule 语句时,将在给定的目录列表中搜索模块 mymodule。此列表包含安装相关的默认路径列表(例如,/usr/lib64/python3.11),以及环境变量 PYTHONPATH 指定的目录列表。

Python 搜索的目录列表由 sys.path 变量给出

In [22]: import sys
In [23]: sys.path
Out[23]:
['/home/runner/work/scientific-python-lectures/scientific-python-lectures',
'/opt/hostedtoolcache/Python/3.12.6/x64/lib/python312.zip',
'/opt/hostedtoolcache/Python/3.12.6/x64/lib/python3.12',
'/opt/hostedtoolcache/Python/3.12.6/x64/lib/python3.12/lib-dynload',
'/opt/hostedtoolcache/Python/3.12.6/x64/lib/python3.12/site-packages']

模块必须位于搜索路径中,因此您可以

  • 将您自己的模块写入搜索路径中已定义的目录(例如,$HOME/.venv/lectures/lib64/python3.11/site-packages)。您可以使用符号链接(在 Linux 上)将代码保存在其他地方。

  • 修改环境变量 PYTHONPATH 以包含包含用户定义模块的目录。

    提示

    在 Linux/Unix 上,将以下行添加到 shell 启动时读取的文件(例如 /etc/profile、.profile)中

    export PYTHONPATH=$PYTHONPATH:/home/emma/user_defined_modules
    

    在 Windows 上,https://support.microsoft.com/kb/310519 说明了如何处理环境变量。

  • 或在 Python 脚本中修改 sys.path 变量本身。

    提示

    import sys
    
    new_path = '/home/emma/user_defined_modules'
    if new_path not in sys.path:
    sys.path.append(new_path)

    然而,此方法并不十分健壮,因为它使代码的可移植性降低(依赖于用户的路径),并且每次您想从该目录中的模块导入时都必须将该目录添加到您的 sys.path 中。

另请参阅

有关模块的更多信息,请参阅 https://docs.pythonlang.cn/3/tutorial/modules.html

1.2.5.6.

包含多个模块的目录称为。包是一个包含子模块(可以有子模块,等等)的模块。一个名为 __init__.py 的特殊文件(可以为空)告诉 Python 该目录是一个 Python 包,可以从中导入模块。

$ ls
_build_utils/ fft/ _lib/ odr/ spatial/
cluster/ fftpack/ linalg/ optimize/ special/
conftest.py __init__.py linalg.pxd optimize.pxd special.pxd
constants/ integrate/ meson.build setup.py stats/
datasets/ interpolate/ misc/ signal/
_distributor_init.py io/ ndimage/ sparse/
$ cd ndimage
$ ls
_filters.py __init__.py _measurements.py morphology.py src/
filters.py _interpolation.py measurements.py _ni_docstrings.py tests/
_fourier.py interpolation.py meson.build _ni_support.py utils/
fourier.py LICENSE.txt _morphology.py setup.py

从 Ipython

In [24]: import scipy as sp
In [25]: sp.__file__
Out[25]: '/opt/hostedtoolcache/Python/3.12.6/x64/lib/python3.12/site-packages/scipy/__init__.py'
In [26]: sp.version.version
Out[26]: '1.14.1'
In [27]: sp.ndimage.morphology.binary_dilation?
Signature:
sp.ndimage.morphology.binary_dilation(
input,
structure=None,
iterations=1,
mask=None,
output=None,
border_value=0,
origin=0,
brute_force=False,
)
Docstring:
Multidimensional binary dilation with the given structuring element.
...

1.2.5.7. 良好实践

  • 使用有意义的物体名称

  • 缩进:没有选择!

    提示

    缩进在 Python 中是强制性的!冒号后面的每个命令块都比前面带有冒号的行多一个缩进级别。因此,在 def f():while: 之后必须缩进。在这些逻辑块的末尾,减少缩进深度(如果进入新块,则重新增加缩进深度,等等)。

    严格遵守缩进是摆脱其他语言中用来划分逻辑块的 {; 字符的代价。不正确的缩进会导致错误,例如

    ------------------------------------------------------------
    
    IndentationError: unexpected indent (test.py, line 2)

    一开始,所有的缩进规则可能会让人有点困惑。但是,有了清晰的缩进,并且没有额外的字符,与其他语言相比,生成的代码非常易于阅读。

  • 缩进深度:在您的文本编辑器中,您可以选择使用任意正整数的空格进行缩进(1、2、3、4、…)。但是,使用4 个空格进行缩进被认为是最佳实践。您可以将编辑器配置为将 Tab 键映射到 4 个空格的缩进。

  • 样式指南

    长行:您不应该写过长的行,这些行超过了(例如)80 个字符。长行可以使用 \ 字符来换行

    >>> long_line = "Here is a very very long line \
    
    ... that we break in two parts."

    空格

    编写间距良好的代码:在逗号之后、算术运算符周围等位置添加空格。

    >>> a = 1 # yes
    
    >>> a=1 # too cramped

    Python 代码样式指南 中给出了编写“漂亮”代码(更重要的是使用与其他人相同的约定!)的若干规则。