《Python 3程序开发指南(第2版•修订版)》——1.2 Python的关键要素

简介:

本节书摘来自异步社区《Python 3程序开发指南(第2版•修订版)》一书中的第1章,第1.2节,作者[英]Mark Summerfield,王弘博,孙传庆 译,更多章节内容可以访问云栖社区“异步社区”公众号查看。

1.2 Python的关键要素

在本节中,我们将学习Python的8个关键要素,下一节中,我们将展示如何借助这些要素编写实际的小程序。关于本节中讲述的各关键要素,都有更多的内容需要阐述,因此,阅读本节的内容时,有时候你会觉得Python似乎遗失了一些内容,使得很多工作只能以一种冗繁的模式完成,如果使用前向索引或索引表格中的内容,那么你几乎总是可以发现Python具备你需要的特性,并且可以更紧凑的表达方式来完成当前展示的工作方式——还有很多其他内容。

1.2.1 要素#1:数据类型

任何程序语言都必须能完成的基本任务是表示数据项。Python提供了几种内置的数据类型,现在我们只关注其中的两种。Python使用int类型表示整数(正整数或负整数),使用str类型表示字符串(Unicode字符序列)。下面给出几个整数类型与字符串类型变量实例:

-973
210624583337114373395836055367340864637790190801098222508621955072
0
"Infinitely Demanding"
'Simon Critchley'
'positively αβγ€÷©'
''

顺便说一下,上面第二个整数代表的是2217。Python所能表示的整数大小只受限于机器内存,而非固定数量的字节数。字符串可以使用双引号或单引号封装——只要字符串头尾使用的符号是对称的。由于Python使用的是Unicode编码,因此字符串中的符号不局限于ASCII字符,比如上面倒数第二个字符串。空字符串则只是使用引号,中间没有任何内容。

Python使用方括号([])来存取字符串等序列中的某一项,比如,在Python Shell中(交互式的解释器,或IDLE),有如下的输入和输出信息——Python Shell的输出是以粗体展示的,输入则是以细体展示的:

>>> "Hard Times"[5]
'T'
>>> "giraffe"[0]
'g'

传统上,Python Shell使用>>>作为其提示符,当然这并非一成不变。方括号存取这种语法适用于任意数据类型(只要构成一个序列)的数据项,比如字符串或列表。这种语法的一致性是Python之所以如此美丽的原因之一。需要注意的是,Python语法中,索引位置是从0开始计数的。

在Python中,str类型与基本的数值类型(比如int)都是固定的——也就是说,一旦设定,其值就不能改变。乍一看,这似乎是一个相当奇怪的限制,但是Python语法的特点使这一限制在实践中并不会成为问题。之所以提及这一点,唯一的原因是想说明:虽然可以使用方括号取回字符串中某给定索引位置处的字符,但是不能将其设置为新字符(注意,在Python中,字符就是指长度为1的字符串)。

如果需要将一个数据项从某种类型转换为另一种类型,那么可以使用语法datatype (item),例如:

>>> int("45")
45
>>> str(912)
'912'

int()转换可以允许头尾处带有空格,因此,int(" 45 ")也是正确的。str()转换几乎可以应用于所有数据项。在第6章中可以看到,我们可以轻易地使自定义的数据类型支持str()转换,也可以支持int()转换或其他转换——只要这种转换是有意义的。如果转换失败,就会给出异常信息——我们将在要素5中简要介绍异常处理,在第4章中对其进行全面介绍。

字符串与整数这两种数据类型和其他一些内置的数据类型与某些来自Python标准库的数据类型一起在第2章中进行全面的讲解。第2章中还介绍了可用于固定序列(比如字符串)的操作。

1.2.2 要素#2:对象引用

定义了数据类型之后,接下来要做的事情就是定义存储某种类型数据的变量,但Python没有这样的变量,而是使用“对象引用”。对固定对象(比如intS与strS)而言,变量与对象引用之间没有可察觉的差别。对于可变对象,则存在差别,但是在实际工作中很少有影响。在后面的讲解中,我们将交替地使用术语“变量”与“对象引用”。

下面看几个tiny实例,并对其中的某些细节进行讨论。

x = "blue"
y = "green"
z = x

在上面的几条语句中,语法都是简单的objectReference = value。这里不需要预先的声明语句,也没有必要指定值的类型。执行上面的第一条语句时,Python会创建一个str对象,其文本内容为“blue”,同时还创建了一个名为x的对象引用,x引用的就是这个str对象。出于实用性的目的,我们可以说“变量x已经被分配了‘blue’这一字符串”;第二条语句是类似的;第三条语句创建了一个名为z的新对象引用,并将其设置为对象引用x所指向的相同对象(这里是包含文本“blue”的str对象)。

在其他一些语言中,操作符“=”与变量分配操作符是不一致的。在Python中,“=”的作用是将对象引用与内存中的某对象进行绑定。如果对象引用已经存在,就简单地进行重绑定,以便引用“=”操作符右面的对象;如果对象引用尚未存在,就由“=”操作符创建对象引用。

让我们继续对上面的x、y、z实例进行讲解,并进行一些重绑定操作——如前面所讲述的,以“#”引导的是注释语句,并作用到该行最后。

print(x, y, z) # prints: blue green blue
z = y
print(x, y, z) # prints: blue green green
x = z
print(x, y, z) # prints: green green green

在第4条语句(x=z)执行之后,所有3个对象引用实际上引用的都是同一个str。由于不存在更多的对字符串“blue”的对象引用,因此Python可以对其进行垃圾收集处理。

图1-2通过图形化方式展示了对象与对象引用之间的关系。


screenshot

