《Dive Into Python 3》翻译 #第一章

第一章 – 你的第一个 Python 程序


❝ Don’t bury your burden in saintly silence. You have a problem? Great. Rejoice, dive in, and investigate. ❞
— Ven. Henepola Gunaratana

1.1. 深入

按照惯例我应该从一些基础的编程语句块开始讲,然后我们逐步构建一些有用的东西,但是这会让你觉得很无聊。咱们就把那些都跳过,这有个完整的能用的 Python 程序。你可能完全看不懂。不慌,我会带你一行行的分析。但是首先你得通读一遍,理解你能看懂的任何东西。

现在让我们在命令行中运行一下这个程序。在 Windows 平台上,它的结果看起来类似这样:

在 Mac OS X 或者 Linux 平台上,结果看起来类似这样:

刚刚发生了啥?你执行了你的第一个 Python 程序。你在命令行里调用了 Python 解释器,然后你传递了你想让 Python 去执行的脚本名。这个脚本定义了一个函数,这个 approximate_size() 函数,他将会计算出一个确切的以 byte 为单位的文件大小,然后计算成易读的(但是是近似值)的大小。(你可能已经在 Windows 资源管理器,或者 Mac OS X 访达,或者 Linux 的 Nautilus,Dolphin,Thunar 上看到过了。如果你以多列列表的形式显示一个包含文档的文件夹,它将会显示一个包括文档图标,文档名,大小,类型,最后修改时间等等的表。如果文件夹包含一个 1093 字节的叫 TODO 的文件,你的文件管理器不会显示 TODO 1093 bytes,而是类似 TODO 1KB。这就是 approximate_size() 所做的事。)

看脚本的结尾,你会看到两次调用 print(approximate_size(arguments))。这些函数调用是这样子的:第一次调用 approximate_size() 函数,同时传递了一些参数,然后把返回值直接传递给了 print() 函数。print() 函数是内置的,你不会看到对它的显式声明。无论何时无论何地你都可以使用它。(还有很多内置的函数,更多的函数被分在了不同的模块(module)里。耐心点,慢慢来。)

所以为什么在命令行里运行的脚本每次都给你一样的输出结果呢?我们就快说到这了。首先,让我们先看一眼 approximate_size() 这个函数。

1.2. 声明函数

Python 的函数和其他大多数语言都差不多,但是他不像 C++ 那样被隔离在头文件里或者 Pascal 的 interface/implementation 部分。当你需要一个函数时声明它就行了,就像这样:

这个关键词 def 标志着函数声明的开始,紧跟着是函数名,然后是在圆括号中的参数表。多个参数用逗号隔开。

还要注意这个函数没有定义一个返回类型。Python 函数不用为它们的返回值特定一个类型;它们甚至不用具体说明是否有返回值。(事实上,每个 Python 函数都会返回一个值,如果函数在任何时候执行了一个 return 语句,它将会返回那个值,否则它将会返回 None,即 Python 中的 null。)

 

在一些语言中,函数(带返回值的)以 function 开头,而子程序 (subroutines,没有返回值) 以 sub 开头。在 Python 中没有子程序这个概念。所有东西都是一个函数,所有的函数都有返回值(即使返回 None),而且所有函数都以 def 开头。

 

这个 approximate_size() 函数有两个参数: size 和 a_kilobyte_is_1024_bytes,但是没有一个参数具体说明了它的数据类型。在 Python 中,变量永远不被显式地指定类型。Python 会弄清楚某个变量是什么类型并且在内部跟踪它。

 

在 Java 和其他静态类型语言中,你必须具体说明函数返回值和每个参数的数据类型。而在 Python 中,你永远不用显式定义任何东西的数据类型。Python 会给予你的赋值而在内部跟踪数据类型。

 

1.2.1. 可选参数和具名参数

Python 允许带有默认值的函数参数,如果函数被调用的时候没有传递参数,那么这个参数会使用它的默认值。而且通过使用具名参数可以用任何顺序指定参数。

让我们再看一眼函数 approximate_size() 的声明:

第二个参数 a_kilobyte_is_1024_bytes,指定了 True 为默认值。这意味着这个参数是可选的。你可以在调用函数的时候不指定这个参数,Python 将会视为你把 True 作为第二个参数调用了这个函数。

现在看一眼脚本的结尾:

  1. 这一句在调用 approximate_size() 时传递了两个参数。由于你明确地把 False作为第二个参数传递了,所以在 approximate_size() 函数中,a_kilobyte_is_1024_bytes 会是 False。
  2. 这一句在调用 approximate_size() 时只传递了一个参数。不过这也是可以的,因为第二个参数是可选的!由于调用者没有具体说明,所以第二个参数为函数声明中的默认值,即 True。

你也可以通过参数名传递参数。

  1. 这次调用 approximate_size() 时给第一个参数 size 传了 4000,给具名参数 a_kilobyte_is_1024_bytes 传了false。(那碰巧是第二个参数,但是也没关系,你马上就会懂了。)
  2. 这次调用 approximate_size() 时给具名参数 size 传了 4000,给具名参数 a_kilobyte_is_1024_bytes 传了 false。(这些具名参数刚好和函数声明中的参数列表是一样的顺序,但是那同样不要紧。)
  3. 这次调用 approximate_size() 时给 a_kilobytes_is_1024_bytes 传了 false,给 size 传了 4000。(看到了吗?我告诉过你了顺序不重要)
  4. 这次调用失败了,因为你在一个具名参数后面跟了个未命名(按位)参数,所以那不会有效。从左到右读这个参数列表,一旦你用了一次具名参数,剩下的参数都必须要带名字。
  5. 这个也失败了,和前一次是一样的原因。惊讶吗?毕竟,你给 size 传了 4000,很明显那个 false 就是传给 a_kilobyte_is_1024_bytes 的。但是 Python 不认识这种写法。你用一个具名参数,右边的参数就都得是具名参数。

1.3. 编写易读的代码

我不会给你来一段长的让人直摇手指的演讲来告诉你给你的代码写文档的重要性,这太无聊了。你只需要知道代码虽然只写一次但是在写完之后的六个月内会经常被读,而且你的代码最重要的作者就是你自己(比如你需要修补某些地方,但是你全忘完了)。Python 把编写易读的代码变得很容易,所以就好好利用它的优势咯。不到六个月你肯定会来感谢我的。

1.3.1. 文档字符串

你可以用文档字符串(documentation string  缩写是 docstring)来给一个 Python 函数写文档。在这个程序中,函数 approximate_size() 有一个文档字符串。

三引号表示一个多行字符串。在开始和结尾的引号中间的所有东西都是这一个字符串的一部分,包括回车换行符,前导空格和其它的引号字符。你可以在任何地方使用他们,但是你最常见的用法是定义一个文档字符串。

 

三引号也是一个定义包含单引号和双引号字符串的捷径,就像 Perl 5 中的 qq/…/。

 

在三引号之间的所有东西都是这个函数的记录函数的功能的文档字符串。如果存在文档字符串,那它必须是函数里定义的第一个东西(就在在函数声明的下一行)。你不用严格地给你的函数写文档字符串,但是你必须得写。你可能你上过的其他语言的编程班上也听过这个,但是 Python 还给你了一个额外的好处:文档字符串像函数的属性一样在运行时也可以获取到。

 

许多 Python IDE 都用 文档字符串来提供上下文关联的文档,这样一来当你输入一个函数名的时候,它的文档字符串就会像工具提示那样出现了。这个功能极其有用,但是它取决于你的文档字符串写得有多好。

 

1.4. import 的搜索路径

