您当前位置:首页 ‣ 深入 Python 3 ‣
难度级别:♦♢♢♢♢
❝ 不要把你的负担埋藏在神圣的沉默中。你有问题吗?太好了。欢欣鼓舞,跳进去,去调查吧。 ❞
—— 尊者 Henepola Gunaratana
惯例要求我应该用编程的基本构建块来让你感到厌烦,这样我们就可以慢慢地构建一些有用的东西。让我们跳过这一切。这是一个完整的、可工作的 Python 程序。它可能对你来说毫无意义。别担心,因为你将逐行剖析它。但先通读一遍,看看你能从中理解什么,如果有什么的话。
SUFFIXES = {1000: ['KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'],
1024: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']}
def approximate_size(size, a_kilobyte_is_1024_bytes=True):
'''Convert a file size to human-readable form.
Keyword arguments:
size -- file size in bytes
a_kilobyte_is_1024_bytes -- if True (default), use multiples of 1024
if False, use multiples of 1000
Returns: string
'''
if size < 0:
raise ValueError('number must be non-negative')
multiple = 1024 if a_kilobyte_is_1024_bytes else 1000
for suffix in SUFFIXES[multiple]:
size /= multiple
if size < multiple:
return '{0:.1f} {1}'.format(size, suffix)
raise ValueError('number too large')
if __name__ == '__main__':
print(approximate_size(1000000000000, False))
print(approximate_size(1000000000000))
现在让我们在命令行上运行这个程序。在 Windows 上,它看起来像这样
c:\home\diveintopython3\examples> c:\python31\python.exe humansize.py 1.0 TB 931.3 GiB
在 Mac OS X 或 Linux 上,它看起来像这样
you@localhost:~/diveintopython3/examples$ python3 humansize.py 1.0 TB 931.3 GiB
刚刚发生了什么?你执行了你的第一个 Python 程序。你调用了命令行上的 Python 解释器,并传递了你想要 Python 执行的脚本的名称。该脚本定义了一个单一函数,即 approximate_size()
函数,它接受以字节为单位的精确文件大小并计算一个“漂亮”的(但近似的)大小。(你可能在 Windows 资源管理器、Mac OS X 查找器或 Linux 上的 Nautilus 或 Dolphin 或 Thunar 中看到过它。如果你将一个包含文档的文件夹显示为多列列表,它将显示一个包含文档图标、文档名称、大小、类型、上次修改日期等的表格。如果该文件夹包含一个名为 TODO
的 1093 字节的文件,你的文件管理器不会显示 TODO 1093 字节
;它会显示类似 TODO 1 KB
的内容。这就是 approximate_size()
函数所做的。)
查看脚本底部,你会看到对 print(approximate_size(参数))
的两个调用。这些是函数调用——首先调用 approximate_size()
函数并传递一些参数,然后获取返回值并直接传递给 print()
函数。print()
函数是内置的;你永远不会看到它的显式声明。你可以随时随地使用它。(有很多内置函数,还有更多被分成模块的函数。耐心点,年轻人。)
那么为什么在命令行上运行脚本每次都会给你相同的输出呢?我们稍后会讲到。首先,让我们看看 approximate_size()
函数。
⁂
Python 拥有像大多数其他语言一样的函数,但它没有像 C++ 那样分离的头文件,也没有像 Pascal 那样分离的 interface
/implementation
部分。当你需要一个函数时,只需声明它,就像这样
def approximate_size(size, a_kilobyte_is_1024_bytes=True):
关键字 def
开始函数声明,后面跟着函数名称,再后面跟着括号中的参数。多个参数用逗号分隔。
还要注意函数没有定义返回值数据类型。Python 函数不指定其返回值的数据类型;它们甚至不指定是否返回值。(实际上,每个 Python 函数都返回值;如果函数执行 return
语句,它将返回该值,否则它将返回 None
,即 Python 的空值。)
☞在一些语言中,函数(返回一个值)以
function
开头,子例程(不返回值)以sub
开头。Python 中没有子例程。一切都是函数,所有函数都返回值(即使是None
),所有函数都以def
开头。
approximate_size()
函数接收两个参数——size 和 a_kilobyte_is_1024_bytes——但两个参数都没有指定数据类型。在 Python 中,变量永远不会被显式类型化。Python 会弄清楚一个变量是什么类型并将其保存在内部。
☞在 Java 和其他静态类型语言中,你必须指定函数返回值和每个函数参数的数据类型。在 Python 中,你永远不会显式指定任何东西的数据类型。根据你分配的值,Python 会在内部跟踪数据类型。
Python 允许函数参数具有默认值;如果调用函数时没有提供参数,则参数会获得其默认值。此外,可以通过使用命名参数按任何顺序指定参数。
让我们再看看 approximate_size()
函数声明
def approximate_size(size, a_kilobyte_is_1024_bytes=True):
第二个参数 a_kilobyte_is_1024_bytes 指定了一个默认值为 True
。这意味着该参数是可选的;你可以不带它调用函数,Python 会像你用 True
作为第二个参数调用它一样执行。
现在看看脚本的底部
if __name__ == '__main__':
print(approximate_size(1000000000000, False)) ①
print(approximate_size(1000000000000)) ②
approximate_size()
函数并传递两个参数。在 approximate_size()
函数内部,a_kilobyte_is_1024_bytes 将为 False
,因为你明确地传递了 False
作为第二个参数。approximate_size()
函数,只传递一个参数。但没关系,因为第二个参数是可选的!由于调用方没有指定,第二个参数默认为 True
,如函数声明所定义。你也可以通过名称将值传递给函数。
>>> from humansize import approximate_size >>> approximate_size(4000, a_kilobyte_is_1024_bytes=False) ① '4.0 KB' >>> approximate_size(size=4000, a_kilobyte_is_1024_bytes=False) ② '4.0 KB' >>> approximate_size(a_kilobyte_is_1024_bytes=False, size=4000) ③ '4.0 KB' >>> approximate_size(a_kilobyte_is_1024_bytes=False, 4000) ④ File "<stdin>", line 1 SyntaxError: non-keyword arg after keyword arg >>> approximate_size(size=4000, False) ⑤ File "<stdin>", line 1 SyntaxError: non-keyword arg after keyword arg
approximate_size()
函数,将 4000
用于第一个参数 (size),将 False
用于名为 a_kilobyte_is_1024_bytes 的参数。(这恰好是第二个参数,但没关系,你很快就会明白。)approximate_size()
函数,将 4000
用于名为 size 的参数,将 False
用于名为 a_kilobyte_is_1024_bytes 的参数。(这些命名参数碰巧与函数声明中列出的参数顺序相同,但这也不重要。)approximate_size()
函数,将 False
用于名为 a_kilobyte_is_1024_bytes 的参数,将 4000
用于名为 size 的参数。(看到了吗?我告诉你顺序无关紧要。)4000
传递给了名为 size
的参数,然后“明显地”这个 False
值是为 a_kilobyte_is_1024_bytes 参数准备的。但 Python 不是这样工作的。一旦你有了命名参数,所有该参数右边的参数也需要是命名参数。⁂
我不会用长篇大论来对你喋喋不休关于文档化代码的重要性。只要知道,代码只写一次,但会被阅读很多次,而你代码最重要的受众是你自己,在你写完它六个月后(即在你忘记了一切但需要修复某些东西之后)。Python 使编写可读的代码变得容易,所以要利用它。六个月后你会感谢我的。
你可以通过给 Python 函数一个文档字符串(简称 docstring
)来记录它。在这个程序中,approximate_size()
函数有一个 docstring
def approximate_size(size, a_kilobyte_is_1024_bytes=True):
'''Convert a file size to human-readable form.
Keyword arguments:
size -- file size in bytes
a_kilobyte_is_1024_bytes -- if True (default), use multiples of 1024
if False, use multiples of 1000
Returns: string
'''
三个引号表示多行字符串。起始和结束引号之间的所有内容都是单个字符串的一部分,包括回车符、前导空白和其他引号字符。你可以在任何地方使用它们,但你最常看到它们在定义 docstring
时被使用。
☞三个引号也是定义包含单引号和双引号的字符串的简单方法,就像 Perl 5 中的
qq/.../
一样。
三个引号之间的所有内容都是函数的 docstring
,它记录了函数的功能。docstring
(如果存在)必须是函数中定义的第一件事(即,在函数声明后的下一行)。从技术上讲,你不需要给你的函数一个 docstring
,但你应该始终给它一个。我知道你已经在每一堂编程课上都听到过这句话,但 Python 给了你一个额外的动机:docstring
在运行时作为一个属性存在于函数中。
☞许多 Python IDE 使用
docstring
来提供上下文相关的文档,因此当你键入函数名称时,它的docstring
会显示为工具提示。这可能非常有用,但它只和你编写的docstring
一样好。
⁂
import
搜索路径在继续之前,我想简要地提到一下库搜索路径。当你尝试导入一个模块时,Python 会在几个地方查找它。具体来说,它会查找 sys.path
中定义的所有目录。这只是一个列表,你可以使用标准列表方法轻松地查看或修改它。(你将在 原生数据类型 中了解更多关于列表的信息。)
>>> import sys ① >>> sys.path ② ['', '/usr/lib/python31.zip', '/usr/lib/python3.1', '/usr/lib/python3.1/plat-linux2@EXTRAMACHDEPPATH@', '/usr/lib/python3.1/lib-dynload', '/usr/lib/python3.1/dist-packages', '/usr/local/lib/python3.1/dist-packages'] >>> sys ③ <module 'sys' (built-in)> >>> sys.path.insert(0, '/home/mark/diveintopython3/examples') ④ >>> sys.path ⑤ ['/home/mark/diveintopython3/examples', '', '/usr/lib/python31.zip', '/usr/lib/python3.1', '/usr/lib/python3.1/plat-linux2@EXTRAMACHDEPPATH@', '/usr/lib/python3.1/lib-dynload', '/usr/lib/python3.1/dist-packages', '/usr/local/lib/python3.1/dist-packages']
sys
模块会使它的所有函数和属性可用。sys.path
是一个目录名称列表,它构成了当前搜索路径。(你的路径会不同,取决于你的操作系统、你运行的 Python 版本以及它最初安装的位置。)Python 会遍历这些目录(按此顺序)查找一个名为你试图导入的模块的 .py
文件。.py
文件。有些是内置模块;它们实际上是直接烘焙到 Python 本身中的。内置模块的行为就像普通模块一样,但它们的 Python 源代码不可用,因为它们不是用 Python 编写的!(就像 Python 本身一样,这些内置模块是用 C 编写的。)sys.path
中来将一个新目录添加到 Python 的搜索路径中,然后 Python 会在每次尝试导入模块时查找该目录。该效果持续到 Python 运行结束。sys.path.insert(0, 新路径)
,你将一个新目录作为第一个项目插入了 sys.path
列表中,因此将其放在 Python 搜索路径的开头。这几乎总是你想要的。如果出现命名冲突(例如,如果 Python 附带了某个特定库的版本 2,但你想要使用版本 3),这将确保找到并使用你的模块,而不是 Python 附带的模块。⁂
如果你没注意到,我刚刚说过 Python 函数有属性,这些属性在运行时可用。一个函数,就像 Python 中的所有其他东西一样,是一个对象。
运行交互式 Python shell 并跟着做
>>> import humansize ① >>> print(humansize.approximate_size(4096, True)) ② 4.0 KiB >>> print(humansize.approximate_size.__doc__) ③ Convert a file size to human-readable form. Keyword arguments: size -- file size in bytes a_kilobyte_is_1024_bytes -- if True (default), use multiples of 1024 if False, use multiples of 1000 Returns: string
humansize
的模块。模块是代码块,可以交互使用,也可以在更大的 Python 程序中使用。导入模块后,就可以引用模块中的所有公共函数、类或属性。模块可以使用这种方式访问其他模块的功能,在 Python 交互式解释器中也可以这样做。这是一个重要的概念,你将在本书中看到更多关于它的内容。approximate_size
;而必须写成humansize.approximate_size
。如果你在 Java 中使用过类,这应该让你感觉有点熟悉。__doc__
。☞Python 中的
import
就像 Perl 中的require
。导入 Python 模块后,使用module.function
访问其函数;导入 Perl 模块后,使用module::function
访问其函数。
Python 中的一切都是对象,并且所有对象都可以具有属性和方法。所有函数都有一个内置属性__doc__
,它返回在函数源代码中定义的docstring。sys
模块是一个对象,它包含(除其他外)一个名为path 的属性。以此类推。
然而,这并没有回答更基本的问题:什么是对象?不同的编程语言对“对象”的定义不同。在某些语言中,这意味着所有对象必须具有属性和方法;在其他语言中,这意味着所有对象都是可子类的。在 Python 中,定义更加宽松。一些对象既没有属性也没有方法,但它们可以。并非所有对象都是可子类的。但一切都是对象,从某种意义上说,它可以被赋值给变量或作为参数传递给函数。
你可能在其他编程环境中听说过“一等公民对象”这个词。在 Python 中,函数是一等公民对象。可以将函数作为参数传递给另一个函数。模块是一等公民对象。可以将整个模块作为参数传递给函数。类是一等公民对象,类的单个实例也是一等公民对象。
这一点很重要,所以我要重复一遍,以防你第一次没听懂:Python 中的一切都是对象。字符串是对象。列表是对象。函数是对象。类是对象。类实例是对象。甚至模块也是对象。
⁂
Python 函数没有显式的begin
或end
,也没有大括号来标记函数代码的开始和结束位置。唯一的定界符是冒号 (:
) 和代码本身的缩进。
def approximate_size(size, a_kilobyte_is_1024_bytes=True): ①
if size < 0: ②
raise ValueError('number must be non-negative') ③
④
multiple = 1024 if a_kilobyte_is_1024_bytes else 1000
for suffix in SUFFIXES[multiple]: ⑤
size /= multiple
if size < multiple:
return '{0:.1f} {1}'.format(size, suffix)
raise ValueError('number too large')
if
语句、for
循环、while
循环等等。缩进开始一个块,取消缩进结束一个块。没有显式的大括号、方括号或关键字。这意味着空格是有意义的,并且必须保持一致。在这个例子中,函数代码缩进了四个空格。它不需要缩进四个空格,只需要保持一致即可。第一个未缩进的行标记函数的结束。if
语句后面跟着一个代码块。如果if
表达式计算结果为真,则执行缩进的代码块,否则将执行else
代码块(如果有)。请注意表达式周围没有括号。if
代码块中。此raise
语句将引发异常(类型为ValueError
),但仅当size < 0
时才会引发。for
循环也标记着代码块的开始。代码块可以包含多行,只要它们都缩进相同数量即可。此for
循环包含三行代码。对于多行代码块,没有其他特殊语法。只需缩进并继续即可。在最初的抗议和一些嘲讽的类比后,你会逐渐适应这一点,并开始看到它的好处。一个主要的好处是,所有 Python 程序看起来都相似,因为缩进是语言要求,而不是风格问题。这使得阅读和理解其他人的 Python 代码变得更加容易。
☞Python 使用回车符来分隔语句,并使用冒号和缩进符来分隔代码块。C++ 和 Java 使用分号来分隔语句,并使用大括号来分隔代码块。
⁂
异常在 Python 中无处不在。标准 Python 库中的几乎每个模块都使用异常,Python 本身也会在许多不同的情况下引发异常。你将在本书中反复看到它们。
什么是异常?通常它是一个错误,表示发生了错误。(并非所有异常都是错误,但现在先别管这个。)一些编程语言鼓励使用错误返回码,你可以检查它们。Python 鼓励使用异常,你可以处理它们。
当 Python Shell 中发生错误时,它会打印出有关异常以及异常发生方式的一些详细信息,然后就结束了。这被称为未处理异常。当异常被引发时,没有代码来显式地注意到它并处理它,因此它会一直冒泡到 Python Shell 的顶层,然后 Python Shell 会输出一些调试信息并结束。在 shell 中,这没什么大不了的,但如果这在你的实际 Python 程序运行时发生,那么如果没有任何代码处理异常,整个程序将立即停止运行。也许这就是你想要的,也许不是。
☞与 Java 不同,Python 函数不声明它们可能引发的异常。你需要自己确定需要捕获哪些可能的异常。
但是,异常并不一定会导致程序完全崩溃。异常可以被处理。有时,异常的出现确实是由于你的代码中存在错误(比如访问不存在的变量),但有时异常是你可以预料到的。如果你要打开一个文件,它可能不存在。如果你要导入一个模块,它可能没有安装。如果你要连接到数据库,它可能不可用,或者你可能没有正确的安全凭据来访问它。如果你知道某行代码可能引发异常,你应该使用try...except
代码块处理异常。
☞Python 使用
try...except
代码块来处理异常,并使用raise
语句来生成异常。Java 和C++ 使用try...catch
代码块来处理异常,并使用throw
语句来生成异常。
approximate_size()
函数在两种情况下引发异常:如果给定的size 大于函数设计处理的范围,或者如果它小于零。
if size < 0:
raise ValueError('number must be non-negative')
引发异常的语法很简单。使用raise
语句,后跟异常名,以及一个可选的人类可读字符串用于调试目的。语法类似于调用函数。(实际上,异常是使用类实现的,此raise
语句实际上是在创建ValueError
类的实例,并将字符串'number must be non-negative'
传递给它的初始化方法。但我们现在还言之尚早!)
☞你不需要在引发异常的函数中处理它。如果一个函数没有处理它,异常将被传递给调用函数,然后传递给调用函数的函数,依此类推,一直“向上冒泡”。如果异常始终未被处理,你的程序将崩溃,Python 将将“回溯”信息打印到标准错误,然后就结束了。再说一遍,也许这就是你想要的;这取决于你的程序的功能。
Python 的内置异常之一是ImportError
,当尝试导入模块但失败时会引发该异常。这可能是由于多种原因造成的,但最简单的情况是模块不存在于你的导入搜索路径 中。你可以使用它在程序中包含可选功能。例如,chardet
库 提供字符编码自动检测功能。也许你的程序想要使用此库如果它存在,但如果用户没有安装它,则继续正常执行。你可以使用try..except
代码块来实现这一点。
try:
import chardet
except ImportError:
chardet = None
稍后,你可以使用简单的if
语句检查chardet
模块是否存在
if chardet:
# do something
else:
# continue anyway
ImportError
异常的另一个常见用法是,当两个模块实现一个共同的API 时,但其中一个比另一个更可取。(也许它更快,或者它使用更少的内存。)你可以尝试导入一个模块,但如果第一个导入失败,则回退到另一个模块。例如,XML 章节 介绍了两个实现相同API 的模块,称为ElementTree
API。第一个lxml
是一个第三方模块,你需要自己下载并安装它。第二个xml.etree.ElementTree
速度较慢,但它是 Python 3 标准库的一部分。
try:
from lxml import etree
except ImportError:
import xml.etree.ElementTree as etree
在执行完此try..except
代码块后,你已经导入了一个某个模块,并将其命名为etree。由于这两个模块都实现了相同API,因此你的代码其余部分不需要继续检查导入的是哪个模块。由于导入的模块始终被称为etree,因此你的代码其余部分不需要使用if
语句来调用不同名称的模块。
⁂
再看一下approximate_size()
函数中的这行代码
multiple = 1024 if a_kilobyte_is_1024_bytes else 1000
你从未声明变量multiple,只是将一个值赋给它。这没关系,因为 Python 允许你这样做。Python 不允许你做的是引用一个从未赋值的变量。尝试这样做会引发NameError
异常。
>>> x Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'x' is not defined >>> x = 1 >>> x 1
你总有一天会感谢 Python 的这一功能。
⁂
Python 中的所有名称都区分大小写:变量名、函数名、类名、模块名、异常名。如果你可以获取它、设置它、调用它、构造它、导入它或引发它,那么它就是区分大小写的。
>>> an_integer = 1 >>> an_integer 1 >>> AN_INTEGER Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'AN_INTEGER' is not defined >>> An_Integer Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'An_Integer' is not defined >>> an_inteGer Traceback (most recent call last): File "<stdin>", line 1, in <module> NameError: name 'an_inteGer' is not defined
等等。
⁂
Python 模块是对象,并且具有几个有用的属性。你可以使用它来轻松地测试你编写的模块,方法是在代码中包含一个特殊的代码块,当你在命令行上运行 Python 文件时,此代码块就会执行。请看humansize.py
的最后几行代码
if __name__ == '__main__':
print(approximate_size(1000000000000, False))
print(approximate_size(1000000000000))
☞与C 类似,Python 使用
==
进行比较,使用=
进行赋值。与C 不同,Python 不支持内联赋值,因此不会出现意外地赋值给本来应该进行比较的值的情况。
那么,是什么让这个if
语句变得特殊呢?模块是对象,所有模块都有一个内置属性__name__
。模块的__name__
取决于你使用模块的方式。如果你import
模块,那么__name__
就是模块的文件名,不包含目录路径或文件扩展名。
>>> import humansize >>> humansize.__name__ 'humansize'
但你也可以直接将模块作为独立程序运行,在这种情况下,__name__
将是一个特殊的默认值,__main__
。Python 将评估此if
语句,发现一个真表达式,并执行if
代码块。在本例中,打印两个值。
c:\home\diveintopython3> c:\python31\python.exe humansize.py 1.0 TB 931.3 GiB
这就是你的第一个 Python 程序!
⁂
docstring
,以及什么是优秀的docstring
。© 2001–11 Mark Pilgrim