用于对象引用的名称(称为“标识符”)有一些限制,尤其是不能与任何Python关键字相同,并且必须以字母或下划线引导,其后跟随0个或多个非空格字符、下划线或数字。标识符没有长度限制,字母与数字指的是Unicode编码中所定义的,也就是说,包含但不仅仅限于ASCII编码定义的字母与数字(“a”、“b”、……、“z”、“A”、“B”、……、“Z”、“0”、“1”、……、“9”)。此外,Python标识符是大小写敏感的,因此,LIMIT、Limit与limit是3个不同的标识符。第2章中会给出进一步的详细信息与一些稍微有些特别的实例。

Python使用“动态类型”机制,也就是说,在任何时刻,只要需要,某个对象引用都可以重新引用一个不同的对象(可以是不同的数据类型)。使用强类型的语言(比如C++、Java),只允许执行与某种特定数据类型绑定的操作,Python也适用于这一约束,但在Python中,由于使用的是动态类型机制,因此有效的操作是可以改变的,比如某个对象引用可以重新绑定到不同数据类型的对象。例如:

route = 866
print(route, type(route)) # prints: 866 <class 'int'>
route = "North"
print(route, type(route)) # prints: North <class 'str'>

这里我们创建了一个称为route的对象引用,并将其设置为引用一个新的int型数值866。对于route,我们可以使用“/”,因为除法对整数而言是一个有效的操作符。之后,我们重用route这一对象引用,并用其引用一个新的str变量,值为“North”,int对象将进入垃圾收集流程,这是因为没有对象引用再对其进行引用。如果此时再使用“/”,就会导致产生一个TypeError,因为对字符串而言,“/”并不是一个有效的操作符。

type()函数会返回给定数据项的数据类型(也称为“类”)——在测试与调试时,这一功能是非常有用的,但是在实际的应用代码中并不常见,因为存在一种更好的替代方案,我们将会在第6章介绍。

如果在交互式解释器环境下(或者在类似于IDLE环境提供的Python Shell环境下)执行Python代码,只需要简单地输入对象引用名,就足以打印出其值,例如:

>>> x = "blue"
>>> y = "green"
>>> z = x
>>> x
'blue'
>>> x, y, z
('blue', 'green', 'blue')

这种方式比调用print()函数打印值信息要方便得多,但是只有在交互式地使用Python时才有效——我们所编写的任意程序与模块都必须使用print()或类似函数才能产生输出信息。在上面的最后一个实例中,输出信息包含在圆括号中,数据项之间以逗号分隔,这意味着输出的是元组,也就是有序的固定对象序列。下一个要素将对元组进行进一步讲述。

1.2.3 要素#3:组合数据类型

通常,将整个的数据项组合在一起会带来不少方便。Python提供了几种组合数据类型,包括关联数组与集合等类型,这里我们只讨论其中的两种:元组与列表。Python元组与列表可用于存储任意数量、任意类型的数据项。元组是固定的,创建之后就不能改变;列表是可变的,在需要的时候,可以插入或移除数据项。

元组使用逗号创建,例如:

>>> "Denmark","Finland", "Norway", "Sweden"
('Denmark', 'Finland', 'Norway', 'Sweden')
>>> "one",
('one',)

在输出元组时,Python使用圆括号将其封装在一起。很多程序员模仿这种机制,总是将自己定义的元组常值包括在圆括号中。如果某个元组只有一个数据项,又需要使用圆括号,就仍然必须使用逗号,比如(1,)。空元组则使用空的()创建。逗号还可以用于在函数调用时分隔参数,因此,如果需要将元组常值作为参数传递,就必须使用括号对其进行封装,以免混淆。

下面给出了一些元组实例:

[1, 4, 9, 16, 25, 36, 49]
['alpha', 'bravo', 'charlie', 'delta', 'echo']
['zebra', 49, -879, 'aardvark', 200]
[]

列表可以使用方括号([])创建,就像上面这些实例,第四个实例展示的是一个空列表。后面我们会看到,还有其他一些创建列表的方法。

实质上,列表与元组并不真正存储数据项,而是存放对象引用。创建列表与元组时(以及在列表中插入数据项时),实际上是使用其给定的对象引用的副本。在字面意义项(比如整数或字符串)的情况下,会在内存中创建适当数据类型的对象,而存放在列表或元组中的才是对象引用。

与Python中的其他内容一样,组合数据类型也是对象,因此,可以将某种组合数据类型嵌套在其他组合数据类型中,比如,创建一个列表,其中的每个元素也是列表,不拘形式。列表、元组以及大多数Python的其他组合数据类型存储的是对象引用,而非对象本身——有些情况下,这一事实会有一定的影响,第3章将会涉及这一问题。

在过程型程序设计中,我们经常需要调用函数,并将某些数据项作为参数传递。比如,前面我们已经看到的print()函数。另一个常用的Python函数是len()函数,该函数以某个单独的数据项作为参数,并返回该数据项的“长度”(int类型)。下面有几个调用len()函数的实例——考虑到读者应该可以区分哪些是自己的输入,哪些是解释器的输出,这里没有再使用粗体表示解释器的输出。

>>> len(("one",))
1
>>> len([3, 5, 1, 2, "pause", 5])
6
>>> len("automatically")
13

元组、列表以及字符串等数据类型是“有大小的”,也就是说,对这些数据类型而言,长度或大小等度量是有意义的,将这些数据类型的数据项作为参数传递给len()函数是有意义的。(如果是那种不能用长度进行度量的数据项传递给len()函数,则会导致异常)。