在进行下一步讲解之前,我想简单提一下库的搜索路径。当你试图导入一个模块时,Python 会在好几个地方查找。具体来说,它会在 sys.path 中定义的目录查找。它就是个列表,你很容易用标准的列表方法查看或者修改它(你会在 Native Datatypes 中了解到更多关于列表的信息)。

  1. 导入sys 模块能让它的全部函数和属性变成可获取状态。
  2. path 是一个构成当前搜索路径的目录名的列表。(根据你的操作系统,正在运行的 Python 版本和它的初始安装位置,你会看到不同的内容。)Python 会遍历这些目录以找到和你试图导入的名字匹配的 .py 文件。
  3. 实际上,我撒谎了。真相比刚才更复杂,因为不是所有的模块都以 .py 文件存储。一些内置模块实际上被整合到 Python 里边了。内置模块和常规模块用起来一样,但是不能获取到它们的 Python 源代码,因为它们都不是用 Python 写的!(像 Python 本身一样,这些内置函数都是用 C 写的。)
  4. 你可以在运行时通过在path 中增加一个目录的方式在 Python 的搜索路径中增加一个新的目录,然后不管什么时候你试图导入一个模块, Python 也会在那个目录中查找。只要 Python 在运行就一直有效。
  5. 通过使用 path.insert(0, new_path),你可以把一个新目录插入到 sys.path 的第一个位置,也就是 Python 搜索路径的开头。这就是你总想干的。在命名冲突的时候(比如,Python 倒入了一个特定库的第二版,但是你想用第三版的),这样做可以确保你指定的模块可以被找到和使用,而不是 Python 给的。

1.5. 所有东西都是对象

以防你没看到,我再说一遍,我之前说过 Python 函数都有属性,它们所有的属性在运行时都是可获取的。一个函数,像 Python 中的其他东西一样,都是一个对象。

打开交互式 Python Shell 然后跟着做:

  1. 在第一行把 humansize 作为模块导入 — 如此一来我们就能交互式地使用一大坨代码或者一个更大的Python 程序。一旦你导入了一个模块,你就能获得它里面任何一个公有函数,类或者属性的引用。模块也可以访问其他模块的功能,而且在交互式 Python Shell 中你也能做到。这是一个重要的概念,你以后会经常在这书里看到它。
  2. 当你想使用定义在已导入模块内部的函数时,你需要带上模块名。所以你不能只写 approximate_size;它必须得是 human size.approximate_size。如果你用过 Java 中的类,这应该感觉有点相似。
  3. 除了按照你想要的方式调用函数,你还可以调用这个函数的属性之一,__doc__。

 

Python 中的 import 就像 Perl 中的 require。一旦你导入了一个 Python 模块,你就可以用  module.fuction 这样的形式访问它的函数。一旦你导入了一个 Perl 模块,你就可以用 module::function 这样的形式访问它的函数。

 

1.5.1. 对象是什么?

Python 中的所有东西都是一个对象,而且所有东西都可以有属性和方法。所有的函数都有一个叫 __doc__ 的属性,它可以返回定义在函数源代码里的文档字符串。sys 是个对象,它带有(除了其他东西之外)一个叫做 path 的属性。其它的也是类似。

这仍然回答不了最根本的问题:对象是什么?不同的编程语言用不同的方法定义了对象。有些指的是所有的对象必须有属性和方法,其它的指对象必须是可被继承的。在 Python 中的定义更宽松一点。一些对象既不用油属性也不用有方法,但是它们可以有。不是所有对象都是可被继承的。但是所有东西都是对象,也就是说它可以被分配给一个变量或者作为参数传给函数。

你可能在其他编程上下文中听说过一个术语,叫“头等对象”。在 Python 中,函数是头等对象,你可以把一个函数当作参数传给其他函数。模块也是头等对象。你能把整个模块当作参数传给函数,类是头等对象,类的个别实例也是头等对象。

这一点很重要,以防你一会就忘了,所以我再强调一遍:在 Python 中的所有东西都是对象。字符串是对象,列表时对象,函数是对象,类是对象,类的实例也是对象,甚至模块也是对象。

1.6. 代码缩进

