第五章-条件、循环和其他语句
我们已经见过几种语句(print语句、import语句和赋值语句),先来看看这些语句的其他一些用法,再深入探讨条件语句和循环语句。然后,我们将介绍列表推导,它们虽然是表达式,但工作原理几乎与条件语句和循环语句相同,最后,我们将介绍pass、del和exec。
5.1 再谈print和import
5.1.1 打印多个参数
我们指导,print可用于打印一个表达式,这个表达式要么是字符串,要么将自动转换为字符串。但实际上,我们可以打印多个表达式,条件是用逗号分隔它们:
>>> print('Age:', 42)
Age: 42
#如我们所见,在参数之间插入了一个空格字符。在我们要合并文本和变量值而又不想使用字符串格式设置功能时,这种行为很有帮助:
>>> name = 'Gumby'
>>> salutation = 'Mr.'
>>> greeting = 'Hello,'
>>> print(greeting, salutation, name)
Hello, Mr. Gumby
#如果字符串变量greeting不包含逗号,如何在结果中添加呢?
##不能像下面这样做:因为这样将在逗号前添加一个空格。
>>> print(greeting, ',', salutation, name)
Hello, , Mr. Gumby
#下面是一种可行的方案:
>>> print(greeting + ',', salutation, name)##它将逗号和变量greeting相加。
Hello,, Mr. Gumby
#如果需要,可以自定义分隔符:
>>> print('I', 'wish', 'to', 'register', 'a', 'complaint', sep='_')
I_wish_to_register_a_complaint
#你还可以自定义结束字符串,以替换默认的换行符。例如,如果将结束字符串指定为空字符串,以后就可继续打印到当前行。
print('Hello,', end='')
print('world!')
##上述代码打印Hello, world!
5.1.2 导入时重命名
#从模块导入时,通常使用:
import somemodule
##或使用:
from somemodule import somefunction
##或:
from somemodule import somefunction, anotherfunction, yetanatherfunction
##或:
from somemodule import *
#仅当你确定要导入模块中的一切,采用最后一种方式。但如果有两个模块,它们都包含函数open,该如何办呢?可以使用第一种方式导入这两种模块,并像下面这样调用函数:
module1.open(...)
module2.open(...)
#但还有一种办法:在语句末尾添加as子句并指定别名,下面是一个导入整个模块并给它指定别名的例子:
>>> import math as foobar
>>> foobar.sqrt(4)
2.0
#下面是一个导入特定函数并给它指定别名的例子:
>>> from math import sqrt as foobar
>>> foobar(4)
2.0
#对于前面的函数open,可像下面这样导入它们:
from module1 import open as open1
from module2 import open as open2
##注:有些模块(如os.path)组成了层次结构(一个模块位于另一个模块(第十章)中)。
5.2 赋值魔法
5.2.1 序列解包
赋值语句我们见过很多,有的给变量赋值,还有的给数据结构的一部分(如列表中的元素和切片,或者字典项)赋值,但还有其他类型的赋值语句。
#可同时(并行)给多个变量赋值:
>>> x, y, z = 1, 2, 3
>>> print(x, y, z)
1 2 3
#使用这种方式还可以交换多个变量的值:
>>> x, y = y, x
>>> print(x, y, z)
2 1 3
实际上,这里执行的操作称为序列解包(或可迭代对象解包):将一个序列(或任何可迭代对象)解包,并将得到的值储存到一系列变量中。例如:
>>> values = 1, 2, 3
>>> values
(1, 2, 3)
>>> x, y, z = values
>>> x
1
在这里使用返回元组(或其他序列或可迭代对象)的函数或方法时很有用。加入要从字典中随便获取(或删除)一个键-值对时,可使用方法popitem,它随便获取一个键-值对并以元组的方式返回。接下来,可字节将返回的元组解包到两个变量中:
>>> scoundrel = {'name': 'Robin', 'girlfriend': 'Marion'}
>>> key, value = scoundrel.popitem()
>>> key
'girlfriend'
>>> value
'Marion'
#这让函数能够返回被打包成元组的多个值,然后通过一条赋值语句轻松地访问这些值。
#注:要解包的序列包含的元素个数必须与你在等号左边列出的目标个数相同,否则Python将引发异常:
>>> x, y, z = 1, 2
Traceback (most recent call last):
File "<pyshell#14>", line 1, in <module>
x, y, z = 1, 2
ValueError: not enough values to unpack (expected 3, got 2)
>>> x, y, z = 1, 2, 3, 4
Traceback (most recent call last):
File "<pyshell#15>", line 1, in <module>
x, y, z = 1, 2, 3, 4
ValueError: too many values to unpack (expected 3)
#可使用星号运算符(*)来收集多余的值,这样无需确保值和变量的个数相同:
>>> a, b, *rest = 1, 2, 3, 4
>>> rest
[3, 4]
#还可以将带星号的变量放在其他位置:
>>> name = "Albus Percival Wulfric Brian Dumbledore"
>>> first, *middle, last = name.split()
>>> middle
['Percival', 'Wulfric', 'Brian']
#赋值语句的右边可以是任何类型的序列,但带星号的变量最终包含的总是一个列表。在变量和值和个数相同时亦如此:
>>> a, *b, c = 'abc'
>>> a, b, c
('a', ['b'], 'c')
#这种收集方式也可用于函数参数列表中。
5.2.2 链式赋值
链式赋值是一种快捷方式,用于将多个变量关联到同一个值。
#这有点像前一节介绍的并行赋值,但只涉及一个值:
x = y = somefunction()
#上述代码与下面的代码等价:
y = somefunction()
x = y
#注:这两条语句可能与下面的语句不等价:
x = somefunction()
y = somefunction()
有关这方面的详细信息,请参阅5.4.6节介绍相同运算符(is)的部分。
5.2.3 增强赋值
可以不编写代码x = x + 1,而将右边表达式中的运算符(这里是+)移到赋值运算符(=)的前面,从而写成x += 1。这称为增强赋值,适用于所有标准运算符,如*、/、%等。
>>> x = 2
>>> x += 1
>>> x *= 2
>>> x
6
#增强赋值也可以用于其他数据类型(只要使用的双目运算符可用于这些数据类型):
>>> found = 'foo'
>>> found += 'bar'
>>> found *= 2
>>> found
'foobarfoobar'
通过使用增强赋值,可让代码更紧凑、更简洁,同时在很多情况下的可读性更强。
5.3 代码块:缩进的乐趣
代码块是一组语句,可在满足条件时执行(if语句),可执行多次(循环),等等。代码块是通过缩进代码(即在前面加空格)来创建的。
注:也可以使用制表符来缩进代码块。Python将制表符解释为移到下一个制表位(相邻制表位相距8个空格),但标准(也是更佳的)做法是只使用空格(而不是使用制表符来缩进),且每级缩进4个空格。
在同一个代码块中,各行代码的缩进量必须相同。下面的伪代码(并非真正的Python代码)演示了如何缩进:
this is a line
this is another line:
this is another block
continuing the same block
the last line of the block
phew, there we escaped the inner block
在很多语言中,都使用一个特殊的单词或字符(如begin或{)来标识代码块的起始位置,并使用另一个特殊的单词或字符(如end或})来标识结束位置。在Python中,使用冒号(:)指出接下来是一个代码块,并将该代码块中的每行代码都缩进相同的程度。发现缩进量与之前相同时,我们就知道当前代码块到此结束了。(很多用于编程的编辑器(如vscode)和IDE(如PyCharm)指导如何缩进代码块,可帮助我们轻松地正确缩进。)
下面来看代码块的用途。
5.4 条件和条件语句
到目前为止,在我们编写的程序中,语句都是逐条执行的。现在更进一步,让程序选择是否执行特定的程序块。
5.4.1 这正是布尔值的用武之地
用作布尔表达式(如用作if语句中的条件)时,下面的值都将被解释器视为假:
False None 0 "" () [] {}
换而言之,标准值False和None、各种类型(包括浮点数、复数等)的数值为0、空序列(如空字符串、空元组和空列表)以及空映射(如空字典)都被视为假,而其他各种值都被视为真,包括特殊值True。
这意味着任何Python值都可以解释为真值,标准真值为True和False。实际上,Ture和False不过是0和1的别名,虽然看起来不同,但作用是相同的。
>>> True
True
>>> False
False
>>> True == 1
True
>>> False == 0
True
>>> True + False + 42
43
因此,如果我们看到一个返回1或0的表达式,就知道这实际上意味着True或False。
布尔值True和False属于类型bool,而bool和list、str和tuple一样,可用来转换其他值。
>>> bool('I think, therefore I am')
True
>>> bool(42)
True
>>> bool('')
False
>>> bool(0)
False
#鉴于任何值都可以用作布尔值,因此我们几乎不需要显式地进行转换(Python会自动转换)
注:虽然[]和””都为假(即bool([]) == bool(“”) == False),但它们并不相等(即[] != “”)。对其他各种为假的对象来说,情况亦如此(一个更显然的例子是() != False)。
5.4.2 有条件地执行和if语句
真值可以合并,至于如何合并稍后再将,先看看真值可以用来做什么:
name = input('What is your name? ')
if name.endswich('Gumby'):
print('Hello, Mr. Gumby')
#str.endswith(suffix[, start[, end]])用于判断字符串是否以指定后缀结尾,如果以指定后缀结尾返回True,否则返回False。可选参数"start"与"end"为检索字符串的开始与结束位置。
这就是if语句,让我们能够有条件地执行代码。这意味着如果条件(if和冒号之间的表达式)为前面定义的真,就执行后续代码块(这里是一条print语句);如果条件为假,就不执行。
5.4.3 else子句
在前一节的示例中,如果你输入以Gumby结尾的名字,方法name.endswith将返回True,导致后续代码块执行——打印问候语。如果你愿意,可以使用else子句增加一种选择(之所以叫子句是因为else不是独立的语句,而是if语句的一部分)。
name = input('What is your name?')
if name.endswith('Gumby'):
print('Hello, Mr. Gumby')
else:
print('Hello, stranger')
还有一个与if语句很像的“亲戚”:条件表达式,下面的表达式使用if和else确定其值:
status = "friend" if name.endswith("Gumby") else "stranger"
#如果条件(紧跟在if后面)为真,表达式的结果为提供的第一个值(这里为"friend"),否则为第二个值"stranger"。
5.4.4 elif子句
要检查多个条件,可使用elif。elif是else if的缩写,由一个if子句和一个else子句组合而成,也就是包含条件的else子句。
num = int(input('Enter a number: '))
if num > 0:
print('The number is positive')
elif num < 0:
print('The number is negative')
else:
print('The number is zero')
5.4.5 代码嵌套
我们可以将if语句放在其他if语句块中,如下所示:
name = input('What is your name? ')
if name.endswith('Gumby'):
if name.startswith('Mr.'):
print('Hello, Mr. Gumby')
elif name.startswith('Mrs.'):
print('Hello, Mrs. Gumby')
else:
print('Hello, stranger')
5.4.6 更复杂的条件
这就是我们需要指导的有关if语句的全部知识。下面来说说条件本身,因为它们是有条件执行中最有趣的部分。
1. 比较运算符
在条件表达式中,最基本的运算符可能是比较运算符,它们用于执行比较。
表达式 | 描述 |
---|---|
x == y | x等于y |
x < y | x小于y |
x > y | x大于y |
x >= y | x大于或等于y |
x <= y | x小于或等于y |
x != y | x不等于y |
x is y | x和y是同一个对象 |
x is not y | x和y是不同的对象 |
x in y | x是容器(如序列)y的成员 |
x not in y | x不是容器(如序列)y的成员 |
与赋值一样,Python也支持链式比较:可同时使用多个比较运算符,如0 < age < 100。
下面是一些需要特别注意的比较运算符:
相等运算符
要确定两个对象是否相等,可使用比较运算符,用两个等号(==)表示。
>>> "foo" == "foo"
True
>>> "foo" == "bar"
False
is:相同运算符
这个运算符很有趣,其作用看似与==一样,但实际上并非如此。
>>> x = y = [1, 2, 3]
>>> z = [1, 2, 3]
>>> x == y
True
>>> x == z
True
>>> x is y
True
>>> x is z
False
is检查两个对象是否相同(而不是相等)。变量x和y指向同一个列表,而z指向另一个列表(其中包含的值以及这些值的排列顺序都与前一个列表相同)。这两个列表虽然相等,但并非是同一个对象。
总之,==用来检查两个对象是否相等,而is用来检查两个对象是否相同(是同一个对象)。
in:成员资格运算符
运算符in在2.2.5节介绍过,与其他比较运算符一样,它也可用于条件表达式中。
name = input('What is your name?')
if 's' in name:
print('Your name contains the letter "s".')
else:
print('Your name does not contains the letter "s".')
字符串和序列的比较
字符串是根据字符的字母排列顺序进行比较的。
>>> "alpha" < "beta"
True
#虽然基于的字母排列顺序,但字母都是Unicode字符,它们是按照码点排列的。
#实际上,字符是根据顺序值排列的。要获悉字母的顺序值,可使用函数ord。这个函数的作用与函数chr相反:
>>> ord("H")
72
>>> chr(128584)
'\U0001f648'
>>> chr(72)
'H'
#涉及大写字母时,可以使用字符串方法lower忽略大小写来比较大小写字母的顺序。
#其他序列的比较方式与此相同,但这些顺序包含的元素可能不是字符,而是其他类型的值:
>>> [1, 2] < [2, 1]
True
#如果序列的元素为其他序列,将根据同样的规则对这些元素进行比较:
>>> [2, [1, 4]] < [2, [1, 5]]
True
2. 布尔运算符
#假设我们要编写一个程序,让它读取一个数,并检查这个数是否位于1~10(含)。为此,可像下面这样做:
number = int(input('Enter a number between 1 and 10: '))
if number <= 10:
if number >=1:
print('Great!')
else:
print('Wrong!')
else:
print('Wrong!')
#这可行,但是我们输入了print('Wrong!')两次,稍显笨拙,可以如下修改:
number = int(input('Enter a number between 1 and 10: '))
if number <= 10 and number >= 1:
print('Great!')
else:
print('Wrong!')
##通过使用链式比较1 <= number <= 10可进一步简化这个示例。
运算符and是一个布尔运算符。它接受两个真值,并在这两个真值都为真时返回真,否则返回假。还有另外两个布尔运算符:or和not。通过使用这三个运算符,能以任何方式组合真值。
if ((cash > price) or customer_has_good_credit) and not out_of_stock:
give_goods()
短路逻辑和条件表达式:
布尔运算符有个有趣的特征:只做必要的计算。例如,仅当x和y都为真时,表达式x and y才为真。因此如果x为假,这个表达式将立即返回假,而不管y。实际上,如果x为假,这个表达式将返回x,否则返回y。这种行为称为短路逻辑(或者延迟求值):布尔运算符常被称为逻辑运算符,如我们所见,在有些情况下将绕过第二个值。对于运算符or,情况亦是如此。需要注意的是,位于布尔运算符后面的代码(如函数调用)可能根本不会执行。
5.4.7 断言
if语句有一个很有用的“亲戚”,其工作原理类似于下面的伪代码:
if not condition:
crash program
编写这样的代码是为了让程序在错误条件出现时立即崩溃胜过以后再崩溃。基本上,我们可以要求某些条件得到满足(如核实函数参数满足要求或为初始测试和调试提供帮助),为此可在语句中使用关键字assert。
>>> age = 10
>>> assert 0 < age < 100
>>> age = -1
>>> assert 0 < age < 100
Traceback (most recent call last):
File "<pyshell#63>", line 1, in <module>
assert 0 < age < 100
AssertionError
#如果知道必须满足特定条件,程序才能正确运行,可在程序中添加assert语句充当检查点。
#还可在条件后面添加一个字符串,对断言做出说明。
>>> age = -1
>>> assert 0 < age <100, 'The age must be realistic'
Traceback (most recent call last):
File "<pyshell#65>", line 1, in <module>
assert 0 < age <100, 'The age must be realistic'
AssertionError: The age must be realistic
5.5 循环
如果我们希望程序不断地执行下去,直到人为停止,需要编写类似于下面的代码(伪代码):
while we are't stopped:
send mail
wait one month
5.5.1 while循环
x = 1
while x <= 100:
print(x)
x += 1
还可以使用循环来确保用户输入名字,如下所示:
name = ''
while not name:
name = input('Please enter your name: ')
print('Hello, {}!'.format(name))
#提示:如果我们只是输入一个空格字符,程序将接受这个名字,因为包含一个空格字符的字符串不是空的,因此不会将name视为假。这无疑是这个小程序的一个瑕疵,但很容易修复:只需将while not name改为while not name or name.isspace()或while not name.strip()即可。
5.5.2 for循环
while语句非常灵活,可用于在条件为真时反复执行代码块。这在通常情况下很好,但有时候我们可能想根据需要进行定制。一种这样的需求是为序列(或其他可迭代对象)中每个元素执行代码块。
注:基本上,可迭代对象(第九章)是使用for循环进行遍历的对象。
为此,可使用for语句:
words = ['this', 'is', 'an', 'ex', 'parrot']
for word in words:
print(word)
#或:
numbers = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
for number in numbers:
print(number)
鉴于迭代(也就是遍历)特定范围内的数是一种常见的任务,Python提供了一个创建范围的内置函数。
>>> range(0, 10)
range(0, 10)
>>> list(range(0, 10))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
#范围类似于切片。它们包含起始位置(这里为0),但不包含结束位置(这里为10)。在很多情况下,我们都希望起始位置为0。实际上,如果只提供一个位置,将把这个位置视为结束位置,并假定起始位置为0。
>>> range(10)
range(0, 10)
#下面的程序打印数1~100:
for number in range(1, 101):
print(number)
#注:相比前面使用的while循环,这些代码要紧凑得多。
提示:只要能够使用for循环,就不要使用while循环。
5.5.3 迭代字典
要遍历字典的所有关键字,可像遍历序列那样使用普通的for语句。
d = {'x': 1, 'y': 2, 'z': 3}
for key in d:
print(key, 'corresponds to', d[key])
#也可使用keys等字典方法来获取所有的键。如果只对值感兴趣,可使用d.values。我们还记得,d.items以元组的方式返回键-值对。for循环的优点之一是,可在其中使用序列解包。
for key, value in d.items():
print(key, 'corresponds to', value)
注:字典元素的排列顺序是不确定的。换而言之,迭代字典的键或值时,一定会处理所有的键或值,但不知道处理的顺序。如果顺序很重要,可将键或值存储在一个列表中并对列表排序,再进行迭代。要让映射记住其项的插入顺序,可使用模块collections中的OrderedDict类。
5.5.4 一些迭代工具
Python提供了多个可比昂注迭代序列(或其他可迭代对象)的函数,其中一些位于第十章将介绍的模块itertools中,但还有一些内置函数使用起来也很方便。
1. 并行迭代
有时候,我们可能想要同时迭代两个序列:
names = ['anne', 'beth', 'george', 'damon']
ages = [12, 45, 32, 102]
#如果要打印名字和对应的年龄,可像下面这样做:
for i in range(len(names)):#i是用作循环索引变量的标准名称。
print(names[i], 'is', ages[i], 'years old.')
一个很有用的并行迭代工具是内置函数zip,它将两个序列“缝合起来”,并返回一个由元组组成的序列。返回值是一个适合迭代的对象,要查看其内容,可使用list将其转换为列表。
>>> names = ['anne', 'beth', 'george', 'damon']
>>> ages = [12, 45, 32, 102]
>>> list(zip(names, ages))
[('anne', 12), ('beth', 45), ('george', 32), ('damon', 102)]
#缝合后,可在循环中将元素解包。
for name, age in zip(names, ages):
print(name, 'is', age, 'years old.')
#函数zip可缝合任意数量的序列。需要指出的是,当序列的长度不同时,函数zip将在最短的序列用完后停止缝合。
2. 迭代时获取索引
在有些情况下,我们需要在迭代对象序列的同时获取当前对象的索引。
#例如,你可能像替换一个字符串列表中所有包含子串‘xxx’的字符串。当然,完成这种任务的方法有很多,但这里假设要像下面这样做:
for string in strings:
if 'xxx' in string:
index = strings.index(string)#在字符串列表中查找字符串
strings[index] = '[censored]'
#这可行,但是替换前的搜索好像没有必要。另外,如果没有替换,搜索返回的索引可能不对(即返回的是该字符串首次出现处的索引)。下面是一种更佳的解决方案:
index = 0
for string in strings:
if 'xxx' in string:
strings[index] = '[censored]'
index +=1
#这个方案虽然也可以接受,但看起来也优点笨拙。另一种解决方案是使用内置函数enumerate:
for index, string in enumerate(strings):
if 'xxx' in string:
strings[index] = '[censored]'
#这个函数让我们能够迭代索引-值对,其中的索引是自动提供的。
3. 反向迭代和排序后再迭代
来看另外两个很有用的函数:reversed和sorted。它们类似于列表方法reverse和sort(sorted接受的参数也与sort类似),但可用于任何序列或可迭代的对象,且不就地修改对象,而是返回反转和排序后的版本。
>>> sorted([4, 3, 6, 8, 3])
[3, 3, 4, 6, 8]
>>> sorted('Hello, world!')
[' ', '!', ',', 'H', 'd', 'e', 'l', 'l', 'l', 'o', 'o', 'r', 'w']
>>> list(reversed('Hello, world!'))
['!', 'd', 'l', 'r', 'o', 'w', ' ', ',', 'o', 'l', 'l', 'e', 'H']
>>> ''.join(reversed('Hello, world!'))
'!dlrow ,olleH'
请注意,sorted返回一个列表,而reversed像zip那样返回一个更神秘的迭代对象。我们无需关系这到底意味着什么,只管在for循环或join等方法中使用它,不会有任何问题。只是我们不能对它执行索引或切片的操作,也不能直接对它调用列表的方法。要执行这些操作,可先使用list对返回对象进行转换。
提示:要按字母表排序,可先转换为小写。为此,可将sort或sorted的key参数设置为str.lower。例如,sorted(“aBc”, kye=str.lower)返回[‘a’, ‘B’, ‘c’]。
5.5.5 跳出循环
通常,循环会不断执行代码块,指导条件为假或使用完序列中的所有元素。但在有些情况下,我们可能想要中断循环、开始新迭代(进入“下一轮”代码块执行流程)或直接结束循环。
1. break
要结束(跳出循环),可使用break。
#假设要找出小于100的最大平方值,可从100开始向下迭代。找到一个平方值后,无需再迭代,因此直接跳出循环。
from math import sqrt
for n in rang(99, 0, -1):
root = sqrt(n)
if root == int(root):
print(n)
break
#range有第三个参数——步长,即序列中相邻数的差。通过将步长设置为负数,可让range向下迭代;还可让它跳过一些数:
>>> range(0, 10, 2)
2. countinue
语句countinue没有break用得多。它结束当前迭代,并跳到下一次迭代开头。这基本上意味着跳过循环体中余下的语句,但不结束循环。这在循环体庞大而复杂,且存在多个要跳过它的原因时很有用。在这种情况下,可使用countinue,如下所示:
for x in seq:
if condition1: continue
if condition2: continue
if condition3: continue
do_something()
do_something_else()
do_another_thing()
etc
#然而,在很多情况下,使用一条if语句就足够了:
for x in seq:
if not (condition1 or condition2 or condition3):
do_something()
do_something_else()
do_another_thing()
etc()
continue虽然是一个很有用的工具,但并非是不可或缺的。然而我们必须熟悉break语句,因为在while True循环中经常用到它,这将在下一小节中讨论。
3. while True/break
如果我们要在用户根据提示输入单词时执行某种操作,并在用户没有提供单词时结束循环。为此,一种办法如下:
word = 'dummyu'
while word:
word = input('Please enter a word: ')
#使用这个单词做些事情:
print('The word was', word)
#这些代码运行情况如下:
Please enter a word: first
The word was first
Please enter a word: second
The word woa second
Please enter a word:
#这与我们希望的一致,但我们可能想使用单词做些比打印它更有用的事情。然而,这写代码有些难看。为进入循环,我们需要将一个哑值(未用的值)赋给word。像这样的哑值通常昭示着我们的做法不太对。下面尝试消除这个哑值:
word = input('Please enter a word: ')
while word:
#使用这个单词做些事情:
print('The word was ', word)
word = input('Please enter a word: ')
#哑值消除了,但包含重复的代码,如何避免这样的重复呢?可使用while True/break。
while True:
word = input('Please enter a word: ')
if not word: break
#使用这个单词做些事情:
print('The word was ', word)
while True导致循环永不结束,但我们将条件放在了循环体内的一条if语句中,而这条if语句将在条件满足时调用break。这说明并非只能像常规while循环那样在循环开头结束循环,而是可在循环体的任何地方结束循环。if/break行将整个循环分成两部分:第一部分负责设置,第二部分在循环条件为真时使用第一部分初始化的数据。
5.5.6 循环中的else子句
要在循环提前结束时采取某种措施很容易(break),但有时可能想在循环正常结束时才采取某种措施。
可在循环开始前定义一个布尔变量值并将其设置为False,再在跳出循环时将其设置为True。这样就可在循环后面使用一条if语句来判断循环是否是提前结束的:
broke_out = False
for x in seq:
do_something(x)
if condition(x):
broke_out = True
break
do_something_else(x)
if not broke_out:
print("I did't break out!")
#一种更简单的办法是在循环中添加一条else子句,它仅仅在没有调用break时才执行。继续前面讨论break时的示例:
form math import sqrt
for n in range(99, 81, -1):#不包含81
root = sqrt(n)
if root == int(root):
print(n)
break
else:
print("Didn't find it!")
5.6 简单推导
列表推导是一种从其他列表创建列表的方式,类似于数序中的集合推导。
#列表推导的原理非常简单,有点类似于for循环:
>>> [x * x for x in range(10)]
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
#如果只想打印那些能被3整除的平方值:
>>> [x * x for x in range(10) if x % 3 == 0]
[0, 9, 36, 81]
#还可以添加更多的for部分:
>>> [(x, y) for x in range(3) for y in range(3)]
[(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2), (2, 0), (2, 1), (2, 2)]
#作为对比,下面的两个for循环创建同样的列表:
result = []
for x in range(3):
for y in range(3):
result.append((x, y))
#与以前一样,使用多个for部分时,也可添加if子句:
>>> girls = ['alice', 'bernice', 'clarice']
>>> boys = ['chris', 'arnold', 'bob']
>>> [b+'+'+g for b in boys for g in girls if b[0] == g[0]]
['chris+clarice', 'arnold+alice', 'bob+bernice']
##这些代码将名字的首字母相同的男孩和女孩配对。
前述配对示例的效率不太高,因为它要检查每种可能的配对。使用Python解决这个问题的方法有很多,下面是Alex Martelli推荐的解决方案:
girls = ['alice', 'bernice', 'clarice']
boys = ['chris', 'arnold', 'bob']
letterGirls = {}
for girl in girls:
letterGirls.setdefault(girl[0], []).append(girl)
print([b+'+'+g for b in boys for g in letterGirls[b[0]]])
#这个程序创建一个名为letterGirls的字典,其中每项的键都是一个字母,而值为以这个字母打头的女孩名字组成的列表。创建这个字典后,列表推导遍历所有的男孩,并查找名字首字母与当前男孩相同的所有女孩。这样,这个列表推导就无需尝试所有的男孩和女好组合并检查他们的名字首字母是否相同了。
使用圆括号代替方括号并不能实现元组推导,而是将创建生成器(第九章)。然而,可使用花括号来执行字典推导。
>>> squares = {i:"{} squared is {}".format(i, i**2) for i in range(10)}
>>> squares[8]
'8 squared is 64'
在列表推导中,for前面只有一个表达式,而在字典推导中,for前面有两个用冒号分隔的表达式。这两个表达式分别为键及其对应的值。
5.7 三人行
结束本章前,大致介绍一下另外三条语句:pass、del和exec。
5.7.1 什么都不做
pass语句什么都不会发生。在编写代码时,可将其用作占位符。例如,我们可能编写了一条if语句并想尝试运行它,但其中可能缺少一个代码块,这些代码将不能运行,因为在Python中代码块不能为空。要修复这个问题,只需要在中间的代码块中添加一条pass语句即可。
if name == 'Ralph Auldus Melish':
print('Welcome!')
elif name == 'Enid':
#还未完成……
pass
elif name == 'Bill Gates':
print('Access Denied')
#注:也可不使用注释和pass语句,而是插入一个字符串。这种做法尤其适用于未完成的函数(第六章)和类(第七章),因为这种字符串将充当文档字符串(第六章)。
5.7.2 使用del删除
对于我们不再使用的对象,Python通常会将其删除(因为没有任何变量或数据结构成员指向它),这被称为垃圾收集。
另一种办法是使用del语句。(第二章和第四章使用这条语句来删除序列和字典)这不仅会删除到对象的引用,还会删除名称本身。
>>> x = 1
>>> del x
>>> x
Traceback (most recent call last):
File "<pyshell#27>", line 1, in <module>
x
NameError: name 'x' is not defined
#这看似简单,但有时不太好理解。
#例如,在下面的示例中,x和y指向同一个列表:
>>> x = ["Hello", "world"]
>>> y = x
>>> y[1] = "Python"
>>> x
['Hello', 'Python']
可能我们通常会认为通过删除x,也将删除y,但并非如此:
>>> del x
>>> y
['Hello', 'Python']
#这是因为x和y指向同一个列表,但删除x对y没有任何影响,因为我们只删除名称x,而没有删除列表本身(值)。事实上,在Python中,根本就没有办法删除值,而我们也不需要这样做,因为对于我们不再使用的值,Python解释器会立即将其删除。
5.7.3 使用exec和eval执行字符串及计算其结果
注:本节介绍如何执行储存在字符串中的Python代码,这样做可能带来严重的安全隐患。如果将部分内容由用户提供的字符串作为代码执行,将无法控制代码的行为。在网络引用程序,如第十五章将介绍的通用网关接口(CGI)脚本中,这样做尤其危险。
1. exec
函数exec将字符串作为代码执行。
>>> exec("print('Hello, world!')")
Hello, world!
然而,调用函数exec时只给它提供一个参数绝非好事。在大多数情况下,还应向它传递一个命名空间——用于放置变量的地方;否则代码将污染命名空间,即修改变量。
exec函数主要用于动态地创建代码字符串。如果这种字符串来自其他地方(可能是用户),就几乎无法确定它将包含什么内容。因此为了安全期间,要提供一个字典以充当命名空间。
注:命名空间(作用域)是个重要的概念,将在下一章讨论。目前,我们可以将命名空间视为放置变量的地方,类似于一个看不见的字典。因此,当你执行赋值语句x = 1时,将在当前命名空间存储键x和值1。当前命名空间通常是全局命名空间(到目前为止,我们使用的大都是全局命名空间),但并非必然如此。
为此,添加第二个参数——字典,用作代码字符串的命名空间。
>>> from math import sqrt
>>> scope = {}
>>> exec('sqrt = 1', scope)
>>> sqrt(4)
2.0
>>> scope['sqrt']
1
#请注意,如果尝试将scope打印出来,将发现它包含很多内容,这是因为自动在其中添加了包含所有内置函数和值的字典__builtins__。
>>> len(scope)
2
>>> scope.keys()
dict_keys(['__builtins__', 'sqrt'])
2. eval
eval是一个类似于exec的内置函数。exec执行一系列Python语句,而eval计算用字符串表示的Python表达式的值,并返回结果(exec什么都不返回,因为它本身是条语句)。例如,我们可以使用如下代码来创建一个Python计算器:
>>> eval(input("Enter an arithmetic expression: "))
Enter an arithmetic expression: 6 + 18 * 2
42
与exec一样,也可向eval提供一个命名空间,虽然表达式通常不会像语句那样给变量重新赋值。
注:虽然表达式通常不会给变量重新赋值,但绝对能够这样做,如调用给全局变量重新赋值的函数。因此,将eval用于不可信任的代码并不比使用exec安全。当前,在Python中执行不可信任的代码时,没有安全的办法。一种替代解决方案是使用Jython(第十七章)等Python实现,以实现Java沙箱等原生机制。
浅谈作用域:
#向exec或eval提供命名空间时,可在使用这个命名空间前在其中添加一些值:
>>> scope = {}
>>> scope['x'] = 2
>>> scope['y'] = 3
>>> eval('x * y', scope)
6
#同样,同一个命名空间可用于多次调用exec或eval。
>>> scope = {}
>>> exec('x = 2', scope)
>>> eval('x * x', scope)
4
#采用这种做法可编写出非常复杂的程序,但你也许不应这样做。
5.8 小结
本章介绍了多种语句。
- 打印语句:可以使用print语句来打印多个用逗号分隔的值。如果print语句以逗号结尾,后续print语句将在当前行接着打印。
- 导入语句:有时候,我们不喜欢要导入的函数的名称——可能是因为你已将这个名称用作他用。在这种情况下,可使用import…as…语句在本地重命名函数。
- 赋值语句:通过使用奇妙的序列解包和链式赋值,可同时给多个变量赋值;而通过使用增强赋值,可就地修改变量。
- 代码块:代码块用于通过缩进将语句编组。代码块可用于条件语句和循环中,还可用于函数和类的定义中。
- 条件语句:条件语句根据条件(布尔表达式)决定是否执行后续代码块。通过使用if/elif/else,可将多个条件语句组合起来。条件语句的一个变种是条件表达式,如a if b else c。
- 断言:断言断定某件事(一个布尔表达式)为真,可包含说明为何必须如此的字符串。如果指定的表达式为假,断言将导致程序停止执行。最好尽早将错误就出来,免得它潜藏在程序中,直到带来麻烦。
- 循环:可针对序列中的每个元素(如特定范围内的每个数)执行代码块,也可在条件为真时反复执行代码块。要跳出代码块中余下的代码,直接进入下一次迭代,可使用continue语句;要跳出循环,可使用break语句。另外,还可以在循环末尾添加一个else子句,它将在没有执行循环中的任何break语句时执行。
- 推导:推导并不是语句,而是表达式。它们看起来很像循环,因此我们将它们放在循环中讨论。通过列表推导,可从既有列表中创建出性列表,这是通过对列表元素调用函数、提出不想要的函数等实现的。推导功能强大,但在很多情况下,使用普通循环和条件语句也可完成任务,且代码的可读性可能更高。使用类似于列表推导的表达式可创建出字典。
- pass、del、exec和eval:pass语句什么都不做,但适合用作占位符。del语句用于删除变量或数据结构的成员,但不能用于删除值。函数exec用于将字符串作为Python程序执行。函数eval计算用字符串表示的表达式并返回结果。