所有Python数据项都是某种特定数据类型(也称之为“类”)的“对象”(也称之为“实例”),我们将交替地使用术语“数据类型”与“类”。对象与有些其他语言(比如,C++或Java的内置数值类型)提供的数据项的关键区别在于,对象可以有“方法”。实质上,简单地说,方法就是某特定对象可以调用的函数。比如,数据类型list有一个append()方法,借助于该方法,可以以如下方式添加对象:

>>> x = ["zebra", 49, -879, "aardvark", 200]
>>> x.append("more")
>>> x
['zebra', 49, -879, 'aardvark', 200, 'more']

对象x知道自身是一个list(所有Python对象都知道自身的数据类型),因此,不需要明确地指定数据类型。在append()方法的实现中,第一个参数是x对象本身——这是由Python自动完成的(作为其对方法的句法支持的一部分)。

append()方法会改变原始的列表。这是可以实现的,因为列表这种数据类型本身就是可变的。与创建新列表(使用原始的数据项以及额外要添加的数据项)、之后重新绑定对新列表的对象引用相比,append()方法具有潜在的更高的效率,对于很长的列表尤其如此。

在过程型程序设计语言中,以如下的方式(完全有效的Python语法)使用列表的append()方法可以完成同样的功能:

>>> list.append(x, "extra")
>>> x
['zebra', 49, -879, 'aardvark', 200, 'more', 'extra']

这里,我们指定了数据类型以及该数据类型的方法,并将要调用该方法的数据项本身作为第一个参数,其后跟随其他一些参数。(在涉及到继承时,两种语法存在微妙的语义差别,第一种形式在实际中应用的更加广泛。第6章将会对继承进行讲解。)

如果你对面向对象程序设计并不熟悉,最初看到这些内容可能会有些奇怪。不过,你现在只需要知道,Python有一种常规的函数调用方式functionName(arguments)、方法调用方式objectName.methodName(arguments)(第6章将会对面向对象程序设计进行讲解)。

点(“存取属性”)操作符用于存取对象的某个属性。虽然到目前为止,我们展示的只有方法属性,但是属性可以是任意类型的对象。由于属性可以是对象,该对象包含某些属性,这些属性又可以包含其他属性,依此类推,因此,可以根据需要使用多级嵌套的点操作符来存取特定的属性。

list类型有很多其他方法,包括insert()方法,该方法用于在某给定的索引位置插入数据项;remove()方法,该方法用于移除某给定索引位置上的数据项。如前面所说的,Python索引总是以0开始。

前面曾经提及,我们可以使用方括号操作符从字符串中获得某个字符,并且该操作符可用于任意序列。列表本身也是一种序列,因此,可以对其进行如下一些操作:

>>> x
['zebra', 49, -879, 'aardvark', 200, 'more', 'extra']
>>> x[0]
'zebra'
>>> x[4]
200

元组也是一种序列,因此,如果x是一个元组,我们也可以以完全同样的方式使用方括号取回项目,就像对x列表一样。但是,由于列表是可变的(不像字符串与元组是固定的),因此我们也可以使用方括号操作符来设置列表元素,例如:

>>> x[1] = "forty nine"
>>> x
['zebra', 'forty nine', -879, 'aardvark', 200, 'more', 'extra']

如果我们给定了一个超出范围的索引位置,就会产生例外——我们将在要素5中简要介绍意外处理,并在第4章中对其进行全面讲解。

我们几次使用了“序列”这个术语,依赖于对其含义的非形式化的理解,目前也将继续这样处理。不过,Python精确地定义了序列必须支持的特性,也定义了有大小的对象必须支持的特性,以及某种数据类型可能属于的其他不同类别,这些内容将在第8章讲述。

列表、元组以及Python其他内置的组合数据类型将在第3章中讲述。

1.2.4 元素#4:逻辑操作符

任何程序设计语言的一个基本功能都是其逻辑运算。Python提供了4组逻辑运算,我们将在这里对其全部进行讲述。

1.2.4.1 身份操作符

由于所有的Python变量实际上都是对象引用,有时,询问两个或更多的对象引用是否都指向相同的对象是有意义的。is操作符是一个二元操作符,如果其左端的对象引用与右端的对象引用指向的是同一个对象,则会返回true。下面给出几个实例:

>>> a = ["Retention", 3, None]
>>> b = ["Retention", 3, None]
>>> a is b
False
>>> b = a
>>> a is b
True

需要注意的是,通常,对intS、strS以及很多其他数据类型进行比较是没有意义的,因为我们几乎总是想比较这些值。实际上,使用is对数据项进行比较可能会导致直觉外的结果。例如,在前面的实例中,虽然a与b在最初设置为同样的列表值,但是列表本身是以单独的list对象存储的,因此,在第一次使用时,is操作符将返回false。

身份比较的一个好处是速度非常快,这是因为,并不必须对进行比较的对象本身进行检查,is操作符只需要对对象所在的内存地址进行比较——同样的地址存储的是同样的对象。

最常见的使用is的情况是将数据项与内置的空对象None进行比较,None通常用作位置标记值,指示“未知”或“不存在”:

>>> a = "Something"
>>> b = None
>>> a is not None, b is None
(True, True)

上面使用is not是对身份测试的反向测试。

身份操作符的作用是查看两个对象引用是否指向相同的对象,或查看某个对象是否为None。如果我们需要比较对象值,就应该使用比较操作符。

1.2.4.2 比较操作符

Python提供了二进制比较操作符的标准集合,每个操作符带有期待中的语义:<表示小于,<=表示小于或等于,==表示等于。!=表示不等于,>=表示大于或等于,>表示大于。这些操作符对对象值进行比较,也就是对象引用在比较中指向的具体对象。下面是在Python Shell中输入的一些实例:

>>> a = 2
>>> b = 6
>>> a == b
False
>>> a < b
True
>>> a <= b, a != b, a >= b, a > b
(True, True, False, False)

