第 1 章 快速上手:基础知识

第 1 章 快速上手:基础知识

该动手实践了。在本章中,你将学习如何借助计算机能够听懂的语言——Python——来控制它。这里没有什么太难的内容,只要了解计算机的基本工作原理,就能按部就班地完成本章的示例。我将从最简单的内容着手介绍一些基本知识,但鉴于Python功能强大,你很快就能完成一些非常复杂的任务。

首先,需要安装Python或核实已经安装了它。如果你使用的是macOS或Linux/UNIX,请打开终端(在Mac中为应用程序Terminal),输入python并按回车键。你将看到一条欢迎消息,其末尾为如下提示符:

>>>

如果情况确实如此,就可以输入Python命令了,但需要注意的是,你的系统安装的可能是较旧的Python版本。如果第一行消息的开头为Python 2,而不是Python 3,你可能要安装较新的版本,因为Python 3在多个方面发生了翻天覆地的变化。

具体的安装步骤视使用的操作系统和安装方式而异,但最简单的方法是访问www.python.org,其中有下载页面的链接。安装过程非常简单,不管你使用的是Windows、macOS、Linux/UNIX还是其他操作系统,只需单击链接就可访问相应的最新版本。如果你使用的是Windows或Mac,将下载一个安装程序,可通过运行它来安装Python。如果你使用的是Linux/UNIX,将下载到源代码压缩文件,需要按说明进行编译,但通过使用Homebrew、APT等包管理器,可简化安装过程。

安装Python后,尝试启动交互式解释器。要从命令行启动Python,只需执行命令python。如果同时安装了较旧的版本,可能需要执行命令python3。如果你更喜欢使用图形用户界面,可启动Python自带的应用程序IDLE。

1.1 交互式解释器

启动Python后,可看到类似于下面的提示符:

Python 3.5.0 (default, Dec 5 2015, 15:03:35)
[GCC 4.2.1 Compatible Apple LLVM 7.0.0 (clang-700.1.76)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>

解释器的外观及其显示的错误消息因版本而异。虽然看上去没多大意思,但请相信我,这其实很有趣,因为这是进入黑客殿堂的大门——对计算机进行控制的第一步。更准确地说,这是一个交互式Python解释器。请尝试像下面这样做,以核实它是否管用:

>>> print("Hello, world!")

等你按下回车键后,将出现如下输出:

Hello, world!
>>>

如果你熟悉其他计算机语言,可能习惯了在每行末尾都加上分号。在Python中无需这样做,因为在Python中,一行就是一行。如果你愿意,也可加上分号,但不会有任何影响(除非后面还有其他代码),况且大家通常都不这样做。

这是怎么回事呢?>>>是提示符,可在它后面输入一些内容。例如,如果你输入print("Hello, world!")并按回车键,Python解释器将打印字符串"Hello, world!",然后再次显示提示符。

如果输入截然不同的内容呢?请尝试这样做:

>>> The Spanish Inquisition
SyntaxError: invalid syntax
>>>

显然,解释器没有看懂1(如果你运行的不是IDLE,而是Linux命令行解释器,错误消息可能稍有不同)。解释器还指出了问题出在什么地方:使用红色背景色(在命令行解释器中,使用的是脱字符号^)突出单词Spanish。

1毕竟,谁都没想到遇上了西班牙宗教裁判所(Spanish Inquisition)。

如果你喜欢的话,可再尝试几次其他语句(要获取使用指南,可在提示符下输入命令help()并按回车键。在IDLE中,还可按F1来获取帮助信息),否则请接着往下读。毕竟,在不知道如何与之交流的情况下,这个解释器并不是很有趣。

1.2 算法是什么

真刀真枪地编写程序前,先来说说何为计算机编程。简而言之,计算机编程就是告诉计算机如何做。计算机多才多艺,但不太善于独立思考,我们必须提供详尽的细节,使用它们能够明白的语言将算法提供给它们。算法只不过是流程或菜谱的时髦说法,详尽地描述了如何完成某项任务。请看下面的菜谱:

鸡蛋火腿肠:先取一些火腿肠。

再加些火腿肠和鸡蛋。

如果喜欢吃辣,加些辣味火腿肠。

煮熟为止。记得每隔10分钟检查一次。

这个菜谱并不神奇,但其结构很有启发性。它由一系列必须按顺序执行的操作说明组成,其中有些可直接完成(取些火腿肠),有些需要特别注意(如果喜欢吃辣),还有一些需要重复多次(每隔10分钟检查一次)。

菜谱和算法都由原料(对象)和操作说明(语句)组成。在这个示例中,火腿肠和鸡蛋是原料,而操作说明包括添加火腿肠、烹饪指定的时间等。下面首先介绍一些非常简单的Python原料,看看可以对它们做些什么。

1.3 数和表达式

交互式Python解释器可用作功能强大的计算器。请尝试执行如下操作:

>>> 2 + 2

结果应该为4,这不难。下面的运算呢?

>>> 53672 + 235253
288925

还是觉得没什么?不可否认,这是很常见的运算。(下面假设你对如何使用计算器很熟悉,知道1 + 2 * 3(1 + 2) * 3有何不同。)所有常见算术运算符的工作原理都与你预期的一致。除法运算的结果为小数,即浮点数(float或floating-point number)。

>>> 1 / 2
0.5
>>> 1 / 1
1.0

如果你想丢弃小数部分,即执行整除运算,可使用双斜杠。

>>> 1 // 2
0
>>> 1 // 1
1
>>> 5.0 // 2.4
2.0

在较旧的Python版本中,对整数执行常规除法运算的结果与使用双斜杠类似。如果你使用的是Python 2.x,要对整数执行常规除法运算,可在程序开头添加如下语句(稍后介绍如何编写完整的程序),也可直接在交互式解释器中执行这条语句:

>>> from __future__ import division

注意 在上述语句中,future前后分别是两条下划线:__future__

另外,从命令行运行较旧的Python版本时,还可使用命令行开关-Qnew。1.8.2节将更详尽地介绍__future__

至此,你了解了基本的算术运算符(加法、减法、乘法和除法),但还有一种与整除关系紧密的运算没有介绍。

>>> 1 % 2
1

这是求余(求模)运算符。x % y的结果为x除以y的余数。换而言之,结果为执行整除时余下的部分,即x % y等价于x - ((x // y) * y)

>>> 10 // 3
3
>>> 10 % 3
1
>>> 9 // 3
3
>>> 9 % 3
0
>>> 2.75 % 0.5
0.25

在这里,10 // 33,因为结果向下圆整,而3 × 3为9,因此余数为1。将9除以3时,结果正好为3,没有向下圆整,因此余数为0。在需要执行之前菜谱指定的“每10分钟检查一次”之类的操作时,这种运算可能很有用:只需检查minute % 10是否为0。(有关如何执行这种检查,请参阅本章后面的旁注“先睹为快:if语句”。)从最后一个示例可知,求余运算符也可用于浮点数。这种运算符甚至可用于负数,但可能不那么好理解。

>>> 10 % 3
1
>>> 10 % -3
-2
>>> -10 % 3
2
>>> -10 % -3
-1

你也许不能通过这些示例一眼看出求余运算的工作原理,但通过研究与之配套的整除运算可帮助理解。

>>> 10 // 3
3
>>> 10 // -3
-4
>>> -10 // 3
-4
>>> -10 // -3
3

基于除法运算的工作原理,很容易理解最终的余数是多少。对于整除运算,需要明白的一个重点是它向下圆整结果。因此在结果为负数的情况下,圆整后将离0更远。这意味着对于-10 // 3,将向下圆整到-4,而不是向上圆整到-3

这里要介绍的最后一个运算符是乘方(求幂)运算符。

>>> 2 ** 3
8
>>> -3 ** 2
-9
>>> (-3) ** 2
9

请注意,乘方运算符的优先级比求负(单目减)高,因此-3**2等价于-(3**2)。如果你要计算的是(-3)**2,必须明确指出。

十六进制、八进制和二进制

结束本节前需要指出的是,十六进制数、八进制数和二进制数分别以下面的方式表示:

>>> 0xAF
175
>>> 0o10
8
>>> 0b1011010010
722

这些表示法都以0打头。(如果你不明白这些表示法有何意义,说明你使用它们的机会不多,只需将其牢记在心即可。)

1.4 变量

另一个你可能熟悉的概念是变量(variable)。如果代数对你来说不过是遥远的记忆,也不用担心,因为Python中的变量理解起来很容易。变量是表示(或指向)特定值的名称。例如,你可能想使用名称x来表示3,为此执行如下代码:

>>> x = 3

这称为赋值(assignment),我们将值3赋给了变量x。换而言之,就是将变量x与值(或对象)3关联起来。给变量赋值后,就可在表达式中使用它。

>>> x * 2
6

不同于其他一些语言,使用Python变量前必须给它赋值,因为Python变量没有默认值。

注意 在Python中,名称(标识符)只能由字母、数字和下划线(_)构成,且不能以数字打头。因此Plan9是合法的变量名,而9Plan不是2

2在某种程度上说,标识符命名规则基于Unicode标准,详情请参阅“Python语言参考手册”(https://docs.python.org/3/reference/lexical_analysis.html)。

1.5 语句

前面使用的几乎都是表达式,相当于菜谱中的原料,但语句(菜谱中的操作说明)是什么样的呢?

实际上,刚才说的不完全正确,因为前面已经介绍过两种语句了:print语句和赋值语句。语句和表达式有何不同呢?你可以这样想:表达式一些东西,而语句一些事情。例如,2 * 2的结果4,而print(2 * 2)打印4。表达式和语句的行为很像,因此它们之间的界线可能并非那么明确。

>>> 2 * 2
4
>>> print(2 * 2)
4

在交互式解释器中执行时,这两段代码的结果没有任何差别,但这是因为解释器总是将表达式的值打印出来(打印的是repr表示的内容,详情请参阅1.10.3节)。然而,在Python中,情况并非都是这样的。本章后面将介绍如何创建无需交互式解释器就能运行的程序。仅将诸如2 * 2等表达式放在程序中不会有任何作用3,但在程序中包含print(2 * 2)将打印结果4

3这个表达式确实会执行一些操作:计算2和2的乘积。但既不会将结果保存起来,也不会向用户显示它。因此除执行计算外,没有其他任何作用。

注意 print实际上是一个函数(这将在本章后面更详细地介绍),因此前面说的print语句其实是函数调用。在Python 2.x 中,print是一条语句,无需将要打印的内容作为参数放在圆括号内。

涉及赋值时,语句和表达式的差别更明显:鉴于赋值语句不是表达式,它们没有可供交互式解释器打印的值。

>>> x = 3
>>>

执行赋值语句后,交互式解释器只是再次显示提示符,但发生了一些变化:有一个名为x的新变量,与值3相关联。可以说,这是所有语句的一个根本特征:执行修改操作。例如,赋值语句改变变量,而print语句改变屏幕的外观。

无论在什么编程语言中,赋值语句都可能是最重要的语句,虽然这一点你可能难以马上明白。变量就像是临时“存储区”(类似于菜谱中的锅碗瓢盆)4,其真正威力在于无需知道它们存储的值就能操作它们。

4请注意,这里给“存储区”加上了引号。值并非存储在变量中,而是存储在变量指向的计算机内存中。多个变量可指向同一个值。深入阅读后会更清楚地了解这一点。

例如,即便根本不知道xy是什么,你也知道x * y的结果为xy的乘积。因此,编写程序时,你能以各种方式使用变量,而无需知道程序运行时它们将存储(指向)的值。

1.6 获取用户输入

前面说过,编写程序时无需知道变量的值就可使用它们。当然,解释器最终必须知道变量的值,可它怎么知道我们不知道的事情呢?解释器只知道我们已告知它的内容,不是吗?未必如此。

你编写的程序可能供他人使用,无法预测用户会向程序提供什么样的值。我们来看看很有用的函数input(稍后将更详细地介绍函数)。

>>> input("The meaning of life: ")
The meaning of life: 42
'42'

这里在交互式解释器中执行了第一行(input(...)),它打印字符串"The meaning of life:",提示用户输入相应的信息。我输入42并按回车。这个数被input(以文本或字符串的方式)返回,并在最后一行被自动打印出来。通过使用int将字符串转换为整数,可编写一个更有趣的示例:

>>> x = input("x: ")
x: 34
>>> y = input("y: ")
y: 42
>>> print(int(x) * int(y))
1428

对于上述在Python提示符(>>>)下输入的语句,可将其放在完整的程序中,并让用户提供所需的值(3442)。这样,这个程序将打印结果1428,即前述两个数的乘积。在这种情况下,你编写程序时无需知道这些值,对吧?

注意 将程序存储在独立的文件中,让其他用户能够执行时,这种获取输入的方式将有用得多。1.8节将介绍如何这样做。

 

先睹为快:if语句

为增添学习乐趣,这里提前说说原本要到第5章才介绍的内容:if语句。通过使用if语句,可在给定条件满足时执行特定的操作(另一条语句)。一种条件是使用相等运算符(==)表示的相等性检查。没错,相等运算符就是两个等号。(一个等号用于赋值,还记得吗?) 你将条件放在if后面,再加上冒号,将其与后面的语句分开。

>>> if 1 == 2: print('One equals two')
...
>>> if 1 == 1: print('One equals one')
...
One equals one
>>>

条件不满足时什么都不做,但条件满足时,将执行冒号后面的语句(这里是一条print语句)。需要注意的另一点是,在交互式解释器中输入if语句后,需要按两次回车键才能执行它(其中的原因将在第5章介绍)。

因此,如果变量time指向的是以分钟为单位的当前时间,可使用如下语句检查当前是不是整点:

if time % 60 == 0: print('On the hour!')

1.7 函数

1.3节使用了乘方运算符(**)来执行幂运算。实际上,可不使用这个运算符,而使用函数pow

>>> 2 ** 3
8
>>> pow(2, 3)
8

函数犹如小型程序,可用来执行特定的操作。Python提供了很多函数,可用来完成很多神奇的任务。实际上,你也可以自己编写函数(这将在后面更详细地介绍),因此我们通常将pow等标准函数称为内置函数

像前一个示例那样使用函数称为调用函数:你向它提供实参(这里是23),而它返回一个值。鉴于函数调用返回一个值,因此它们也是表达式,就像本章前面讨论的算术表达式一样5。实际上,你可结合使用函数调用和运算符来编写更复杂的表达式(就像前面使用函数int时那样)。

5函数调用也可用作语句,但在这种情况下,将忽略函数的返回值。

>>> 10 + pow(2, 3 * 5) / 3.0
10932.666666666666

有多个内置函数可用于编写数值表达式。例如,abs计算绝对值,round将浮点数圆整为与之最接近的整数。

>>> abs(-10)
10
>>> 2 // 3
0
>>> round(2 / 3)
1.0

请注意最后两个表达式的差别。整除总是向下圆整,而round圆整到最接近的整数,并在两个整数一样近时圆整到偶数。如果要将给定的数向下圆整,该如何做呢?例如,你知道某人的年龄为32.9,并想将这个值向下圆整为32,因为他还没有满33岁。Python提供了完成这种任务的函数floor,但你不能直接使用它,因为像众多很有用的函数一样,它也包含在模块中。

1.8 模块

可将模块视为扩展,通过将其导入可以扩展Python功能。要导入模块,可使用特殊命令import。前一节提及的函数floor包含在模块math中。

>>> import math
>>> math.floor(32.9)
32

请注意其中的工作原理:我们使用import导入模块,再以module.function的方式使用模块中的函数。就这里执行的操作而言,也可像前面处理input的返回值那样,将这个数字转换为整数。

>>> int(32.9)
32

注意 还有一些类似的函数,可用于转换类型,如strfloat。实际上,它们并不是函数,而是类。类将在本书后面更详细地介绍。

模块math还包含其他几个很有用的函数。例如,ceilfloor相反,返回大于或等于给定数的最小整数。

>>> math.ceil(32.3)
33
>>> math.ceil(32)
32

如果确定不会从不同模块导入多个同名函数,你可能不想每次调用函数时都指定模块名。在这种情况下,可使用命令import的如下变种:

>>> from math import sqrt
>>> sqrt(9)
3.0

通过使用命令import的变种from module import function,可在调用函数时不指定模块前缀。

提示 事实上,可使用变量来引用函数(以及其他大部分Python元素)。执行赋值语句foo = math.sqrt后,就可使用foo来计算平方根。例如,foo(4)的结果为2.0

1.8.1 cmath和复数

函数sqrt用于计算平方根。下面来看看向它提供一个负数的情况:

>>> from math import sqrt
>>> sqrt(-1)
Traceback (most recent call last):
...
ValueError: math domain error

在有些平台上,结果如下:

>>> sqrt(-1)
nan

注意 nan具有特殊含义,指的是“非数值”(not a number)。

如果我们坚持将值域限定为实数,并使用其近似的浮点数实现,就无法计算负数的平方根。负数的平方根为虚数,而由实部和虚部组成的数为复数。Python标准库提供了一个专门用于处理复数的模块。

>>> import cmath
>>> cmath.sqrt(-1)
1j

注意到这里没有使用from ... import ...。如果使用了这种import命令,将无法使用常规函数sqrt。类似这样的名称冲突很隐蔽,因此除非必须使用from版的import命令,否则应坚持使用常规版import命令。

1j是个虚数,虚数都以j(或J)结尾。复数算术运算都基于如下定义:-1的平方根为1j。这里不深入探讨这个主题,只举一个例子来结束对复数的讨论:

>>> (1 + 3j) * (9 + 4j)
(-3 + 31j)

从这个示例可知,Python本身提供了对复数的支持。

注意 Python没有专门表示虚数的类型,而将虚数视为实部为零的复数。

1.8.2 回到未来

据说Python之父Guido van Rossum有一台时光机,因为这样的情况出现了多次:大家要求Python提供某项功能时,却发现这项功能早已实现。当然,并非什么人都能进入这台时光机,不过Guido很体贴,通过神奇模块__future__让Python具备了时光机的部分功能。对于Python当前不支持,但未来将成为标准组成部分的功能,你可从这个模块进行导入。这一点你在1.3节已经见识过,本书后面也将经常遇到这个模块。

1.9 保存并执行程序

交互式解释器是Python的亮点之一,它让你能够实时地测试解决方案以及尝试使用Python。要了解隐藏在背后的工作原理,只需尝试使用即可!然而,等你退出交互式解释器时,你在其中编写的所有代码都将丢失。你的终极目标是编写自己和他人都能运行的程序。本节将介绍如何达成这种目标。

首先,你需要一个文本编辑器——最好是专门用于编程的(不推荐使用Microsoft Word之类的软件,但如果你使用的是这样的软件,务必以纯文本的方式保存代码)。如果你使用的是IDLE,那就太幸运了。在这种情况下,只需选择菜单File→New File。这将新建一个编辑器窗口,其中没有交互式提示符。首先,输入如下代码:

print("Hello, world!")

接下来,选择菜单File→Save保存程序(其实就是一个纯文本文件)。务必将文件存储在以后能够找到的地方,并指定合理的文件名,如hello.py(扩展名.py很重要)。

保存好了吗?请不要关闭包含程序的窗口。如果关闭了,选择菜单File→Open重新打开。现在可以运行这个程序了,方法是选择菜单Run→Run Module。(如果你使用的不是IDLE,请参阅下一节,了解如何从命令提示符运行程序。)

结果如何呢?在解释器窗口中打印了Hello, world!,这正是我们想要的结果。根据你使用的版本,解释器提示符可能消失,要让它重新出现,可在解释器窗口中按回车键。

接下来,将脚本扩展成下面这样:

name = input("What is your name? ")
print("Hello, " + name + "!")

如果你运行这个脚本(别忘了先保存),将在解释器窗口中看到如下提示信息:

What is your name?

输入你的名字(如Gumby)并按回车键,你将看到类似于下面的内容:

Hello, Gumby!

强大的海龟绘图法

编写简单示例时,print语句很有用,因为几乎在任何地方都可使用它。如果你要尝试提供更有趣的输出,应考虑使用模块turtle,它实现了海龟绘图法。如果你正在运行IDLE,就可使用这个模块,它让你能够绘制图形(而不是打印文本)。通常,应避免导入模块中所有的名称,但尝试使用海龟绘图法时,这样做可提供极大的方便。

from turtle import *

确定需要使用哪些函数后,可回过头去修改import语句,以便只导入这些函数。

海龟绘图法的理念源自形如海龟的机器人。这种机器人可前进和后退,还可向左和向右旋转一定的角度。另外,这种机器人还携带一只铅笔,可通过抬起或放下来控制铅笔在什么时候接触到脚下的纸张。模块turtle让你能够模拟这样的机器人。例如,下面的代码演示了如何绘制一个三角形:

forward(100)
left(120)
forward(100)
left(120)
forward(100)

如果你运行这些代码,将出现一个新窗口,其中有一个箭头形“海龟”不断地移动,并在身后留下移动轨迹。要将铅笔抬起,可使用penup();要将铅笔重新放下,可使用pendown()。要了解其他的命令,请参阅“Python库参考手册”的相关部分(https://docs.python.org/3/library/turtle.html)。要了解如何绘图,可尝试在网上搜索海龟绘图法(turtle graphic)。学习更多的概念后,你可能想用海龟绘图法替换平淡的print语句。在尝试使用海龟绘图法的过程中,你很快就会发现需要使用后面将介绍的一些基本编程结构。例如,如何在前面的示例中避免反复调用命令forwardleft,如何绘制八角形(而不是三角形)以及如何以尽可能少的代码绘制多个边数各不相同的正多边形。

1.9.1 从命令提示符运行Python脚本

实际上,运行程序的方式有多种。首先,假定你打开了DOS窗口或UNIX shell,并切换到了Python可执行文件(在Windows中为python.exe,在UNIX中为python)或将该可执行文件所在的目录加入到了环境变量PATH中(仅适用于Windows)6。另外,假定前一节的脚本(hello.py)存储在当前目录下。满足上述条件后,就可在Windows中使用如下命令来执行这个脚本:

6如果你看不懂这句话,可以跳过1.9.1节,因为这一节的内容不是非得掌握的。

C:\>python hello.py

在UNIX系统中,可使用如下命令:

$ python hello.py

如你所见,命令是一样的,只是系统提示符不同。

1.9.2 让脚本像普通程序一样

在有些情况下,你希望能够像执行其他程序(如Web浏览器或文本编辑器)一样执行Python脚本,而无需显式地使用Python解释器。UNIX提供了实现这种目标的标准方式:让脚本的第一行以字符序列#!(称为pound bang或shebang)开始,并在它后面指定用于对脚本进行解释的程序(这里是Python)的绝对路径。即便你对这一点不太明白,只需将下面的代码作为脚本的第一行,就可在UNIX中轻松运行脚本:

#!/usr/bin/env python

不管Python库位于什么地方,这都将让你能够像运行普通程序一样运行脚本。如果你安装了多个版本的Python,可用更具体的可执行文件名(如python3)替换python。

要像普通程序一样运行脚本,还必须将其变成可执行的:

$ chmod a+x hello.py

现在,可以像下面这样来运行它(假定当前目录包含在执行路径中):

$ hello.py

如果这不管用,请尝试使用./hello.py,这在当前目录(.)未包含在执行路径中时也管用(负责的系统管理员会告诉你执行路径是什么)。

如果你愿意,可对文件进行重命名并删除扩展名.py,使其看起来更像普通程序。

如果双击会如何呢

在Windows中,扩展名.py是让脚本像普通程序一样的关键所在。请尝试双击前一节保存的文件hello.py。如果正确地安装了Python,这将打开一个DOS窗口,其中包含提示信息What is your name?7。然而,这样运行程序存在一个问题:输入名字后,程序窗口将立即关闭,你根本来不及看清结果。这是因为程序结束后窗口将立即关闭。尝试修改脚本,在末尾添加如下代码行:

7是否会这样取决于你使用的操作系统以及安装的Python解释器。例如,在macOS中,如果文件是使用IDLE存储的,双击文件将只会在IDLE代码编辑器中打开它。

input("Press <enter>")

现在运行这个程序并输入名字后,DOS窗口将包含如下内容:

What is your name? Gumby
Hello, Gumby!
Press <enter>

等你按回车键后,窗口将立即关闭,因为程序结束了。

1.9.3 注释

在Python中,井号(#)比较特殊:在代码中,井号后面到行尾的所有内容都将被忽略。(这也是Python解释器未被前面的/usr/bin/env卡住的原因所在。)下面是一个示例:

# 打印圆的周长:
print(2 * pi * radius)

第一行为注释。注释让程序更容易理解:对其他人来说如此,在程序编写者回过头来阅读代码时亦如此。据说程序员应遵守的首要戒律是“汝应注释”,但是一些不那么宽容的程序员的座右铭是“如果写起来难,理解起来必然也难”。注释务必言而有物,不要重复去讲通过代码很容易获得的信息。无用而重复的注释还不如没有。例如,下述代码中的注释根本就是多余:

# 获取用户的名字:
user_name = input("What is your name?")

在任何情况下,都应确保代码即便没有注释也易于理解。所幸Python是一种卓越的语言,能让人很容易编写出易于理解的程序。

1.10 字符串

前一节的代码"Hello, " + name + "!"是什么意思呢?本章的第一个程序只包含如下代码:

print("Hello, world!")

编程教程通常以类似的程序开篇,问题是我还未全面阐述其工作原理。你已掌握了print语句的基本知识(后面将更详细地介绍它),但"Hello, world!"是什么呢?这是一个字符串(string)。几乎所有真实的Python程序中都有字符串的身影。字符串用途众多,但主要用途是表示一段文本,如感叹句“Hello, world!”。

1.10.1 单引号字符串以及对引号转义

与数一样,字符串也是值:

>>> "Hello, world!"
'Hello, world!'

在这个示例中,有一点可能让你颇感意外:Python在打印字符串时,用单引号将其括起,而我们使用的是双引号。这有什么差别吗?其实没有任何差别。

>>> 'Hello, world!'
'Hello, world!'

这里使用的是单引号,结果却完全相同。既然如此,为何同时支持单引号和双引号呢?因为在有些情况下,这可能会有用。

>>> "Let's go!"
"Let's go!"
>>> '"Hello, world!" she said'
'"Hello, world!" she said'

在上述代码中,第一个字符串包含一个单引号(就这里而言,可能称之为撇号更合适),因此不能用单引号将整个字符串括起,否则解释器将报错(做出这样的反应是正确的)。

>>> 'Let's go!'
SyntaxError: invalid syntax

在这里,字符串为'Let',因此Python不知道如何处理后面的s(更准确地说是当前行余下的内容)。

第二个字符串包含双引号,因此必须使用单引号将整个字符串括起,原因和前面一样。实际上,并非必须这样做(这样做只是出于方便考虑)。可使用反斜杠(\)对引号进行转义,如下所示:

>>> 'Let\'s go!'
"Let's go!"

这样Python将明白中间的引号是字符串的一部分,而不是字符串结束的标志。虽然如此,Python打印这个字符串时,还是使用了双引号将其括起。与你预期的一样,对于双引号可采用同样的处理手法。

>>> "\"Hello, world!\" she said"
'"Hello, world!" she said'

像这样对引号进行转义很有用,且在有些情况下必须这样做。例如,在字符串同时包含单引号和双引号(如'Let\'s say "Hello, world!"')时,如果不使用反斜杠进行转义,该如何办呢?

注意 厌烦了反斜杠?你在本章后面将看到,在大多数情况下,可通过使用长字符串和原始字符串(可结合使用这两种字符串)来避免使用反斜杠。

1.10.2 拼接字符串

为处理前述不太正常的示例,来看另一种表示这个字符串的方式:

>>> "Let's say " '"Hello, world!"'
'Let\'s say "Hello, world!"'

我依次输入了两个字符串,而Python自动将它们拼接起来了(合并为一个字符串)。这种机制用得不多,但有时候很有用。然而,仅当你同时依次输入两个字符串时,这种机制才管用。

>>> x = "Hello, "
>>> y = "world!"
>>> x y
SyntaxError: invalid syntax

换而言之,这是一种输入字符串的特殊方式,而非通用的字符串拼接方法。那么应该如何拼接字符串呢?就像将数相加一样,将它们相加:

>>> "Hello, " + "world!"
'Hello, world!'
>>> x = "Hello, "
>>> y = "world!"
>>> x + y
'Hello, world!'

1.10.3 字符串表示strrepr

Python打印所有的字符串时,都用引号将其括起。你可能通过前面的示例发现了这一点。这是因为Python打印值时,保留其在代码中的样子,而不是你希望用户看到的样子。但如果你使用print,结果将不同。

>>> "Hello, world!"
'Hello, world!'
>>> print("Hello, world!")
Hello, world!

如果再加上表示换行符的编码\n,差别将更明显。

>>> "Hello,\nworld!"
'Hello,\nworld!'
>>> print("Hello,\nworld!")
Hello,
world!

通过两种不同的机制将值转换成了字符串。你可通过使用函数strrepr8直接使用这两种机制。使用str能以合理的方式将值转换为用户能够看懂的字符串。例如,尽可能将特殊字符编码转换为相应的字符。然而,使用repr时,通常会获得值的合法Python表达式表示。

8实际上,像int一样,str也是一个类,但repr是一个函数。

>>> print(repr("Hello,\nworld!"))
'Hello,\nworld!'
>>> print(str("Hello,\nworld!"))
Hello,
world!

1.10.4 长字符串、原始字符串和字节

有一些独特而有用的字符串表示方式。例如,有一种独特的语法可用于表示包含换行符或反斜杠的字符串(长字符串原始字符串)。对于包含特殊符号的字符串,Python 2还提供了一种专用的表示语法,结果为Unicode字符串。这种语法现在依然管用,但是多余,因为在Python 3中,所有的字符串都是Unicode字符串。Python 3还引入了一种新语法,用于表示大致相当于老式字符串的字节对象。你将看到,在处理Unicode编码方面,这种对象依然扮演着重要的角色。

  1. 长字符串

    要表示很长的字符串(跨越多行的字符串),可使用三引号(而不是普通引号)。

    print('''This is a very long string. It continues here.
    And it's not over yet. "Hello, world!"
    Still here.''')
    
    

    还可使用三个双引号,如"""like this"""。请注意,这让解释器能够识别表示字符串开始和结束位置的引号,因此字符串本身可包含单引号和双引号,无需使用反斜杠进行转义。

    提示 常规字符串也可横跨多行。只要在行尾加上反斜杠,反斜杠和换行符将被转义,即被忽略。例如,如果编写如下代码:

    print("Hello, \ 
    

    world!")

    它将打印Hello, world!。这种处理手法也适用于表达式和语句。

    >>> 1 + 2 + \
        4 + 5
    12
    >>> print \
        ('Hello, world')
    Hello, world
    
  2. 原始字符串

    原始字符串不以特殊方式处理反斜杠,因此在有些情况下很有用9。在常规字符串中,反斜杠扮演着特殊角色:它对字符进行转义,让你能够在字符串中包含原本无法包含的字符。例如,你已经看到可使用\n表示换行符,从而像下面这样在字符串中包含换行符:

    >>> print('Hello,\nworld!')
    Hello,
    world!
    
    

    这通常挺好,但在有些情况下,并非你想要的结果。如果你要在字符串中包含\n呢?例如,你可能要在字符串中包含DOS路径C:\nowhere。

    >>> path = 'C:\nowhere'
    >>> path
    'C:\nowhere'
    
    

    这好像没问题,但如果将其打印出来,就会出现问题。

    >>> print(path)
    C:
    owhere
    
    

    这并非你想要的结果,不是吗?那该怎么办呢?可对反斜杠本身进行转义。

    >>> print('C:\\nowhere')
    C:\nowhere
    
    

    这很好,但对于很长的路径,将需要使用大量的反斜杠。

    path = 'C:\\Program Files\\fnord\\foo\\bar\\baz\\frozz\\bozz'
    
    

    在这样的情况下,原始字符串可派上用场,因为它们根本不会对反斜杠做特殊处理,而是让字符串包含的每个字符都保持原样。

    >>> print(r'C:\nowhere')
    C:\nowhere
    >>> print(r'C:\Program Files\fnord\foo\bar\baz\frozz\bozz')
    C:\Program Files\fnord\foo\bar\baz\frozz\bozz
    
    

    如你所见,原始字符串用前缀r表示。看起来可在原始字符串中包含任何字符,这大致是正确的。一个例外是,引号需要像通常那样进行转义,但这意味着用于执行转义的反斜杠也将包含在最终的字符串中。

    >>> print(r'Let\'s go!')
    Let\'s go!
    
    

    另外,原始字符串不能以单个反斜杠结尾。换而言之,原始字符串的最后一个字符不能是反斜杠,除非你对其进行转义(但进行转义时,用于转义的反斜杠也将是字符串的一部分)。根据前一个示例,这一点应该是显而易见的。如果最后一个字符(位于结束引号前面的那个字符)为反斜杠,且未对其进行转义,Python将无法判断字符串是否到此结束。

    >>> print(r"This is illegal\")
    SyntaxError: EOL while scanning string literal
    
    

    这合乎情理,但如果要指定以反斜杠结尾的原始字符串(如以反斜杠结尾的DOS路径),该如何办呢?本节介绍了大量技巧,应该能够帮助你解决这个问题,但基本技巧是将反斜杠单独作为一个字符串,下面是一个简单的示例:

    >>> print(r'C:\Program Files\foo\bar' '\\')
    C:\Program Files\foo\bar\
    
    

    请注意,指定原始字符串时,可使用单引号或双引号将其括起,还可使用三引号将其括起。

  3. Unicode、bytesbytearray

    Python字符串使用Unicode编码来表示文本。对大多数简单程序来说,这一点是完全透明的,因此如果你愿意,可跳过本节,等需要时再学习这个主题。然而,鉴于处理字符串和文本文件的Python代码很多,大致浏览一下本节至少不会有什么坏处。

    大致而言,每个Unicode字符都用一个码点(code point)表示,而码点是Unicode标准给每个字符指定的数字。这让你能够以任何现代软件都能识别的方式表示129个文字系统中的12万个以上的字符。当然,鉴于计算机键盘不可能包含几十万个键,因此有一种指定Unicode字符的通用机制:使用16或32位的十六进制字面量(分别加上前缀\u\U)或者使用字符的Unicode名称(\N{name})。

    >>> "\u00C6"
    'Æ'
    >>> "\U0001F60A"
    '☺'
    >>> "This is a cat: \N{Cat}"
    'This is a cat: '
    

    要获悉字符的Unicode码点和名称,可在网上使用有关该字符的描述进行搜索,也可参阅特定的网站,如http://unicode-table.com

    Unicode的理念很简单,却带来了一些挑战,其中之一是编码问题。在内存和磁盘中,所有对象都是以二进制数字(0和1)表示的(这些数字每8个为一组,即1字节),字符串也不例外。在诸如C等编程语言中,这些字节完全暴露,而字符串不过是字节序列而已。为与C语言互操作以及将文本写入文件或通过网络套接字发送出去,Python提供了两种类似的类型:不可变的bytes和可变的bytearray。如果需要,可直接创建bytes对象(而不是字符串),方法是使用前缀b

    >>> b'Hello, world!'
    b'Hello, world!'
    
    

    然而,1字节只能表示256个不同的值,离Unicode标准的要求差很远。Python bytes字面量只支持ASCII标准中的128个字符,而余下的128个值必须用转义序列表示,如\xf0表示十六进制值0xf0(即240)。

    唯一的差别好像在于可用的字母表规模,但实际上并非完全如此。乍一看,好像ASCII和Unicode定义的都是非负整数和字符之间的映射,但存在细微的差别:Unicode码点是使用整数定义的,而ASCII字符是使用对应的数及其二进制编码定义的。这一点好像无关紧要,原因之一是整数0~255和8位二进制数之间的映射是固定的,几乎没有任何机动空间。问题是超过1字节后,情况就不那么简单了:直接将每个码点表示为相应的二进制数可能不再可行。这是因为不仅存在字节顺序的问题(即便对整数值进行编码,也会遇到这样的问题),而且还可能浪费空间:如果对于每个码点都使用相同数量的字节进行编码,就必须考虑到文本可能包含安那托利亚象形文字或皇家亚兰字母。有一种Unicode编码标准是基于这种考虑的,它就是UTF-32(32位统一编码转换格式,Unicode Transformation Format 32 bits),但如果你主要处理的是使用互联网上常见语言书写的文本,那么使用这种编码标准将很浪费空间。

    然而,有一种非常巧妙的替代方式:不使用全部32位,而是使用变长编码,即对于不同的字符,使用不同数量的字节进行编码。这种编码方式主要出自计算机先锋Kenneth Thompson之手。通过使用这种编码,可节省占用的空间,就像摩尔斯码使用较少的点和短线表示常见的字母,从而减少工作量一样10。具体地说,进行单字节编码时,依然使用ASCII编码,以便与较旧的系统兼容;但对于不在这个范围内的字符,使用多个字节(最多为6个)进行编码。下面来使用ASCII、UTF-8和UTF-32编码将字符串转换为bytes

    >>> "Hello, world!".encode("ASCII")
    b'Hello, world!'
    >>> "Hello, world!".encode("UTF-8")
    b'Hello, world!'
    >>> "Hello, world!".encode("UTF-32")
    b'\xff\xfe\x00\x00H\x00\x00\x00e\x00\x00\x00l\x00\x00\x00l\x00\x00\x00o\x00\x00\x00,\x00\
    x00\x00 \x00\x00\x00w\x00\x00\x00o\x00\x00\x00r\x00\x00\x00l\x00\x00\x00d\x00\x00\x00!\x00\
    x00\x00'
    
    

    从中可知,使用前两种编码的结果相同,但使用最后一种编码的结果长得多。再来看一个示例:

    >>> len("How long is this?".encode("UTF-8"))
    17
    >>> len("How long is this?".encode("UTF-32"))
    72
    
    

    只要字符串包含较怪异的字符,ASCII和UTF-8之间的差别便显现出来了:

    >>> "Hællå, wørld!".encode("ASCII")
    Traceback (most recent call last):
      ...
    UnicodeEncodeError: 'ascii' codec can't encode character '\xe6' in position 1: ordinal not
    in range(128)
    
    

    斯堪的纳维亚字母没有对应的ASCII编码。如果必须使用ASCII编码(这样的情况肯定会遇到),可向encode提供另一个实参,告诉它如何处理错误。这个参数默认为strict,但可将其指定为其他值,以忽略或替换不在ASCII表中的字符。

    >>> "Hællå, wørld!".encode("ASCII", "ignore")
    b'Hll, wrld!'
    >>> "Hællå, wørld!".encode("ASCII", "replace")
    b'H?ll?, w?rld!'
    >>> "Hællå, wørld!".encode("ASCII", "backslashreplace")
    b'H\\xe6ll\\xe5, w\\xf8rld!'
    >>> "Hællå, wørld!".encode("ASCII", "xmlcharrefreplace")
    b'H&#230;ll&#229;, w&#248;rld!'
    
    

    几乎在所有情况下,都最好使用UTF-8。事实上,它也是默认使用的编码。

    >>> "Hællå, wørld!".encode()
    b'H\xc3\xa6ll\xc3\xa5, w\xc3\xb8rld!'
    
    

    这相比于Hello, world!,编码结果要长些;但使用UTF-32编码时,结果一样长。

    可将字符串编码为bytes,同样也可将bytes解码为字符串。

    >>> b'H\xc3\xa6ll\xc3\xa5, w\xc3\xb8rld!'.decode()
    'Hællå, wørld!'
    
    

    与前面一样,默认编码也是UTF-8。你可指定其他编码,但如果指定的编码不正确,将出现错误消息或得到一堆乱码。bytes对象本身并不知道使用的是哪种编码,因此你必须负责跟踪这一点。

    可不使用方法encodedecode,而直接创建bytesstr(即字符串)对象,如下所示:

    >>> bytes("Hællå, wørld!", encoding="utf-8")
    b'H\xc3\xa6ll\xc3\xa5, w\xc3\xb8rld!'
    >>> str(b'H\xc3\xa6ll\xc3\xa5, w\xc3\xb8rld!', encoding="utf-8")
    'Hællå, wørld!'
    
    

    这种方法更通用一些,在你不知道类似于字符串或bytes的对象属于哪个类时,使用这种方法也更管用。一个通用规则是,不要做过于严格的假设。

    编码和解码的最重要用途之一是,将文本存储到磁盘文件中。然而,Python提供的文件读写机制通常会替你完成这方面的工作!只要文件使用的是UTF-8编码,就无需操心编码和解码的问题。但如果原本正常的文本变成了乱码,就说明文件使用的可能是其他编码。在这种情况下,对导致这种问题的原因有所了解将大有裨益。如果你想更详细地了解Python中的Unicode,请参阅在线文档中有关该主题的HOWTO部分11

    注意 源代码也将被编码,且默认使用的也是UTF-8编码。如果你想使用其他编码(例如,如果你使用的文本编辑器使用其他编码来存储源代码),可使用特殊的注释来指定。

    # -*- coding: encoding name -*-
    
    

    请将其中的encoding name替换为你要使用的编码(大小写都行),如utf-8latin-1

    最后,Python还提供了bytearray,它是bytes的可变版。从某种意义上说,它就像是可修改的字符串——常规字符串是不能修改的。然而,bytearray其实是为在幕后使用而设计的,因此作为类字符串使用时对用户并不友好。例如,要替换其中的字符,必须将其指定为0~255的值。因此,要插入字符,必须使用ord获取其序数值(ordinal value)。

    >>> x = bytearray(b"Hello!")
    >>> x[1] = ord(b"u")
    >>> x
    bytearray(b'Hullo!')
    
    

9编写正则表达式时,原始字符串很有用,这将在第10章详细介绍。

10这是一种重要的压缩方法,为多个现代压缩工具使用的霍夫曼编码所采用。

11请参见https://docs.python.org/3/howto/unicode.html

1.11 小结

本章介绍的内容很多,先来看看你都学到了什么,再接着往下讲。

  • 算法:算法犹如菜谱,告诉你如何完成特定的任务。从本质上说,编写计算机程序就是使用计算机能够理解的语言(如Python)描述一种算法。这种对机器友好的描述被称为程序,主要由表达式和语句组成。
  • 表达式:表达式为程序的一部分,结果为一个值。例如,2 + 2就是一个表达式,结果为4。简单表达式是使用运算符(如+%)和函数(如pow)将字面值(如2"Hello")组合起来得到的。通过组合简单的表达式,可创建复杂的表达式,如(2 + 2) *(3 - 1)。表达式还可能包含变量
  • 变量:变量是表示值的名称。通过赋值,可将新值赋给变量,如x = 2。赋值是一种语句
  • 语句:语句是让计算机执行特定操作的指示。这种操作可能是修改变量(通过赋值)、将信息打印到屏幕上(如print("Hello, world!"))、导入模块或执行众多其他任务。
  • 函数:Python函数类似于数学函数,它们可能接受参数,并返回结果(在第6章学习编写自定义函数时,你将发现函数实际上可以在返回前做很多事情)。
  • 模块:模块是扩展,可通过导入它们来扩展Python的功能。例如,模块math包含多个很有用的函数。
  • 程序:你通过练习学习了如何编写、保存和运行Python程序。
  • 字符串:字符串非常简单。它们其实就是一段文本,其中的字符是用Unicode码点表示的。然而,对于字符串,需要学习的知识有很多。本章介绍了很多表示字符串的方式,第3章将介绍众多字符串用法。

1.11.1 本章介绍的新函数

函数

描述

abs(number)

返回指定数的绝对值

bytes(string, encoding[, errors])

对指定的字符串进行编码,并以指定的方式处理错误

cmath.sqrt(number)

返回平方根;可用于负数

float(object)

将字符串或数字转换为浮点数

help([object])

提供交互式帮助

input(prompt)

以字符串的方式获取用户输入

int(object)

将字符串或数转换为整数

math.ceil(number)

以浮点数的方式返回向上圆整的结果

math.floor(number)

以浮点数的方式返回向下圆整的结果

math.sqrt(number)

返回平方根;不能用于负数

pow(x, y[, z])

返回xy次方对z求模的结果

print(object, ...)

将提供的实参打印出来,并用空格分隔

repr(object)

返回指定值的字符串表示

round(number[, ndigits])

四舍五入为指定的精度,正好为5时舍入到偶数

str(object)

将指定的值转换为字符串。用于转换bytes时,可指定编码和错误处理方式

在上表中,方括号内的参数是可选的。

1.11.2 预告

介绍表达式的基本知识后,接下来将介绍更复杂的内容:数据结构。你将学习如何将简单值(如数)组合成更复杂的结构,如列表和字典,而不是分别处理它们。另外,你还将更深入学习字符串。在第5章,你将更深入地学习语句,为编写巧妙的程序做好准备。

目录

  • 版权声明
  • 前言
  • 引言
  • 第 1 章 快速上手:基础知识
  • 第 2 章 列表和元组
  • 第 3 章 使用字符串
  • 第 4 章 当索引行不通时
  • 第 5 章 条件、循环及其他语句
  • 第 6 章 抽象
  • 第 7 章 再谈抽象
  • 第 8 章 异常
  • 第 9 章 魔法方法、特性和迭代器
  • 第 10 章 开箱即用
  • 第 11 章 文件
  • 第 12 章 图形用户界面
  • 第 13 章 数据库支持
  • 第 14 章 网络编程
  • 第 15 章 Python和Web
  • 第 16 章 测试基础
  • 第 17 章 扩展Python
  • 第 18 章 程序打包
  • 第 19 章 趣味编程
  • 第 20 章 项目1:自动添加标签
  • 第 21 章 项目2:绘制图表
  • 第 22 章 项目3:万能的XML
  • 第 23 章 项目4:新闻汇总
  • 第 24 章 项目5:虚拟茶话会
  • 第 25 章 项目6:使用CGI进行远程编辑
  • 第 26 章 项目7:自建公告板
  • 第 27 章 项目8:使用XML-RPC共享文件
  • 第 28 章 项目9:使用GUI共享文件
  • 第 29 章 项目10:自制街机游戏
  • 附录 A 简明教程
  • 附录 B Python参考手册