您当前位置:首页 深入 Python 3

难度级别:♦♢♢♢♢

你的第一个 Python 程序

不要把你的负担埋藏在神圣的沉默中。你有问题吗?太好了。欢欣鼓舞,跳进去,去调查吧。
—— 尊者 Henepola Gunaratana

 

深入

惯例要求我应该用编程的基本构建块来让你感到厌烦,这样我们就可以慢慢地构建一些有用的东西。让我们跳过这一切。这是一个完整的、可工作的 Python 程序。它可能对你来说毫无意义。别担心,因为你将逐行剖析它。但先通读一遍,看看你能从中理解什么,如果有什么的话。

[下载 humansize.py]

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() 函数接收两个参数——sizea_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))         
  1. 这调用 approximate_size() 函数并传递两个参数。在 approximate_size() 函数内部,a_kilobyte_is_1024_bytes 将为 False,因为你明确地传递了 False 作为第二个参数。
  2. 这调用 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
  1. 这调用 approximate_size() 函数,将 4000 用于第一个参数 (size),将 False 用于名为 a_kilobyte_is_1024_bytes 的参数。(这恰好是第二个参数,但没关系,你很快就会明白。)
  2. 这调用 approximate_size() 函数,将 4000 用于名为 size 的参数,将 False 用于名为 a_kilobyte_is_1024_bytes 的参数。(这些命名参数碰巧与函数声明中列出的参数顺序相同,但这也不重要。)
  3. 这调用 approximate_size() 函数,将 False 用于名为 a_kilobyte_is_1024_bytes 的参数,将 4000 用于名为 size 的参数。(看到了吗?我告诉你顺序无关紧要。)
  4. 这次调用失败,因为你有一个命名参数,后面跟着一个未命名的(位置)参数,而这永远不会生效。从左到右读取参数列表,一旦你有一个命名参数,其余的参数也必须是命名参数。
  5. 这次调用也失败了,原因与上一次调用相同。这令人惊讶吗?毕竟,你将 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']
  1. 导入 sys 模块会使它的所有函数和属性可用。
  2. sys.path 是一个目录名称列表,它构成了当前搜索路径。(你的路径会不同,取决于你的操作系统、你运行的 Python 版本以及它最初安装的位置。)Python 会遍历这些目录(按此顺序)查找一个名为你试图导入的模块的 .py 文件。
  3. 实际上,我撒谎了;真相比这更复杂,因为并非所有模块都存储为 .py 文件。有些是内置模块;它们实际上是直接烘焙到 Python 本身中的。内置模块的行为就像普通模块一样,但它们的 Python 源代码不可用,因为它们不是用 Python 编写的!(就像 Python 本身一样,这些内置模块是用 C 编写的。)
  4. 你可以在运行时通过将目录名称添加到 sys.path 中来将一个新目录添加到 Python 的搜索路径中,然后 Python 会在每次尝试导入模块时查找该目录。该效果持续到 Python 运行结束。
  5. 通过使用 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

  1. 第一行代码导入名为humansize的模块。模块是代码块,可以交互使用,也可以在更大的 Python 程序中使用。导入模块后,就可以引用模块中的所有公共函数、类或属性。模块可以使用这种方式访问其他模块的功能,在 Python 交互式解释器中也可以这样做。这是一个重要的概念,你将在本书中看到更多关于它的内容。
  2. 当你想使用导入模块中定义的函数时,需要包含模块名。因此,你不能只说approximate_size;而必须写成humansize.approximate_size。如果你在 Java 中使用过类,这应该让你感觉有点熟悉。
  3. 你没有像预期的那样调用函数,而是询问了函数的某个属性,__doc__

Python 中的import 就像 Perl 中的require。导入 Python 模块后,使用module.function 访问其函数;导入 Perl 模块后,使用module::function 访问其函数。

什么是对象?

Python 中的一切都是对象,并且所有对象都可以具有属性和方法。所有函数都有一个内置属性__doc__,它返回在函数源代码中定义的docstringsys 模块是一个对象,它包含(除其他外)一个名为path 的属性。以此类推。

然而,这并没有回答更基本的问题:什么是对象?不同的编程语言对“对象”的定义不同。在某些语言中,这意味着所有对象必须具有属性和方法;在其他语言中,这意味着所有对象都是可子类的。在 Python 中,定义更加宽松。一些对象既没有属性也没有方法,但它们可以。并非所有对象都是可子类的。但一切都是对象,从某种意义上说,它可以被赋值给变量或作为参数传递给函数。

你可能在其他编程环境中听说过“一等公民对象”这个词。在 Python 中,函数是一等公民对象。可以将函数作为参数传递给另一个函数。模块是一等公民对象。可以将整个模块作为参数传递给函数。类是一等公民对象,类的单个实例也是一等公民对象。

这一点很重要,所以我要重复一遍,以防你第一次没听懂:Python 中的一切都是对象。字符串是对象。列表是对象。函数是对象。类是对象。类实例是对象。甚至模块也是对象。

代码缩进

Python 函数没有显式的beginend,也没有大括号来标记函数代码的开始和结束位置。唯一的定界符是冒号 (:) 和代码本身的缩进。

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')
  1. 代码块由其缩进定义。所谓“代码块”,指的是函数、if 语句、for 循环、while 循环等等。缩进开始一个块,取消缩进结束一个块。没有显式的大括号、方括号或关键字。这意味着空格是有意义的,并且必须保持一致。在这个例子中,函数代码缩进了四个空格。它不需要缩进四个空格,只需要保持一致即可。第一个未缩进的行标记函数的结束。
  2. 在 Python 中,if 语句后面跟着一个代码块。如果if 表达式计算结果为真,则执行缩进的代码块,否则将执行else 代码块(如果有)。请注意表达式周围没有括号。
  3. 此行位于if 代码块中。此raise 语句将引发异常(类型为ValueError),但仅当size < 0 时才会引发。
  4. 不是函数的结尾。完全空白行不算。它们可以使代码更具可读性,但它们不算作代码块定界符。函数在下一行继续。
  5. 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 程序!

进一步阅读

© 2001–11 Mark Pilgrim