模块化对于除了琐碎的软件系统之外的任何东西来说都是一个重要的属性,因为它使我们能够制作自包含的、可重用的部件,这些部件可以以新的方式组合以解决不同的问题。在 Python 中,与大多数编程语言一样,最细粒度的模块化工具是可重用函数的定义。但是 Python 还为我们提供了其他几种强大的模块化机制。
相关函数的集合本身以一种称为模块的形式组合在一起。模块是其他模块可以引用的源代码文件,允许在一个模块中定义的函数在另一个模块中重用。只要您注意避免任何循环依赖,模块就是组织程序的简单而灵活的方法。
在前面的章节中,我们已经看到可以将模块导入 REPL。我们还将向您展示如何将模块直接作为程序或脚本执行。作为这项工作的一部分,我们将研究 Python 执行模型,以确保您对代码的求值和执行有准确的理解。我们将通过演示如何使用命令行参数将基本配置数据输入到程序中并使程序可执行来结束本章。
为了说明本章,我们将从上一章末尾开发的用于从 web 托管文本文档检索单词的代码片段开始。我们将通过将代码组织成一个成熟的 Python 模块来详细说明该代码。
让我们从我们在第 2 章、字符串和集合中使用的片段开始。打开一个文本编辑器——最好是一个支持 Python 语法高亮显示的编辑器——并将其配置为在按 tab 键时,在每个缩进级别插入四个空格。您还应该检查编辑器是否使用 UTF 8 编码保存文件,因为默认情况下,Python 3 运行时希望使用 UTF 8 编码。
在主目录中创建一个名为pyfund的目录。这是我们将为本章编写代码的地方。
所有 Python 源文件都使用.py扩展名,因此让我们将在上一个模块末尾的 REPL 中编写的代码片段放入一个名为pyfund/words.py的文本文件中。文件的内容应如下所示:
from urllib.request import urlopen
with urlopen('http://sixty-north.com/c/t.txt') as story:
story_words = []
for line in story:
line_words = line.decode('utf-8').split()
for word in line_words:
story_words.append(word)您会注意到上面的代码与我们之前在 REPL 上编写的代码之间存在一些细微的差异。现在我们在代码中使用了文本文件,我们可以更加注意可读性,例如,我们在 import 语句后面放了一个空行。
继续之前保存此文件。
使用操作系统的 shell 提示符切换到控制台,并切换到新的pyfund目录:
$ cd pyfund我们只需调用 Python 并传递模块的文件名即可执行模块:
$ python3 words.py如果在 Mac 或 Linux 上,命令如下:
> python words.py在 Windows 上运行时。
当您按键进入键时,经过短暂的延迟后,您将返回系统提示。不是很令人印象深刻,但是如果没有得到响应,那么程序将按预期运行。另一方面,如果你看到了一些错误,那么它就错了。例如,HTTPError表示存在网络问题,而其他类型的错误可能表示您输入了错误的代码。
让我们在程序末尾添加另一个for-循环,以便每行打印一个单词。将以下代码添加到 Python 文件的末尾:
for word in story_words:
print(word)如果转到命令提示符并再次执行代码,您应该会看到一些输出。现在我们有了一个有用的程序的开始!
我们的模块也可以导入 REPL。让我们试试看会发生什么。启动 REPL 并导入模块。导入模块时,使用import <module-name>,在模块名称中省略.py扩展名。在我们的例子中,它看起来像这样:
$ python
Python 3.5.0 (default, Nov 3 2015, 13:17:02)
[GCC 4.2.1 Compatible Apple LLVM 6.1.0 (clang-602.0.53)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import words
It
was
the
best
of
times
. . .模块中的代码在导入时立即执行!这可能不是你所期望的,而且肯定不是很有用。为了让我们能够更好地控制代码的执行时间,并允许代码被重用,我们需要将代码放入函数中。
函数的定义使用def关键字,后跟函数名、括号中的参数列表和冒号来启动新块。让我们在 REPL 上快速定义几个函数来了解一下:
>>> def square(x):
... return x * x
...我们使用return关键字从函数返回一个值。
如前所述,我们通过在函数名后的括号中提供实际参数来调用函数:
>>> square(5)
5函数不需要显式返回值-可能会产生副作用:
>>> def launch_missiles():
... print("Missiles launched!")
...
>>> launch_missiles()
Missiles launched!您可以使用不带参数的return关键字提前从函数返回:
>>> def even_or_odd(n):
... if n % 2 == 0:
... print("even")
... return
... print("odd")
...
>>> even_or_odd(4)
even
>>> even_or_odd(5)
odd如果函数中没有显式返回,Python 将在函数末尾隐式添加一个。这种隐式返回,或者不带参数的返回,实际上会导致函数返回 None。但是请记住,REPL 不会显示 None 结果,因此我们不会看到它们。通过将返回的对象捕获到命名变量中,我们可以测试无:
>>> w = even_or_odd(31)
odd
>>> w is None
True让我们使用函数来组织单词模块。
首先,我们将把除了 import 语句之外的所有代码移到一个名为fetch_words()的函数中。您只需添加def语句并将下面的代码缩进一个额外级别即可:
from urllib.request import urlopen
def fetch_words():
with urlopen('http://sixty-north.com/c/t.txt') as story:
story_words = []
for line in story:
line_words = line.decode('utf-8').split()
for word in line_words:
story_words.append(word)
for word in story_words:
print(word)保存模块,并使用新的 Python REPL 重新加载模块:
$ python3
Python 3.5.0 (default, Nov 3 2015, 13:17:02)
[GCC 4.2.1 Compatible Apple LLVM 6.1.0 (clang-602.0.53)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import words模块导入,但是在调用fetch_words()函数之前不会提取单词:
>>> words.fetch_words()
It
was
the
best
of
times或者,我们可以导入我们的特定功能:
>>> from words import fetch_words
>>> fetch_words()
It
was
the
best
of
times到目前为止还不错,但是当我们尝试直接从操作系统外壳运行模块时会发生什么呢?
使用 Mac 或 Linux 上的Ctrl+D或 Windows 上的Ctrl+Z退出 REPL,并通过模块文件名运行 Python 3:
$ python3 words.py没有印刷文字。这是因为模块现在所做的只是定义一个函数,然后立即退出。为了制作一个模块,从中我们可以有效地将函数导入 REPL和中,这两个模块可以作为脚本运行,我们需要学习一种新的 Python 习惯用法。
Python 运行时系统定义了一些特殊的变量和属性,它们的名称由双下划线分隔。一个这样的特殊变量称为__name__,它为我们的模块提供了确定它是作为脚本运行还是导入到另一个模块或 REPL 的方法。要了解如何操作,请添加:
print(__name__)在fetch_words()功能之外添加模块的末尾。
Speaking Python aloud
You will from time to time need to talk about Python aloud, and you’ll invariably find that — like any programming language — Python has elements which don't lend themselves to human speech. The special names denoted by double underscores are a prime example because they're ubiquitous in Python and, frankly, you can only say "double underscore name double underscore" so many times before you start to think about changing careers. To help alleviate this situation, a common practice among Pythonistas is to use the term "dunder" as short hand for "surrounded by double underscores". So, for example, __name__ would be pronounced "dunder name". As an added bonus, saying "dunder" is fun! Try it and I guarantee you'll feel better.
首先,让我们将修改后的 Word 模块导入到 REPL 中:
$ python3
Python 3.5.0 (default, Nov 3 2015, 13:17:02)
[GCC 4.2.1 Compatible Apple LLVM 6.1.0 (clang-602.0.53)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import words
words我们可以看到,当导入__name__时,确实会对模块的名称求值。
简单地说,如果再次导入模块,print语句将不会执行;模块代码仅在第一次导入时执行一次:
>>> import words
>>>现在,让我们尝试将模块作为脚本运行:
$ python3 words.py
__main__在本例中,特殊的__name__变量等于字符串main,该字符串也由双下划线分隔。我们的模块可以使用这个行为来检测它是如何被使用的。我们将 print 语句替换为测试__name__值的if语句。如果该值等于main,则执行我们的函数:
if __name__ == '__main__':
fetch_words()现在,我们可以安全地导入模块,而不必过度执行我们的函数:
$ python3
>>> import words
>>>如果我们可以有效地将函数作为脚本运行:
$ python3 words.py
It
was
the
best
of
times为了在 Python 中有一个非常坚实的基础,了解 Python Po.T2A.执行模型 TyT3。这里,我们指的是在模块导入和执行期间,精确定义函数定义和其他重要事件的规则。为了帮助您理解这一点,我们将重点关注def关键字,因为您已经熟悉它。一旦您了解了 Python 是如何处理def的,您将了解 Python 的执行模型的大部分内容。
重要的是要理解:def 不仅仅是一种声明,它是一种声明。这意味着def实际上是在运行时与其他顶级模块作用域代码一起执行的。def所做的是将函数体中的代码绑定到def后面的名称。导入或运行模块时,所有顶级语句都会运行,这是定义module命名空间中函数的方法。
重申一下,def是在运行时执行的。这与许多其他语言如何处理函数定义非常不同,尤其是 C++、java 和 C 语言等编译语言。在这些语言中,函数定义由编译器在编译时进行处理,而不是在运行时。当程序实际执行时,这些函数定义是固定的。在 Python 中没有编译器,函数在执行之前不以任何形式存在(除了源代码的形式)。事实上,由于函数仅在导入时处理其def时定义,因此永远不会定义从未导入的模块中的函数。
理解 Python 函数定义的这种动态特性对于理解本书后面的重要概念至关重要,因此请确保您熟悉它。例如,如果您可以访问 IDE 中的 Python 调试器,那么在导入words.py模块时,您可能需要花一些时间逐步完成该模块。
我们有时会被问及 Python 模块、Python 脚本和 Python 程序之间的区别。任何.py文件都构成一个 Python 模块,但正如我们所看到的,可以编写模块以方便导入和执行,或者使用if __name__ == "__main__"习惯用法,两者兼而有之。
我们强烈建议,即使是简单的脚本也可以导入,因为如果您可以从 Python REPL 访问代码,那么开发和测试就会大大简化。同样,即使是只打算在生产设置中导入的模块,也可以从具有可执行测试代码中获益。出于这个原因,我们创建的几乎所有模块都使用 postscript 定义一个或多个可导入函数,以便于执行。
无论您认为模块是 Python 脚本还是 Python 程序,都是上下文和使用的问题。认为 Python 只是一个脚本工具——在 Windows 批处理文件或 UNIX shell 脚本的静脉中当然是错误的——因为许多大型复杂的应用程序都是用 Python 专门构建的。
让我们进一步完善我们的单词提取模块。首先,我们将执行一个小的重构,并将单词检索和收集与单词打印分开:
from urllib.request import urlopen
# This fetches the words and returns them as a list.
def fetch_words():
with urlopen('http://sixty-north.com/c/t.txt') as story:
story_words = []
for line in story:
line_words = line.decode('utf-8').split()
for word in line_words:
story_words.append(word)
return story_words
# This prints a list of words
def print_words(story_words):
for word in story_words:
print(word)
if __name__ == '__main__':
words = fetch_words()
print_words(words)我们这样做是因为它分离了两个重要的关注点:导入时,我们更希望将单词作为列表,但直接运行时,我们更希望打印单词 。
接下来,我们将从 if__name__ == '__main__'块中提取代码,并将其放入一个名为main()的函数中:
def main():
words = fetch_words()
print_words(words)
if __name__ == '__main__':
main()通过将此代码移动到函数中,我们可以从 REPL 对其进行测试,这在模块作用域if块中是不可能的。
现在,我们可以从 REPL 中尝试以下功能:
>>> from words import (fetch_words, print_words)
>>> print_words(fetch_words())我们利用这个机会介绍了几种新的导入声明形式。第一个新表单使用逗号分隔的列表从模块导入多个对象。括号是可选的,但如果列表变长,可以将其拆分为多行。这种形式可能是 import 语句中使用最广泛的形式。
第二个新表单使用星号通配符从模块导入所有内容:
>>> from words import *后一种形式仅建议在 REPL 临时使用。它可能会对程序造成严重破坏,因为导入的内容现在可能超出您的控制范围,在将来某个时候可能会出现名称空间冲突。
完成此操作后,我们可以从 URL 获取单词:
>>> fetch_words()
['It', 'was', 'the', 'best', 'of', 'times', 'it', 'was', 'the', 'worst','of', 'times', 'it', 'was', 'the', 'age', 'of', 'wisdom', 'it', 'was','the', 'age', 'of', 'foolishness', 'it', 'was', 'the', 'epoch', 'of','belief', 'it', 'was', 'the', 'epoch', 'of', 'incredulity', 'it', 'was','the', 'season', 'of', 'Light', 'it', 'was', 'the', 'season', 'of','Darkness', 'it', 'was', 'the', 'spring', 'of', 'hope', 'it', 'was', 'the','winter', 'of', 'despair', 'we', 'had', 'everything', 'before', 'us', 'we','had', 'nothing', 'before', 'us', 'we', 'were', 'all', 'going', 'direct','to', 'Heaven', 'we', 'were', 'all', 'going', 'direct', 'the', 'other','way', 'in', 'short', 'the', 'period', 'was', 'so', 'far', 'like', 'the','present', 'period', 'that', 'some', 'of', 'its', 'noisiest', 'authorities','insisted', 'on', 'its', 'being', 'received', 'for', 'good', 'or', 'for','evil', 'in', 'the', 'superlative', 'degree', 'of', 'comparison', 'only']由于我们已经将取数代码和打印代码分开,我们还可以打印任何单词列表:
>>> print_words(['Any', 'list', 'of', 'words'])
Any
list
of
words事实上,我们甚至可以运行主程序:
>>> main()
It
was
the
best
of
times请注意,print_words()函数对列表中的类型项并不挑剔。打印一份数字列表非常愉快:
>>> print_words([1, 7, 3])
1
7
3所以也许print_words()不是最好的名字。事实上,该函数也没有提到列表-它很乐意打印for循环能够迭代的任何集合,例如字符串:
>>> print_words("Strings are iterable too")
S
t
r
i
n
g
s
a
r
e
i
t
e
r
a
b
l
e
t
o
o因此,让我们执行一次小的重构,并将此函数重命名为print_items(),更改函数中的变量名称以适应:
def print_items(items):
for item in items:
print(item)We'll talk more about the dynamic typing in Python which allows this degree of flexibility in the next module.
最后,我们模块的一个明显改进是用我们可以传递的值替换硬编码的 URL。让我们将该值提取到fetch_words()函数的参数中:
def fetch_words(url):
with urlopen(url) as story:
story_words = []
for line in story:
line_words = line.decode('utf-8').split()
for word in line_words:
story_words.append(word)
return story_words最后一个变化实际上打破了我们的main(),因为它没有通过新的url参数。当作为独立程序运行模块时,我们需要接受 URL 作为命令行参数。Python 中的命令行参数是通过名为argv的sys模块的属性访问的,该属性是字符串列表。要使用它,我们必须首先导入程序顶部的sys模块:
import sys然后,我们从列表中获得第二个参数(索引为 1):
def main():
url = sys.argv[1]
words = fetch_words(url)
print_items(words)当然,这是可以预期的:
$ python3 words.py http://sixty-north.com/c/t.txt
It
was
the
best
of
times这看起来很好,直到我们意识到我们无法再从 REPL 有效地测试main(),因为它引用sys.argv[1],在该环境中不太可能有有用的值:
$ python3
Python 3.5.0 (default, Nov 3 2015, 13:17:02)
[GCC 4.2.1 Compatible Apple LLVM 6.1.0 (clang-602.0.53)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from words import *
>>> main()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/Users/sixtynorth/projects/sixty-north/the-python-
apprentice/manuscript/code/pyfund/words.py", line 21, in main
url = sys.argv[1]
IndexError: list index out of range
>>>解决方案是允许参数列表作为正式参数传递给main()函数,使用sys.argv作为if __name__ == '__main__' block中的实际参数:
def main(url):
words = fetch_words(url)
print_items(words)
if __name__ == '__main__':
main(sys.argv[1])再次从 REPL 进行测试,我们可以看到一切都按预期工作:
>>> from words import *
>>> main("http://sixty-north.com/c/t.txt")
It
was
the
best
of
timesPython 是开发命令行工具的好工具,您可能会发现在许多情况下需要处理命令行参数。对于更复杂的命令行处理,我们建议您查看Python 标准库 argparse模块或受启发的第三方 docopt 模块。
Figure 3.1: Moment of zen: Two between functions
您会注意到,我们的顶级函数之间有两个空行。这是现代 Python 代码的常规。
根据PEP 8 样式指南的规定,通常在模块级功能之间使用两个空行。我们发现这个约定对我们很有用,使代码更容易导航。类似地,我们在函数中使用单个空行作为逻辑中断。
我们在前面看到了如何向 REPL 寻求 Python 函数方面的帮助。让我们看看如何将这种自文档功能添加到我们自己的模块中。
Python 中的 API 文档使用了一个名为docstrings的工具。DocString 是作为命名块(如函数或模块)中的第一条语句出现的文本字符串。让我们记录一下fetch_words()函数:
def fetch_words(url):
"""Fetch a list of words from a URL."""
with urlopen(url) as story:
story_words = []
for line in story:
line_words = line.decode('utf-8').split()
for word in line_words:
story_words.append(word)
return story_words我们甚至对单行 docstring 使用三重引号字符串,因为它们可以很容易地扩展以添加更多细节。
PEP 257中记录了一种针对 docstring 的 Python 约定,但并未被广泛采用。各种工具,如Sphinx可用于从 Python docstring 构建 HTML 文档,并且每个工具都指定其首选的 docstring 格式。我们倾向于使用谷歌 Python 风格指南中的表单,因为它可以被机器解析,同时在控制台上仍然保持可读性:
def fetch_words(url):
"""Fetch a list of words from a URL.
Args:
url: The URL of a UTF-8 text document.
Returns:
A list of strings containing the words from
the document.
"""
with urlopen(url) as story:
story_words = []
for line in story:
line_words = line.decode('utf-8').split()
for word in line_words:
story_words.append(word)
return story_words现在我们将从 REPL 访问此help():
$ python3
Python 3.5.0 (default, Nov 3 2015, 13:17:02)
[GCC 4.2.1 Compatible Apple LLVM 6.1.0 (clang-602.0.53)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from words import *
>>> help(fetch_words)模块文字中关于功能fetch_words的帮助:
fetch_words(url)
Fetch a list of words from a URL.
Args:
url: The URL of a UTF-8 text document.
Returns:
A list of strings containing the words from
the document.我们将为其他功能添加类似的docstrings:
def print_items(items):
"""Print items one per line.
Args:
items: An iterable series of printable items.
"""
for item in items:
print(item)
def main(url):
"""Print each word from a text document from at a URL.
Args:
url: The URL of a UTF-8 text document.
"""
words = fetch_words(url)
print_items(words)一个用于模块本身。模块docstrings应放在模块的开头,在任何语句之前:
"""Retrieve and print words from a URL.
Usage:
python3 words.py <URL>
"""
import sys
from urllib.request import urlopen现在当我们对整个模块进行help()请求时,我们得到了很多有用的信息:
$ python3
Python 3.5.0 (default, Nov 3 2015, 13:17:02)
[GCC 4.2.1 Compatible Apple LLVM 6.1.0 (clang-602.0.53)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import words
>>> help(words)
Help on module words:
NAME
words - Retrieve and print words from a URL.
DESCRIPTION
Usage:
python3 words.py <URL>
FUNCTIONS
fetch_words(url)
Fetch a list of words from a URL.
Args:
url: The URL of a UTF-8 text document.
Returns:
A list of strings containing the words from
the document.
main(url)
Print each word from a text document from at a URL.
Args:
url: The URL of a UTF-8 text document.
print_items(items)
Print items one per line.
Args:
items: An iterable series of printable items.
FILE
/Users/sixtynorth/the-python-apprentice/words.py
(END)我们相信 docstring 是 Python 代码中大多数文档的合适位置。它们解释了如何使用模块提供的设施,而不是如何工作。理想情况下,您的代码应该足够干净,不需要辅助解释。然而,有时有必要解释为什么选择了一种特定的方法或使用了一种特定的技术,我们可以使用 Python 注释来做到这一点。Python 中的注释以#开头,并继续到行尾。
作为演示,让我们记录一个事实,即我们在调用main()时使用sys.argv[1]而不是sys.argv[0]的原因可能并不明显:
if __name__ == '__main__':
main(sys.argv[1]) # The 0th arg is the module filename.在类 Unix 系统中,脚本的第一行通常包含一条特殊注释#!,称为shebang。这允许程序加载器识别应该使用哪个解释器来运行程序。Shebangs 还有一个额外的用途,即方便地在文件顶部记录其中的 Python 代码是 Python 2 还是 Python 3。
shebang 命令的确切细节取决于 Python 在系统中的位置。典型的 Python 3 Shebang 使用 Unixenv程序在PATH环境变量上定位 Python 3,这与 Python 虚拟环境非常兼容:
#!/usr/bin/env python3在 Mac 或 Linux 上,我们必须使用chmod命令将脚本标记为可执行文件,然后 shebang 才会生效:
$ chmod +x words.py完成后,我们现在可以直接运行脚本:
$ ./words.py http://sixty-north.com/c/t.txt从 Python3.3 开始,Windows 上的 Python 还支持使用 shebang 使 Python 脚本可以使用正确版本的 Python 解释器直接执行,即使 shebang 看起来只能在类似 Unix 的系统上工作,也能在 Windows 上正常工作。这是因为 Windows Python 发行版现在使用一个名为PyLauncher的程序。PyLauncher 的可执行文件名为py.exe,它将解析 shebang 并找到相应版本的 Python。
例如,在 Windows 上的cmd提示符下,此命令足以使用 Python 3 运行脚本(即使您还安装了 Python 2):
> words.py http://sixty-north.com/c/t.txt在 Powershell 中,等效值为:
PS> .\words.py http://sixty-north.com/c/t.txt你可以在政治公众人物 397中阅读更多关于派劳彻的信息。
-
Python 模块:
-
Python 代码放在称为模块的
*.py文件中。 -
通过将模块作为第一个参数传递给 Python 解释器,可以直接执行模块。
-
模块也可以导入到 REPL 中,此时模块中的所有顶级语句都按顺序执行。
-
-
Python 函数:
-
命名函数是使用 def 关键字,后跟函数名和括号中的参数列表定义的。
-
我们可以使用 return 语句从函数返回对象。
-
不带参数的 Return 语句返回 None,每个函数体末尾的隐式返回也是如此。
-
-
模块执行:
-
我们可以通过检查特殊
__name__变量的 值来检测模块是否已导入或执行。如果它等于字符串"__main__"我们的模块已作为程序直接执行。如果在模块末尾使用顶级的if __name__ == '__main__'习惯用法满足此条件,则通过执行函数,我们可以使模块有效地可导入和可执行,这是一项重要的测试技术,即使对于短脚本也是如此。 -
模块代码仅在第一次导入时执行一次。
-
def关键字是将可执行代码绑定到函数名的语句。 -
命令行参数可以通过
sys模块的argv属性访问字符串列表。zero-th命令行参数是脚本文件名,因此索引 1 处的项是第一个真参数。 -
Python 的动态类型化意味着我们的函数在参数类型方面可以非常通用。
-
-
文档串:
-
作为函数定义第一行的文本字符串构成函数的 docstring。它们通常是包含使用信息的三引号多行字符串。
-
docstrings 中提供的函数文档可以使用 REPL 中的
help()进行检索。 -
模块 docstring 应该放在模块开头附近的任何 Python 语句(如 import 语句)之前。
-
-
评论:
-
Python 中的注释以散列字符开始,一直到行尾。
-
模块的第一行可以包含一个名为 shebang 的特殊注释,允许程序加载器在所有主要平台上启动正确的 Python 解释器。
-
-
从技术上讲,模块不必是简单的源代码文件,但就本书而言,这是一个足够的定义。
-
从技术上讲,其中一些编译语言确实提供了在运行时动态定义函数的机制。然而,到目前为止,在几乎所有情况下,这些方法都是例外而不是规则。
-
Python 代码实际上是编译成字节码的,所以从这个意义上讲,Python 有一个编译器。但是编译器所做的工作与您可能习惯于使用流行的编译、静态类型语言的工作有很大不同。