Python 函数没有明确的开始或者结束,而且没有大括号标志函数的代码在哪里开始或者停止。唯一的分隔符就是冒号(:)和代码自己的缩进。

  1. 代码块通过他们的缩进被定义。关于代码块,我指的是函数,if 语句,for 循环, while 循环,等等。缩进是代码块的开始,不缩进是它的结束。没有明确的大括号,圆括号或者关键词。这意味着空格很重要,而且必须得一致。在这个例子中,函数块缩进了四个空格,它不需要必须是四个空格,只需要一致就好。第一行没有缩进的代码标记着函数的结束。
  2. 在 Python 中,一个 if 语句后边跟着一个代码块。如果 if 表达式运算结果是 true ,就会执行 if 后面的缩进代码块,否则会跳到 else 代码块(如果有的话),注意表达式周围没有括号。
  3. 这一行是在 if 代码块里边的。raise 语句会抛出一个异常( ValueError 异常 ),但是只在 size < 0 成立的时候才抛出。
  4. 这不是函数的结束。空行不被考虑在内。它们只是让代码更易读,但是它们不算是代码块的分隔符。函数会在下一行继续运行。
  5. for 循环也标记着一个代码块的开始。代码块能容纳很多行,只要它们的缩进量都一样。这个 for 循环里面有三行代码。对于多行代码块也没有其他的特殊语法。只需要缩进就行了,继续享受生活吧。

在一些最初的抱怨和一些对于类似 Fortran 的嘲讽之后,你会平和地对待它,开始看到它的好处。一个主要的好处就是由于缩进是一个语言要求而不是代码风格问题,所有的 Python 程序看起来都很相似。这让阅读和理解其他人的 Python 代码变得更容易了。

 

Python 用回车符分隔语句,用冒号和缩进分隔代码块。C++ 和 Java 用分号分隔语句,用大括号分隔代码块。

 

1.7. 异常

在 Python 里边到处都有异常。实际上在 Python 标准库中的每个模块都用异常,而且在很多不同的情况下,Python 自身也会抛出异常。你会在书中反复地看到它们。

什么是异常?通常来说它是一个错误,一个表明有些东西错了的标志。(不是所有异常都是错误,但是现在先别担心那些。)一些编程语言鼓励使用可检查的错误返回代码。Python 鼓励使用可处理的异常。

当在 Python Shell 中发生一个错误的时候,它会打印输出一些关于这个异常和它是如何发生的详细信息,仅此而已。这叫做未处理的异常。当这个异常被抛出,没有代码会注意到这个异常然后处理它,因此它会输出它的调用路径和一些调试信息,然后就万事大吉了。在这个 shell 中,那不是什么大问题,但是如果它发生在你正在运行的实际的 Python 程序中,如果没有东西来处理这个异常的,整个程序就会嘎的一声停住。可能那就是你想要的,可能也不是。

 

不像 Java ,Python 函数不声明它们会抛出什么异常。判断哪些异常需要捕获完全取决于你。

 

尽管一个异常不一定会造成整个程序崩溃,但异常应该被处理。有时一个异常是因为你的代码里有 bug(像访问一个不存在的变量),但是有时一个异常是因为一些你能预料到的事。比如你要打开一个可能不存在的文件。比如你要导入一个可能没有安装的模块。比如你要链接一个可能不可达的或者没有访问权限的数据库。如果你知道哪行代码可能会抛出异常,你应该用 try…except 语句块处理异常。

 

Python 用try…except 语句块去处理异常,用 raise 语句来抛出异常。

Java 和 C++ 使用 try…catch 语句块去处理异常,用throw 语句来抛出异常。

 

这个 approximate_size() 函数在两种不同的情况下抛出异常:如果赋给 size 的值大于函数能处理的大小,或者小于零。

这个抛出异常的语法太简单了。用跟着一个异常的名字和一个可有可无的给人看的字符串用来调试。这个语法让人联想到调用函数。(事实上,异常和类一样被执行,这个 raise 语句实际上创建了一个 ValuerError 类的实例并传递字符串 ‘number must be non-negative’ 到它的初始化方法中。但是我们已经有些超前了!)

 