对于整数,比较的结果与我们期待的结果是一样的。同样,对字符串进行比较操作,也可以获得正确的结果:

>>> a = "many paths"
>>> b = "many paths"
>>> a is b
False
>>> a == b
True

从上面的实例可以看出,虽然a与b是不同的对象(有不同的身份),但是具有相同的值,因此比较的结果是相等的。需要注意的是,因为Python使用Unicode编码表示字符串,与简单的ASCII字符串比较相比,对包含非ASCII字符的字符串进行比较可能更微妙、更复杂——我们将在第2章对这些问题进行全面讨论。

Python比较操作符的一个特别好用的特性是可以进行结链比较,例如:

>>> a = 9
>>> 0 <= a <= 10
True

对给定数据项取值在某个范围内的情况,这种测试方式提供了很多便利,不再需要使用逻辑运算符and进行两次单独的比较操作(大多数其他语言都需要如此)。这种方式的另一个附加的好处是只需要对数据项进行一次评估(因为数据项在表达式中只出现一次),如果数据项值的计算需要耗费大量时间或存取数据项会带来一些副作用,这种优势就会更加明显。

归功于Python动态类型机制的“强大”,进行无意义的比较会导致异常,例如:

>>> "three" < 4
Traceback (most recent call last):
...
TypeError: unorderable types: str() < int()

出现异常而又未得到处理时,Python会输出该异常错误消息的回溯与追踪信息。为使得输出更加清晰,我们忽略了输出信息中的回溯部分,而使用省略号替代1。如果我们输入的是“3”< 4,就会导致同样的异常,因为Python并不会猜测我们的意图——正确的方法或者是进行明确的转换,例如使用可比较的类型,也就是说,都是整数或都是字符串。

Python中可以容易地创建自定义数据类型,并且与已有数据类型进行很好的整合,比如,我们可以创建自定义的数值类型,并将其与内置的int类型进行比较,也可以与其他内置的或自定义的数值类型进行比较,但不能与字符串或其他非数值类型进行比较。

1.2.4.3 成员操作符

对序列或集合这一类数据类型,比如字符串、列表或元组,我们可以使用操作符in来测试成员关系,用not in来测试非成员关系。例如:

>>> p = (4, "frog", 9, -33, 9, 2)
>>> 2 in p
True
>>> "dog" not in p
True

对列表与元组,in操作符使用线性搜索,对非常大的组合类型(包含数万个或更多的数据项),速度可能会较慢;而对字典或集合,in操作可以非常快。这些组合数据类型都将在第3章讲述,这里只展示如何使用in操作符对字符串进行相关操作:

>>>phrase = "Wild Swans by Jung Chang"
>>> "J" in phrase
True
>>> "han" in phrase
True

对字符串数据类型,使用成员运算符可以很方便地测试任意长度的子字符串。(前面已经讲过,字符只不过是长度为1的字符串。)

1.2.4.4 逻辑运算符

Python提供了3个逻辑运算符:and、or与not。and与or都使用short-circuit逻辑,并返回决定结果的操作数——而不是返回布尔值(除非实际上就是布尔操作数)。下面给出一些实际例子:

>>> five = 5
>>> two = 2
>>> zero = 0
>>> five and two
2
>>> two and five
5
>>> five and zero
0

如果逻辑表达式本身出现在布尔上下文中,那么结果也为布尔值,因此,前面的表达式结果将变为True、True与False,比如,if语句。

>>> nought = 0
>>> five or two
5
>>> two or five
2
>>> zero or five
5
>>> zero or nought
0

or操作符是类似的,这里,在布尔上下文中,结果应该为True、True、True与False。

not单一操作符在布尔上下文中评估其参数,并总是返回布尔型结果,对前面的实例,not(zero or nought)的结果为True,not two的结果为False。

1.2.5 要素#5:控制流语句

前面我们曾提及,.py文件中的每条语句都是顺序执行的,从第一条语句开始,逐行执行。实际上,函数、方法调用或控制结构都可以使控制流转向,比如条件分支或循环语句。有意外产生时,控制流也会被转向。

在这一小节中,我们将讲述Python的if语句、while语句以及loop语句,函数将在要素#8中讲解,方法将在第6章讲解。这里还会介绍基本的异常处理机制,并在第4章对其进行全面讲解,但这里首先澄清一些术语。

布尔表达式实际上就是对某对象进行布尔运算,并可以产生一个布尔值结果(True或False)。在Python中,预定义为常量False的布尔表达式、特殊对象None、空序列或集合(比如,空字符串、列表或元组)、值为0的数值型数据项等的布尔结果为False,其他的则为True。创建自定义数据类型(比如在第6章中)时,我们可以自己决定这些自定义数据类型在布尔上下文中的返回值。

在Python中,一块代码,也就是说一条或多条语句组成的序列,称为suite。由于Python中的某些语法要求存在一个suite,Python就提供了关键字pass,pass实际上是一条空语句,不进行任何操作,可以用在需要suite(或者需要指明我们已经考虑了特殊情况)但又不需要进行处理的地方。

1.2.5.1 if语句

Python的常规if语句语法如下2:

if boolean_expression1:
   suite1
elif boolean_expression2:
   suite2
...
elif boolean_expressionN:
   suiteN
else:
   else_suite

与if语句对应的可以有0个或多个elif分支,最后的else分支是可选的。如果需要考虑某个特定的情况,但又不需要在这种情况发生时进行处理,那么可以使用pass作为该分支的suite。

对习惯于C++或Java语法的程序员而言,第一个突出的差别是这里没有圆括号与方括号,另一个需要注意到的情况是冒号的使用,冒号是上面语法中的一个组成部分,但是最初使用时容易忘记。冒号与else、elif一起使用,实质上在后面要跟随suite的任意地方也需要使用。

