第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功能。


 

posted @ 2019-08-25 22:35  susanhonly  阅读(170)  评论(0)    收藏  举报