第5章 条件、循环和其他语句
5.5.5 跳出循环
一般来说,循环会一致执行到条件为假,或者到序列元素用完时。但是有些时候可能会提前中断一个循环,进行新的迭代(新一“轮”的代码执行),或者仅仅就是想结束循环。
1.break
结束(跳出)循环可以使用break语句。假设需要寻找100以内的最大平方数,那么程序可以开始从100往下迭代到0.当找到一个平方数时就不需要继续循环了,所以可以跳出循环:
平方数(或称完全平方数),是指可以写成某个整数的平方的数,即其平方根为整数的数。例如,9 = 3 × 3,9是一个平方数。
from math import sqrt for n in range(99, 0, -1): root = sqrt(n) if root == int(root): print (n) break
如果执行这个程序的话,会打印出81,然后程序停止。注意,上面代码中range函数增加了第3个参数——表示步长,步长表示每对相邻数字之间的差别。将其设置为负值的话就会像例子中一样反向迭代。它也可以用来跳过数字。
2.continue
continue语句比break语句用得要少得多。它会让当前的迭代结束,“跳”到下一轮循环的开始。它最基本的意思是“跳过剩余的循环体,但是不结束循环”。当循环体很大而且很复杂的时候,这会很有用,有些时候因为一些原因可能会跳过它——这个时候可以使用continue语句:
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习语
Python种的while和for循环非常灵活,但一旦使用while语句就会遇到一个需要更多功能的问题。如果需要当用户在提示符下输入单词时做一些事情,并且在用户不输入单词后结束循环。可以使用下面的方法:
word = 'dummy' while word: word = input('Please enter a word:') # 处理word: print ('The word was '+ word)
下面是一个会话示例:
Please enter a word:first
The word was first
Please enter a word:second
The word was second
Please enter a word:
代码按要求的方式工作,但是代码有些丑。在进入循环之前需要给word赋一个哑(未使用的)值。使用哑值(dummy value)就是工作没有尽善尽美的标志。让我们试着避免它:
word = raw_input('Please input a word: ') while word: # 处理word: print 'The word was ' + word word = raw_input('Please input a word: ')
哑值不见了。但是有重复的代码:要用一样的赋值语句在两个地方两次调用raw_input。能否不这么做呢?可以使用while True/break语句:
while True: word = raw_input('Please input a word: ') if not word: break # 处理word: print 'The word was ' + word
while True的部分实现了一个永远不会自己停止的循环。但是在循环内部的if语句中加入条件可以的,在条件满足时调用break语句。这样一来就可以在循环内部任何地方而不是只在开头终止循环。if/break语句自然地将循环分为两部分:第1部分负责初始化(在普通的while循环中,这部分需要重复),第2部分则在循环条件为真的情况下使用第1部分内初始化好的数据。
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 don't break out!"
更简单的方式是在循环中增加一个else子句——它仅在没有调用break时执行。让我们用这个方法重写刚才的例子:
from math import sqrt for n in range(99, 81, -1): root = sqrt(n) if root == int(root): print(n) break else: print("Didn't find it!")
注意我将下限改为81(不包括81)以测试else自居。如果执行程序的话,它会打印出“Didn't find it!”,因为100以内最大的平方数是81。for和while循环中都可以使用continue、break语句和else子句。
5.6列表推导式——轻量级循环
列表推导式(list comprehension)是利用其他列表创建新列表的一种方法。它的工作方式类似于for循环。
>>> [x*x for x in range(10)] [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
列表由range(10)中每个x的平方组成。太容易了?如果只想打印出那些能被3整除的平方数呢?那么可以使用模除运算符——y%3,当数字可以被3整除时返回0(注意,x能被3整除时,x的平方必然也可以被3整除)。这个语句可以通过增加一个if部分添加到列表推导式中:
>>> [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)) >>> print(result) [(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2), (2, 0), (2, 1), (2, 2)]
也可以和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']
这样就得到了那些名字首字母相同的男孩和女孩。
更优方案
男孩/女孩名字对的例子其实效率不高,因为它会检查每个可能的配对。
>>> 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]]] ['chris+clarice', 'arnold+alice', 'bob+bernice']
这个程序建造了一个叫做letterGirl的字典,其中每一项都把单字母作为键,以女孩名字组成的列表作为值。在字典建立之后,列表推导式循环整个男孩集合,并且查找那些和当前男孩名字首字母相同的女孩集合。这样列表推导式就不用尝试所有的男孩女孩的组合,检查首字母是否匹配。
5.7三人行
5.7.1什么都没发生
有的时候,程序什么事情都不用做。这种情况不多,但是一旦出现,就应该让pass语句出马了:
>>> pass
>>>
似乎没什么动静。
那么究竟为什么使用一个什么都不做的语句?它可以在的代码中做占位符使用。比如程序需要一个if语句,然后进行测试,但是缺少其中一个语句块的代码,考虑下面的情况:
if name == 'Ralph Auldus Melish': print('Welcome!') elif name == 'Enid': # 还没完…… elif name == 'Bill Gates': print('Access Denied')
代码不会执行,因为Python中空代码块是非法的。解决方法就是在语句块中加上一个pass语句:
if name == 'Ralph Auldus Melish': print('Welcome!') elif name == 'Enid': # 还没完…… pass elif name == 'Bill Gates': print('Access Denied')
5.7.2使用del删除
一般来说,Python会删除那些不再使用的对象(因为使用者不会再通过任何变量或者数据结构引用它们):
>>> scoundrel = {'age': 42, 'first name': 'Robin', 'last name': 'of Locksley'}
>>> robin = scoundrel
>>> scoundrel
{'age': 42, 'first name': 'Robin', 'last name': 'of Locksley'}
>>> robin
{'age': 42, 'first name': 'Robin', 'last name': 'of Locksley'}
>>> scoundrel = None
>>> robin
{'age': 42, 'first name': 'Robin', 'last name': 'of Locksley'}
>>> robin = None
首先,robin和scoundrel都被绑定到同一个字典上。所以当设置scoundrel为None的时候,字典通过robin还是可用的。但是当我把robin也设置为None的时候,字典就“漂”在内存里面了,没有任何名字绑定到它上面。没有办法获取和使用它,所以Python解释器直接删除了那个字典(这种行为被称为垃圾收集)。注意,也可以使用None之外的其他值。字典同样会“消失不见”。
另外一个方法就是使用del语句,它不仅会移除一个对象的引用,也会移除那个名字本身。
>>> x = 1 >>> del x >>> x Traceback (most recent call last): File "<pyshell#48>", 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。原因就是删除的只是名称,而不是列表本身(值)。事实上,在Python中是没有办法删除值的(也不需要过多考虑删除值的问题,因为在某个值不再使用的时候,Python解释器会负责内存的回收)。
5.7.3使用exec和eval执行和求值字符串
有些时候可能会需要动态地创造Python代码,然后将其作为语句执行或作为表达式计算,这可能近似于“黑暗魔法”——在此之前,一定要慎之又慎,仔细考虑。
警告:本节中,会学到如何执行存储在字符串中的Python代码。这样做会有很严重的潜在安全漏洞。如果程序将用户提供的一段内容中的一部分字符串作为代码执行,程序可能会时去对代码执行的控制,这在情况在网络应用程序——比如CGI脚本中尤其危险。
1.exec
执行一个字符串的语句是exec:
>>> exec("print('Hello, world!')") Hello, world!
但是,使用简单形式的exec语句绝不是好事。很多情况下可以给它提供命名空间——可以放置变量的地方。你想这样做,从而使代码不会干扰命名空间(也就是改变你的变量),比如,下面的代码中使用了名称sqrt:
>>> from math import sqrt >>> exec("sqrt = 1") >>> sqrt(4) Traceback (most recent call last): File "<pyshell#60>", line 1, in <module> sqrt(4) TypeError: 'int' object is not callable
想想看,为什么一开始我们要这样做?exec语句最有用的地方在于可以动态地创建代码字符串。如果字符串是从其他地方获得的——很有可能是用户——那么几乎不能确定其中到底包含上面代码。所以为了安全起见,可以增加一个字典,起到命名空间的作用。
注意:命名空间的概念,或称为作用域(scope)是非常重要的知识。所以在程序执行x=1这类赋值语句时,就将x和值1放在当前的命名空间内,这个命名空间一般来说都是全局命名空间。
可以通过增加in<scope>来实现,其中的<scope>就是起到放置代码字符串命名空间作用的字典。
2.eval
eval(用于“求值”)是类似于exec的内建函数。exec语句会执行一系列Python语句,而eval会计算Python表达式(以字符串形式书写),并且返回结果值。(exec语句并不返回任何对象,因为它本身就是语句。)例如,可以使用下面的代码创建一个Python计算器:
>>> eval(input('Enger an arithmetic expression: ')) Enger an arithmetic expression: 6 + 18 * 2 42
跟exec一样,eval也可以使用命名空间。尽管表达式几乎不像语句那样为变量重新赋值。(事实上,可以给eval语句提供两个命名空间,一个全局的一个局部的。全局的必须是字典,局部的可以是任何形式的映射。)
警告:尽管表达式一般不给变量重新赋值,但它们的确可以(比如可以调用函数给全局变量重新赋值)。所以使用eval语句对付一些不可信任的代码并不比exec语句安全。目前,在Python内没有任何执行部可信任代码的安全方式。一个可选的方案是使用Python的实现,比如Jython,以及使用一些本地机制,比如Java的sandbox功能。

浙公网安备 33010602011771号