与大多数其他程序设计语言不同的是,Python使用缩排来标识其块结构。有些程序员不喜欢这一点,尤其是在使用这一功能之前,有些人对这一情况甚至极为讨厌。但习惯这一情况只需要几天时间,在几个星期或几个月之后,不带方括号的代码看起来比使用方括号的代码更优美、更不容易混乱。

由于suite是使用缩排指明的,因此很自然地带来的一个问题是:使用哪种缩排?Python 风格指南建议的是每层缩排4个空格,并且只有空格(没有制表符)。大多数现代的文本编辑器可设置为自动处理这一问题(IDLE的编辑器当然也是如此,大多数其他可以感知Python代码的编辑器也是如此)。如果使用的缩排是一致的,那么对任意数量的空格或制表符,或二者的混合,Python都可以正常处理。本书中,我们遵循官方的Python指南。

下面给出一个非常简单的if语句实例:

if x:
   print("x is nonzero")

对于上面的情况,如果条件x为真,那么suite(print()函数调用)将得以执行。

if lines < 1000:
  print("small")
elif lines < 10000:
  print("medium")
else:
  print("large")

上面给出的是一条稍微复杂一些的if语句,该语句打印一个单词,这个单词用于描述lines变量的值。

1.2.5.2 while语句

while语句用于0次或多次执行某个suite,循环执行的次数取决于while循环中布尔表达式的状态,下面给出其语法格式:

while boolean_expression:
    suite

实际上,while循环的完整语法比上面的要复杂得多,这是因为在其中可以支持break与continue,还包括可选的else分支,这些在第4章将进行讨论。break语句的作用是将控制流导向到break所在的最内层循环——也就是说跳出循环;continue语句的作用是将控制流导向到循环起始处。通常,break语句与continue语句都用在if语句内部,以便条件性地改变某个循环的执行流程。

while True:
    item = get_next_item()
    if not item:
       break
    process_item(item)

while循环具有非常典型的结构,只要还存在需要处理的数据项,就一直循环(get_next_item()与process_item()都是在某处定义的自定义函数)。在上面的实例中,while语句的suite中包含了一条if语句,该if语句本身又包含了自己的suite,因此,在这一实例中必须包含一条break语句。

1.2.5.3 for…in语句

Python的for循环语句重用了关键字in(在其他上下文中,in是一个成员操作符),并使用如下的语法格式:

for variable in iterable:
   suite

与while循环类似,for循环也支持break语句与continue语句,也包含可选的else分支。variable将逐一引用iterable中的每个对象,iterable是可以迭代的任意数据类型,包括字符串(此时的迭代是逐个字符进行)、列表、元组以及Python的其他组合数据类型。

for country in ["Denmark", "Finland", "Norway", "Sweden"]:
   print(country)

上面给出的是一个非常简化的方法,用于打印国家列表。在实际的代码中,更常见的做法是使用变量:

countries = ["Denmark", "Finland", "Norway", "Sweden"]
for country in countries:
   print(country)

实际上,完整的列表(或元组)可以使用print()函数直接打印,比如print(countries),但我们通常更愿意使用一个for循环(或list comprehension,后面会讲述)来打印,以便对格式进行完全的控制。

for letter in "ABCDEFGHIJKLMNOPQRSTUVWXYZ":
   if letter in "AEIOU":
      print(letter, "is a vowel")
   else:
      print(letter, "is a consonant")

在这一代码段中,第一次使用关键字in是将其作为for循环的一部分,变量letter则取值从“A”、“B”一直到“Z”,在循环的每次迭代中变化一次。在该代码段的第二行又一次使用了in,但这次是将其作为成员关系测试操作符。还要注意的是,该实例展示了嵌套循环结构:for循环的suite是if…else语句,同时if语句与else语句又都有自己的suite。

1.2.5.4 基本的异常处理

Python的很多函数与方法都会产生异常,并将其作为发生错误或重要事件的标志。与Python的其他对象一样,异常也是一个对象,转换为字符串时(比如,打印时),异常会产生一条消息文本。异常处理的简单语法格式如下:

try:
    try_suite
except exception1 as variable1:
    exception_suite1
...
except exceptionN as variableN:
   exception_suiteN

要注意的是,as variable部分是可选的。我们可以只关心产生了某个特定的异常,而不关心其具体的消息文本。

完整的语法要更加复杂一些,比如,每个except分支都可以处理多个异常,还有可选的else分支,所有这些内容将在第4章中集中讲述。

异常处理以如下的逻辑工作:如果try块中的suite都正常执行,而没有产生异常,则except模块将被跳过;如果try块中产生了异常,则控制流会立即转向第一个与该异常匹配的suite——这意味着,跟随在产生异常的语句后面的suite中的语句将不再执行;如果发生了异常,并且给定了as variable部分,则在异常处理suite内部,variable引用的是异常对象。

如果异常发生在处理except块时,或者某个异常不能与任何一个except块匹配,Python就会在下一个封闭范围内搜索一个匹配的except块。对合适的异常处理模块的搜索是向外扩展的,并可以延展到调用栈内,直到发现一个匹配的异常处理模块,或者找不到匹配的模块,这种情况下,程序将终止,并留下一个未处理的异常,此时,Python会打印回溯信息以及异常的消息文本。

下面给出一个实例:

s = input("enter an integer: ")
try:
   i = int(s)
   print("valid integer entered:", i)
except ValueError as err:
   print(err)

如果用户输入的是3.5,那么输出为:

invalid literal for int() with base 10: '3.5'

但是如果用户输入的是13,那么输出为:

valid integer entered: 13

很多书籍都把异常处理作为一个高级专题,并尽可能将其安排在后面讲解。实际上,了解异常的产生与处理机制对理解Python的工作方式是基本的要求,因此,我们在一开始就讲述这方面的内容。我们会看到,通过将异常情况从我们真正关心的处理流程中剥离出来,异常处理机制可以使Python代码更具可读性。

1.2.6 要素#6:算术操作符

Python提供了完整的算术运算符集,包括用于基本四则数学运算的操作符+、-、、/。此外,很多Python数据类型可以使用一些增强的赋值操作符,比如+=与=。在操作数都是整数时,+、-、*等操作符可以分别按正常的加法、减法、乘法进行运算:

>>> 5 + 6
11
>>> 3 - 7
-4
>>> 4 * 8
32

要注意的是,像大多数程序设计语言一样,在Python中,-号既可以作为单值操作符(否定),也可以作为二元操作符(减法),Python与一般程序语言不同的地方在于对除法的处理:

>>> 12 / 3
4.0
>>> 3 / 2
1.5

除法操作符会产生一个浮点值,而不是一个整数值;很多其他程序设计语言都是产生一个整数值,并剥离掉小数部分。如果需要产生一个整数值结果,我们可以使用int()进行转换(或使用剥离操作符//,后面会进行讨论)。

>>> a = 5
>>> a
5
>>> a += 8
>>> a
13

乍一看,上面的语句没什么奇怪的地方,尤其是对熟悉类C语言的读者而言,在这种语言中,增强的赋值操作符是一种速记法,用于对某操作生成的结果进行赋值。例如,a += 8实际上与a = a + 8是一样的。然而,这里有两个重要的地方,一个是Python特定的,另一个是任何语言中处理增强的赋值操作符时都会用到的。

第一点需要记住的是,int数据类型是固定的——也就是说,一旦赋值,就不能改变,因此,对固定的对象使用增强的赋值操作符时,实际上是创建一个对象来存储结果,之后,目标对象引用重新绑定,以便引用上面创建的结果对象,而不再引用以前的对象。根据这一原理,前面的例子中,在执行到a += 8语句时,Python会计算a+8,将所得结果存储到新的int对象,之后将a重新绑定为引用这个新的int对象(如果a正在引用的原始对象没有其他的对象引用,就会进入垃圾收集流程)。图1-3所示示出了这一过程。


screenshot

第二个微妙之处在于,a operator= b与a = a operator b并不完全一致。前者只查询一次a的值,因而具有更快的可能性。此外,如果a是一个复杂的表达式(例如,列表元素的索引位置计算,如items[offset + index]),那么使用增强的赋值操作符时后者较少出错。这是因为,如果计算过程需要改变,那么维护者只需要改变一次,而不是两次。

Python重载(对不同的数据类型进行重用)了操作符+与+=,将其分别用于字符串与列表,前者表示连接,后者表示追加字符串并扩展(追加另一个字符串)列表:

>>> name = "John"
>>> name + "Doe"
'JohnDoe'
>>> name += " Doe"
>>> name
'John Doe'

与整数类似,字符串也是固定的,因此,当使用+=时,会创建一个新字符串,并且表达式左边的对象引用将重新绑定到新字符串,就像前面描述的ints一样。列表支持同样的语法,但隐含在后面的流程并不相同:

>>> seeds += ["pumpkin"]
>>> seeds
['sesame', 'sunflower', 'pumpkin']

由于列表是可变的,使用+=后,原始的列表对象会被修改,因此,没有必要对seeds进行重新绑定。图1-4示出了这一过程。


screenshot

既然Python语法聪明地隐藏了可变的与固定的数据类型的区别,为什么还需要规定这两种数据类型?原因最可能还是在于性能。在实现上,固定的数据类型具有比可变的数据类型更加高效的潜力(因为这些固定的数据类型从不改变)。此外,有些组合数据类型(比如集合)只能操纵固定的数据类型。另一方面,可变的数据类型使用起来更加方便。在这些差别起作用的地方,我们将对其进行讨论,比如,在第4章中讨论为自定义函数设置默认参数时,在第3章中讨论列表、集合以及一些其他数据类型时,以及在第6章中展示如何创建自定义数据类型时。

列表+=操作符右边的操作数必须是一个iterable,如果不是,就会产生意外:

>>> seeds += 5
Traceback (most recent call last):
 ...
TypeError: 'int' object is not iterable

对列表进行扩展的正确方法是使用iterable对象,例如:

>>> seeds += [5]
>>> seeds
['sesame', 'sunflower', 'pumpkin', 5]

当然,用于扩展列表的iterable对象本身就有多个数据项:

>>> seeds += [9, 1, 5, "poppy"]
>>> seeds
['sesame', 'sunflower', 'pumpkin', 5, 9, 1, 5, 'poppy']

添加一个普通的字符串——比如“durian—”而不是包含字符串的列表["durian"],就会导致一个合乎逻辑但可能比较奇怪的结果。

>>> seeds = ["sesame", "sunflower", "pumpkin"]
>>> seeds += "durian"
>>> seeds
['sesame', 'sunflower', 'pumpkin', 'd', 'u', 'r', 'i', 'a', 'n']

列表的+=操作符会扩展列表,并将给定的iterable中的每一项追加到列表后。由于字符串是一个iterable,这会导致字符串中的每个字符被单独添加。如果我们使用append()方法,那么该参数总是以单独的项目添加。

1.2.7 要素#7:输入/输出

如果要编写真正有用的程序,我们必须能够读取输入(比如,从控制台用户处,或者从文件中),还要产生输出,并写到控制台或文件。我们已经展示过如何使用Python的内置print()函数,在第4章中我们将对其进一步展开。在这一小节中,我们将集中讲解控制台I/O,并使用shell重定向读取或写入文件。

Python提供了内置的input()函数,用于接收来自用户的输入。这一函数需要一个可选的字符串参数(并将其在控制台上打印),之后等待用户输入响应信息或按Enter键(或Return键)来终止。如果用户不输入任何文本,而只是按Enter键,那么input()函数会返回一个空字符串;否则,会返回一个包含了用户输入内容的字符串,而没有任何行终止符。

下面给出的是我们提供的第一个完整的“有用的”程序,这个程序吸取了前面很多有用的要素——唯一新的是input()函数:

print("Type integers, each followed by Enter; or just Enter to finish")
total = 0
count = 0
while True:
    line = input("integer: ")
    if line:
       try:
           number = int(line)
       except ValueError as err:
           print(err)
           continue
       total += number
       count += 1
   else:
       break
if count:
   print("count =", count, "total =", total, "mean =", total / count)

上面的程序(在本书实例中的sum1.py文件中)只有17个可执行行,下面给出了该程序的典型运行情况:

Type integers, each followed by Enter; or just Enter to finish
number: 12
number: 7
number: 1x
invalid literal for int() with base 10: '1x'
number: 15
number: 5
number:
count = 4 total = 39 mean = 9.75

尽管这一程序很短,但是程序鲁棒性很好。如果用户输入的是无法转换为整数的字符串,那么这一问题会被异常处理流程捕捉,并打印一条相关信息,之后程序流程转向到循环的开始处(“继续循环”)。最后一个if语句的作用是:如果用户不输入任何数值,那么摘要不会输出,并且被0除也会避免。

第7章中将全面讲解文件处理,不过在这里我们也可以创建文件,这是通过将print()函数的输出从控制台重定向到文件来实现的,例如:

C:\>test.py > results.txt

上面的语句使得test.py中的print()函数调用产生的结果写入到文件result.txt中。上面的语法格式在Windows控制台与UNIX控制台中都可以正常工作。对于Windows,如果Python 2是系统默认的Python版本,我们就必须写成C:Python30python.exe test.py > results.txt,或者如果Python 3在PATH中占先(尽管我们不会再提及这个问题),就写成python.exe test.py > results.txt;对于UNIX,我们首先要把程序变为可执行的(chmod +x test.py),之后通过./test.py调用该程序——除非程序所在目录恰好为PATH。

通过将数据文件重定向为输入(与上面重定向输出的方式类似),可以实现数据的读取。然而,如果我们对sum1.py使用重定向,就会导致失败。这是因为,在收到EOF(文件终止)字符时,input()函数会产生异常。下面给出了一个更具鲁棒性的程序版本(sum2.py),该程序可以接收来自用户键盘输入的输入信息,也可以接收来自文件重定向的输入信息:

print("Type integers, each followed by Enter; or ^D or ^Z to finish")
total = 0
count = 0
while True:
    try:
        line = input()
        if line:
           number = int(line)
           total += number
           count += 1
    except ValueError as err:
        print(err)
        continue
    except EOFError:
        break
if count:
   print("count =", count, "total =", total, "mean =", total / count)

在命令行中,输入sum2.py< datasum2.dat(这里,实例的data子目录中的文件sum2.dat包含了一列数据,每行一个),输出信息为:

Type integers, each followed by Enter; or ^D or ^Z to finish
count = 37 total = 1839 mean = 49.7027027027

为使程序更适合交互式使用并使用重定向技术,我们对该程序进行了几处修改。首先,我们将终止方式从空白行变为EOF字符(在UNIX上为Ctrl+D,Windows上为Ctrl+Z并按Enter键),这样,在处理包含空白行的输入文件时,程序更富于鲁棒性,不再为每个数值打印一个提示符(因为对重定向的输入而言这个没有意义)。此外,我们还使用了一个单独的try块,其中包含两个异常处理过程。

要注意的是,如果输入了无效的整数(来自键盘或重定向输入文件中的损坏行),那么int()转换将产生一个ValueError异常,控制流也将立即转向相关的except模块。这意味着,输入无效数据时,total或count都不会递增,而这正是我们所期望的。

我们也可以使用两个单独的异常处理Try语句块:

while True:
    try:
        line = input()
        if line:
           try:
               number = int(line)
           except ValueError as err:
               print(err)
               continue
           total += number
           count += 1
    except EOFError:
        break

但是我们更愿意将异常处理模块集中放在程序末尾,以保证主要流程尽可能清晰。

1.2.8 要素#8:函数的创建与调用

使用前面几个要素中讲解的数据类型与控制结构编写程序是完全可能的,然而,在实际中,非常常见的情况是重复进行同样的处理过程,只不过有细微的差别,比如不同的起点值。Python提供了一种将多个suites封装为函数的途径,函数就可以参数化,并通过传递不同的参数来调用。下面给出的是用于创建函数的通常语法格式:

def functionName(arguments):
    suite

这里,arguments是可选的;如果有多个参数,就必须使用逗号进行分隔。每个Python函数有一个返回值,默认情况下为None,除非我们使用语法return value来从函数返回,此时value是实际的返回值。返回值可以是仅仅一个值,也可以是一组值。调用者可以忽略返回值,并简单地将其丢弃。

要注意的是,def是一条与赋值操作符工作方式类似的语句。执行def时,会创建一个函数对象,同时创建一个带有指定名的对象引用(引用该函数对象)。由于函数也是对象,因此可以存储在组合数据类型中,并作为参数传递给其他函数,后续章节中将展示这一点。

在编写交互式的控制台应用程序时,一个频繁的需求是从用户处获取整数,下面给出了一个完成这一功能的函数:

def get_int(msg):
    while True:
        try:
            i = int(input(msg))
            return i
       except ValueError as err:
            print(err)

这个函数有一个参数msg,在while循环内部,用户被要求输入一个整数,如果输入无效,则会产生一个ValueError异常,并打印错误消息,同时循环也将迭代进行。输入有效的整数后,会返回给调用者。下面展示了如何调用这个函数:

age = get_int("enter your age: ")

在这一实例中,强制使用单一的参数,这是因为我们没有提供默认值。实际上,对于支持默认参数值、位置参数与关键字参数的函数参数,Python支持非常复杂与灵活的语法结构。第4章将集中讲述这些语法。

尽管创建自己的函数是一件很惬意的事情,但是很多时候并不需要这样做。这是因为,Python有大量的内置函数,其标准库的大量模块中包含更多的函数,因此大多数我们所需要的函数都可以直接使用。

Python模块实际上就是包含Python代码的.py文件,比如自定义函数与类(自定义数据类型)的定义,有时候还包括变量等。要使用某个模块内的函数功能,必须先导入该模块,例如:

import sys

要导入一个模块,必须使用inport语句,其后跟随.py文件名,但是不需要写出该扩展名3。导入一个模块后,就可以访问其内部包含的任意函数、类以及变量。例如:

print(sys.argv)

sys模块提供了argv变量——该变量实际上是一个列表,其首项为该程序的名称,第二个参数及后续的参数为该程序的命令行参数。前面两行构成了完整的echoargs.py程序。如果在命令行中以echoargs.py -v命令调用该程序,就会在控制台上打印['echoargs.py', '-v']。(在UNIX上,第一个条目应该是/echoargs.py')

通常,使用模块中函数的语法格式为moduleName.functionName(arguments)。其中使用了我们在要素3中介绍的点(“存取属性”)操作符。标准库中包含大量的模块,随着本书的逐步展开,我们会逐渐介绍和使用其中的大量模块。标准模块的模块名都是小写字母,因此,一些程序员为自己编写的模块使用首字母大写的名(比如,My Module),以便区别于标准模块。

下面看一个实例,random模块(在标准库的random.py文件中)提供了很多有用的函数:

import random
x = random.randint(1, 6)
y = random.choice(["apple", "banana", "cherry", "durian"])

在这些语句执行之后,x将包含一个1~6之间(包含边界值)的一个整数,y将包含传递给random.choice()函数的列表之间的一个字符串。

常规的做法是将所有import语句放在.py文件的起始处,在shebang行和模块的文档之后(第5章将对模块文档进行介绍)。我们建议程序员首先导入标准库模块,之后导入第三方库模块,最后才导入自己编写的模块。

相关文章
|
26天前
|
Python
Python的第一个程序
【4月更文挑战第8天】Python的第一个程序
34 6
|
26天前
|
存储 人工智能 算法
【Python】Yahtzee(掷骰子游戏)模拟程序【独一无二】
【Python】Yahtzee(掷骰子游戏)模拟程序【独一无二】
|
1天前
|
监控 测试技术 持续交付
Python自动化测试代理程序可用性
总之,通过编写测试用例、自动化测试和设置监控系统,您可以确保Python自动化测试代理程序的可用性,并及时发现和解决问题。这有助于提供更可靠和高性能的代理服务。
11 4
|
3天前
|
Python
简单的 Python 计算器程序
这是一个简单的Python计算器程序,实现了加、减、乘、除功能。用户选择运算类型及输入两个数字后,程序依据选择调用相应函数进行计算并显示结果。若输入非法,程序显示错误信息。
10 3
|
6天前
|
监控 测试技术 API
Python Web应用程序构建
【4月更文挑战第11天】Python Web开发涉及多种框架,如Django、Flask和FastAPI,选择合适框架是成功的关键。示例展示了使用Flask创建简单Web应用,以及如何使用ORM(如SQLAlchemy)管理数据库。
17 4
|
8天前
|
人工智能 数据库 开发者
Python中的atexit模块:优雅地处理程序退出
Python中的atexit模块:优雅地处理程序退出
8 3
|
10天前
|
存储 索引 Python
Python从入门到精通——1.3.1练习编写简单程序
Python从入门到精通——1.3.1练习编写简单程序
|
10天前
|
Python
过年了,让GPT用Python给你写个放烟花的程序吧!
过年了,让GPT用Python给你写个放烟花的程序吧!
16 0
|
15天前
|
存储 数据采集 NoSQL
使用Python打造爬虫程序之数据存储与持久化:从网络到硬盘的无缝对接
【4月更文挑战第19天】本文探讨了爬虫中的数据存储与持久化技术,包括文本文件存储、数据库(关系型与非关系型)、NoSQL数据库和键值存储,以及ORM框架的使用。根据数据类型、规模和访问需求选择合适存储方式,并注意数据安全、备份和恢复策略。正确选择和应用这些技术能有效管理和利用爬取数据。
|
15天前
|
数据采集 缓存 算法
使用Python打造爬虫程序之Python中的并发与异步IO:解锁高效数据处理之道
【4月更文挑战第19天】本文探讨了Python中的并发与异步IO,区分了并发(同时处理任务)与并行(同时执行任务)的概念。Python的多线程受限于GIL,适合IO密集型任务,而多进程适用于CPU密集型任务。异步IO通过非阻塞和回调/协程实现高效IO,Python的asyncio库提供了支持。应用场景包括Web开发和网络爬虫等。实践指南包括理解任务类型、使用asyncio、避免阻塞操作、合理设置并发度和优化性能。理解并运用这些技术能提升Python程序的效率和性能。