你不需要在抛出异常的函数中处理这个异常。如果一个函数没处理它,这个异常会被传递到它的调用函数,然后函数的调用函数,调用函数的调用函数等等这些栈上面的。如果异常都没被处理,你的程序就会崩溃,Python 会打印一个 “traceback” 的标准错误信息,然后就完事了。再说一遍,可能这就是你想要的,这取决于你的程序做了什么。

 

1.7.1. 捕获导入错误

ImportError 是 Python 内建的异常之一,当你想导入一个模块但是失败了的时候就会抛出这个异常。这可能是由于多种原因引起的,但是最简单的原因就是在你的导入搜索路径中并不存在这个模块。你可以在你的程序中把这个作为你的已包含的可选特性来使用。例如,这个 chardet 库提供了字符编码的自动检测。或许你的程序想在这个库存在的情况下使用它,但是如果用户没安装它的话程序也得优雅地继续。你得用 try…except 语句块来导入。

随后,你可以用简单的 if 语句检查chardet 模块的存在状态。

另一个 ImportError 的普遍用法是当两个模块可以实现相同的 API,但是一个比另一个更好。(可能它更快,或者占用内存少)你可以在尝试调用第一个模块失败的时候调用另一个。例如,在 XML 章节中讨论了两个实现同一个叫做 ElementTree 的 API 的模块。第一个叫做 lxml,这是个第三方模块,它需要你自己下载安装。第二个叫 xml.etree.ElementTree,它更慢一点但是是 Python 3 标准库的一部分。

在 try…except 语句块的结尾,你导入了一些模块并把它命名为 etree。因为两个模块实现了相同的 API,在你其余的代码中不需要再一直检查导入了哪个模块了。因为这个一定会被导入的模块总是叫做 etree,其余的代码就不会因为用 if 语句调用不同名字的模块而被打乱。

1.8. Unbound 变量

看一眼 approximate_size() 函数中的这行代码:

你从来没声明过这个 multiple 变量,你就能直接给它赋值了。这样是可行的,因为 Python 允许你这样做。Python 不允许你做的是引用一个变量但是从来没给它赋值。这么做会抛出一个 NameError 异常。

会有一天你要为这个感谢 Python。

1.9. 所有的东西都区分大小写

在 Python 中的所有名字都区分大小写:变量名,函数名,类名,模块名,异常名,如果你能获取它,设置它,调用它,构造它,导入它,或者抛出它,它就是区分大小写的。

诸如此类。

1.10. 运行脚本

Python 模块都是对象,而且还有几个有用的属性。你可以使用这个来轻松地测试你写的模块,通过包含仅在命令行运行时才会执行的一个特殊的代码块。来看看 humansize.py 的最后几行代码。

 

和 C 一样的是,Python 用 == 来比较,用 = 来赋值。和 C 不一样的是,Python 不允许内嵌的赋值,所以不会出现你以为是在比较,实际上赋值了的情况。

 

所以什么让这个 if 语句变得特殊了呢?好吧,模块都是对象,而且所有的模块都有一个叫做 __name__ 的内置属性。一个模块的 __name__ 取决于你怎么调用的这个模块。如果你导入了这个模块,那么这个 __name__ 就是这个模块的名字,不带目录路径或者文件扩展名。

但是你也可以把这个模块作为一个独立的程序来直接运行,这时候 __name__ 会是一个特殊的默认值,__main__。Python 将会评判这个 if 语句,寻找一个为真的表达式,然后执行 if 代码块。在这个例子中,会打印两个值。

这就是你的第一个 Python 程序!

1.11. 深入阅读

PEP 257: Docstring Conventions 解释了怎么来从一堆 docstring 中挑出好的 docstring。

Python Tutorial: Documentation Strings 也提到了这个主题。

PEP 8: Style Guide for Python Code 讨论了好的缩进风格。

Python Reference Manual 解释了 Python 中的所有东西都是对象 意味着什么,因为有些人就是喜欢详细讨论一些东西的书呆子

发布者

[email protected]

信息安全本科在读,喜欢做 Android 开发,正在自学 python。

2 thoughts on “《Dive Into Python 3》翻译 #第一章”

发表评论

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据