第5章 条件、循环及其他语句
你已见过几种语句(print语句、import语句和赋值语句),先来看看这些语句的其他一些用法,再深入探讨条件语句和循环语句。然后,我们将介绍列表推导,它们虽然是表达式,但工作原理几乎与条件语句和循环语句相同。最后,我们将介绍pass、del和exec。
1、再谈print 和import
虽然print现在实际上是一个函数,但以前却是一种语句,因此在这里进行讨论。
提示 对很多应用程序来说,使用模块logging来写入日志比使用print更合适,详情请参阅第19章。
1.1 打印多个参数
你知道,print可用于打印一个表达式,这个表达式要么是字符串,要么将自动转换为字符串。但实际上,你可同时打印多个表达式,条件是用逗号分隔它们:
>>> print('Age:', 42)
Age: 42
你还可自定义结束字符串,以替换默认的换行符。例如,如果将结束字符串指定为空字符串,以后就可继续打印到当前行。
print('Hello,', end='')
print('world!')
上述代码打印Hello, world!。
1.2 导入时重命名
如果有两个模块,它们都包含函数open,该如何办呢?你可使用第一种方式导入这两个模块,并像下面这样调用函数:
import somemodule
module1.open(...)
module2.open(...)
但还有一种办法:在语句末尾添加as子句并指定别名。下面是一个导入整个模块并给它指定别名的例子:
>>> import math as foobar
>>> foobar.sqrt(4)
2.0
2、赋值魔法
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将引发异常。可使用星号运算符(*)来收集多余的值,这样无需确保值和变量的个数相同,如下例所示:
>>> a, b, *rest = [1, 2, 3, 4]
>>> rest
[3, 4]
还可将带星号的变量放在其他位置。赋值语句的右边可以是任何类型的序列,但带星号的变量最终包含的总是一个列表。在变量和值的个数相同时亦如此。
>>> a, *b, c = "abc"
>>> a, b, c
('a', ['b'], 'c')
2.2 链式赋值
链式赋值是一种快捷方式,用于将多个变量关联到同一个值。这有点像前一节介绍的并行赋值,但只涉及一个值:
x = y = somefunction()
上述代码与下面的代码等价:
y = somefunction()
x = y
请注意,这两条语句可能与下面的语句不等价:
x = somefunction()
y = somefunction()
2.3 增强赋值
可以不编写代码x = x + 1,而将右边表达式中的运算符(这里是+)移到赋值运算符(=)的前面,从而写成x += 1。这称为增强赋值,适用于所有标准运算符,如*、/、%等。
>>> x = 2
>>> x += 1
>>> x *= 2
>>> x
6
增强赋值也可用于其他数据类型(只要使用的双目运算符可用于这些数据类型)。通过使用增强赋值,可让代码更紧凑、更简洁,同时在很多情况下的可读性更强。
>>> fnord = 'foo'
>>> fnord += 'bar'
>>> fnord *= 2
>>> fnord
'foobarfoobar'
3、代码块:缩进的乐趣
代码块是一组语句,可在满足条件时执行(if语句),可执行多次(循环),等等。代码块是通过缩进代码(即在前面加空格)来创建的。
注意 也可使用制表符来缩进代码块。Python将制表符解释为移到下一个制表位(相邻制表位相距8个空格),但标准(也是更佳的)做法是只使用空格(而不使用制表符)来缩进,且每级缩进4个空格。
在Python中,使用冒号(:)指出接下来是一个代码块,并将该代码块中的每行代码都缩进相同的程度。发现缩进量与之前相同时,你就知道当前代码块到此结束了。
4、条件和条件语句
4.1 这正是布尔值的用武之地
在本书前面,你多次遇到了真值,现在终于需要用到它们了。真值也称布尔值,是以在真值方面做出了巨大贡献的George Boole命名的。用作布尔表达式(如用作if语句中的条件)时,下面的值都将被解释器视为假:
False None 0 "" () [] {}
布尔值True和False属于类型bool,而bool与list、str和tuple一样,可用来转换其他的值。鉴于任何值都可用作布尔值,因此你几乎不需要显式地进行转换(Python会自动转换)。
注意 虽然[]和"“都为假(即bool([]) == bool(”") == False),但它们并不相等(即[] != “”)。对其他各种为假的对象来说,情况亦如此(一个更显而易见的例子是() != False)。
4.2 有条件地执行和if 语句
真值可合并,至于如何合并稍后再讲,先来看看真值可用来做什么。请尝试运行下面的脚本:
name = input('What is your name? ')
if name.endswith('Gumby'):
print('Hello, Mr. Gumby')
这就是if语句,让你能够有条件地执行代码。这意味着如果条件(if和冒号之间的表达式)为前面定义的真,就执行后续代码块(这里是一条print语句);如果条件为假,就不执行(你应该猜到了)。
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语句很像的“亲戚”,它就是条件表达式——C语言中三目运算符的Python版本。下面的表达式使用if和else确定其值:
status = "friend" if name.endswith("Gumby") else "stranger"
如果条件(紧跟在if后面)为真,表达式的结果为提供的第一个值(这里为"friend"),否则为第二个值(这里为"stranger")
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')
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, Gumby')
else:
print('Hello, stranger')
在这里,如果名字以Gumby结尾,就同时检查名字开头,这是在第一个代码块中使用一条独立的if语句完成的。请注意,这里还使用了elif。最后一个分支(else子句)没有指定条件——如果没有选择其他分支,就选择最后一个分支。如果需要,这里的两个else子句都可省略。如果省略里面的else子句,将忽略并非以Mr.或Mrs.打头的名字(假设名字为Gumby)。如果省略外面的else子句,将忽略陌生人。
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。有些比较运算符需要特别注意,下面就来详细介绍。
-
相等运算符
要确定两个对象是否相等,可使用比较运算符,用两个等号(==)表示。
-
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指向另一个列表(其中包含的值以及这些值的排列顺序都与前一个列表相同)。这两个列表虽然相等,但并非同一个对象。
-
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 contain the letter "s".')
-
字符串和序列的比较
字符串是根据字符的字母排列顺序进行比较的。
>>> "alpha" < "beta"
True
实际上,字符是根据顺序值排列的。要获悉字母的顺序值,可使用函数ord。这个函数的作用与函数chr相反:
>>> ord("a")
97
>>> ord("b")
98
这种方法既合理又一致,但可能与你排序的方式相反。例如,涉及大写字母时,排列顺序就可能与你想要的不同。
>>> "a" < "B"
False
其他序列的比较方式与此相同,但这些序列包含的元素可能不是字符,而是其他类型的值。如果序列的元素为其他序列,将根据同样的规则对这些元素进行比较。
>>> [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 and number >= 1:
print('Great!')
else:
print('Wrong!')
运算符and是一个布尔运算符。它接受两个真值,并在这两个值都为真时返回真,否则返回假。还有另外两个布尔运算符:or和not。通过使用这三个运算符,能以任何方式组合真值。
布尔运算符有个有趣的特征:只做必要的计算。例如,仅当x和y都为真时,表达式x andy才为真。因此如果x为假,这个表达式将立即返回假,而不关心y。实际上,如果x为假,这个表达式将返回x,否则返回y。(这将提供预期的结果,你明白了其中的原理吗?)这种行为称为短路逻辑(或者延迟求值):布尔运算符常被称为逻辑运算符,如你所见,在有些情况下将“绕过”第二个值。对于运算符or,情况亦如此。在表达式x or y中,如果x为真,就返回x,否则返回y。(你明白这样做合理的原因吗?)请注意,这意味着位于布尔运算符后面的代码(如函数调用)可能根本不会执行。像下面这样的代码就利用了这种行为:
name = input('Please enter your name: ') or '<unknown>'
如果没有输入名字,上述or表达式的结果将为’’。在很多情况下,你都宁愿使用条件表达式,而不耍这样的短路花样。不过前面这样的语句确实有其用武之地。
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 "<stdin>", line 1, in ?
AssertionError
如果知道必须满足特定条件,程序才能正确地运行,可在程序中添加assert语句充当检查点,这很有帮助。还可在条件后面添加一个字符串,对断言做出说明。
>>> age = -1
>>> assert 0 < age < 100, 'The age must be realistic'
Traceback (most recent call last):
File "<stdin>", line 1, in ?
AssertionError: The age must be realistic
5、循环
5.1 while 循环
x = 1
while x <= 100:
print(x)
x += 1
打印1~100的所有数。你还可以使用循环来确保用户输入名字,如下所示:
name = ''
while not name:
name = input('Please enter your name: ')
print('Hello, {}!'.format(name))
提示 如果你只是输入一个空格字符(将其作为你的名字),结果将如何呢?试试看。程序将接受这个名字,因为包含一个空格字符的字符串不是空的,因此不会将name视为假。这无疑是这个小程序的一个瑕疵,但很容易修复:只需将while not name改为while not nameor name.isspace()或while not name.strip()即可。
5.2 for 循环
while语句非常灵活,可用于在条件为真时反复执行代码块。这在通常情况下很好,但有时候你可能想根据需要进行定制。一种这样的需求是为序列(或其他可迭代对象)中每个元素执行代码块。
注意 基本上,可迭代对象是可使用for循环进行遍历的对象。第9章将详细介绍可迭代对象和迭代器。就目前而言,只需将可迭代对象视为序列即可。
为此,可使用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。
提示 只要能够使用for循环,就不要使用while循环。
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.4 一些迭代工具
Python提供了多个可帮助迭代序列(或其他可迭代对象)的函数,其中一些位于第10章将介绍的模块itertools中,但还有一些内置函数使用起来也很方便。
1. 并行迭代
有时候,你可能想同时迭代两个序列。假设有下面两个列表:如果要打印名字和对应的年龄,可以像下面这样做:
names = ['anne', 'beth', 'george', 'damon']
ages = [12, 45, 32, 102]
for i in range(len(names)):
print(names[i], 'is', ages[i], 'years old')
i是用作循环索引的变量的标准名称。一个很有用的并行迭代工具是内置函数zip,它将两个序列“缝合”起来,并返回一个由元组组成的序列。返回值是一个适合迭代的对象,要查看其内容,可使用list将其转换为列表。
>>> 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”, key=str.lower)返回[‘a’, ‘B’, ‘c’]。
5.5 跳出循环
通常,循环会不断地执行代码块,直到条件为假或使用完序列中的所有元素。但在有些情况下,你可能想中断循环、开始新迭代(进入“下一轮”代码块执行流程)或直接结束循环。
1. break
要结束(跳出)循环,可使用break。假设你要找出小于100的最大平方值(整数与自己相乘的结果),可从100开始向下迭代。找到一个平方值后,无需再迭代,因此直接跳出循环。
from math import sqrt
for n in range(99, 0, -1):
root = sqrt(n)
if root == int(root):
print(n)
break
如果你运行这个程序,它将打印81并结束。注意到我向range传递了第三个参数——步长,即序列中相邻数的差。通过将步长设置为负数,可让range向下迭代,如上面的示例所示。
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中,for和while循环非常灵活,但偶尔遇到的一些问题可能让你禁不住想:如果这些循环的功能更强些就好了。例如,假设你要在用户根据提示输入单词时执行某种操作,并在用户没有提供单词时结束循环。为此,一种办法如下:
word = 'dummy'
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 was 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: ')
哑值消除了,但包含重复的代码(这样也不好):需要在两个地方使用相同的赋值语句并调用input。如何避免这样的重复呢?可使用成例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行将整个循环分成两部分:第一部分负责设置(如果使用常规while循环,将重复这部分),第二部分在循环条件为真时使用第一部分初始化的数据。
5.6 循环中的else 子句
通常,在循环中使用break是因为你“发现”了什么或“出现”了什么情况。要在循环提前结束时采取某种措施很容易,但有时候你可能想在循环正常结束时才采取某种措施。如何判断循环是提前结束还是正常结束的呢?一种更简单的办法是在循环中添加一条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!")
请注意,为测试else子句,我将下限改成了81(不包含)。如果你运行这个程序,它将打印"Didn’t find it!",因为正如你在前面讨论break时看到的,小于100的最大平方值为81。无论是在for循环还是while循环中,都可使用continue、break和else子句。
6、简单推导
列表推导是一种从其他列表创建列表的方式,类似于数学中的集合推导。列表推导的工作原理非常简单,有点类似于for循环。
>>> [x * x for x in range(10)]
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
这个列表由range(10)内每个值的平方组成,非常简单吧?如果只想打印那些能被3整除的平方值,该如何办呢?可使用求模运算符:如果y能被3整除,y % 3将返回0(请注意,仅当x能被3整除时,x*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部分时,也可添加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的字典,其中每项的键都是一个字母,而值为以这个字母打头的女孩名字组成的列表(字典方法setdefault在前一章介绍过)。创建这个字典后,列表推导遍历所有的男孩,并查找名字首字母与当前男孩相同的所有女孩。这样,这个列表推导就无需尝试所有的男孩和女孩组合并检查他们的名字首字母是否相同了。
使用圆括号代替方括号并不能实现元组推导,而是将创建生成器,详细信息请参阅第9章的旁注“简单生成器”。然而,可使用花括号来执行字典推导。
>>> squares = {i:"{} squared is {}".format(i, i**2) for i in range(10)}
>>> squares[8]
'8 squared is 64'
在列表推导中,for前面只有一个表达式,而在字典推导中,for前面有两个用冒号分隔的表达式。这两个表达式分别为键及其对应的值。
7、三人行
结束本章前,大致介绍一下另外三条语句:pass、del和exec。
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')
注意也可不使用注释和pass语句,而是插入一个字符串。这种做法尤其适用于未完成的函数(参见第6章)和类(参见第7章),因为这种字符串将充当文档字符串(将在第6章介绍)。
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指向同一个字典,因此将None赋给scoundrel后,依然可以通过robin来访问这个字典。但将robin也设置为None之后,这个字典就漂浮在计算机内存中,没有任何名称与之相关联,再也无法获取或使用它了。因此,智慧无穷的Python解释器直接将其删除。这被称为垃圾收集。请注意,在前面的代码中,也可将其他任何值(而不是None)赋给两个变量,这样字典也将消失。另一种办法是使用del语句。(第2章和第4章使用这条语句来删除序列和字典,还记得吗?)这不仅会删除到对象的引用,还会删除名称本身。
>>> x = 1
>>> del x
>>> x
Traceback (most recent call last):
File "<pyshell#255>", line 1, in ?
x
NameError: name 'x' is not defined
这看似简单,但有时不太好理解。例如,在下面的示例中,x和y指向同一个列表:你可能认为通过删除x,也将删除y,但情况并非如此。
>>> x = ["Hello", "world"]
>>> y = x
>>> y[1] = "Python"
>>> x
['Hello', 'Python']
>>> del x
>>> y
['Hello', 'Python']
这是为什么呢?x和y指向同一个列表,但删除x对y没有任何影响,因为你只删除名称x,而没有删除列表本身(值)。事实上,在Python中,根本就没有办法删除值,而且你也不需要这样做,因为对于你不再使用的值,Python解释器会立即将其删除。
7.3 使用exec 和eval 执行字符串及计算其结果
有时候,你可能想动态地编写Python代码,并将其作为语句进行执行或作为表达式进行计算。这可能犹如黑暗魔法,一定要小心。exec和eval现在都是函数,但exec以前是一种语句,而eval与它紧密相关。这就是我在这里讨论它们的原因所在。
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#18>", line 1, in ?
sqrt(4)
TypeError: object is not callable: 1
既然如此,为何要将字符串作为代码执行呢?函数exec主要用于动态地创建代码字符串。如果这种字符串来自其他地方(可能是用户),就几乎无法确定它将包含什么内容。因此为了安全起见,要提供一个字典以充当命名空间。
注意 命名空间(作用域)是个重要的概念,将在下一章深入讨论,但就目前而言,你可将命名空间视为放置变量的地方,类似于一个看不见的字典。因此,当你执行赋值语句x = 1时,将在当前命名空间存储键x和值1。当前命名空间通常是全局命名空间(到目前为止,我们使用的大都是全局命名空间),但并非必然如此。
为此,你添加第二个参数——字典,用作代码字符串的命名空间。如你所见,可能带来破坏的代码并非覆盖函数sqrt。函数sqrt该怎样还怎样,而通过exec执行赋值语句创建的变量位于scope中。
>>> from math import sqrt
>>> scope = {}
>>> exec('sqrt = 1', scope)
>>> sqrt(4)
2.0
>>> scope['sqrt']
1
请注意,如果你尝试将scope打印出来,将发现它包含很多内容,这是因为自动在其中添加了包含所有内置函数和值的字典__ builtins__。
>>> len(scope)
2
>>> scope.keys()
['sqrt', '__builtins__']
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提供一个命名空间,虽然表达式通常不会像语句那样给变量重新赋值。
向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
采用这种做法可编写出非常复杂的程序,但你也许不应这样做。
8、小结
本章介绍了多种语句。
- 打印语句:你可使用print语句来打印多个用逗号分隔的值。如果print语句以逗号结尾,后续print语句将在当前行接着打印。
- 导入语句:有时候,你不喜欢要导入的函数的名称——可能是因为你已将这个名称用作他用。在这种情况下,可使用import … as …语句在本地重命名函数。
- 赋值语句:通过使用奇妙的序列解包和链式赋值,可同时给多个变量赋值;而通过使用增强赋值,可就地修改变量。
- 代码块:代码块用于通过缩进将语句编组。代码块可用于条件语句和循环中,还可用于函数和类定义中(这将在本书后面介绍)。
- 条件语句:条件语句根据条件(布尔表达式)决定是否执行后续代码块。通过使用if/elif/else,可将多个条件语句组合起来。条件语句的一个变种是条件表达式,如a if b else c。
- 断言:断言断定某件事(一个布尔表达式)为真,可包含说明为何必须如此的字符串。如果指定的表达式为假,断言将导致程序停止执行(或引发第8章将介绍的异常)。最好尽早将错误揪出来,免得它潜藏在程序中,直到带来麻烦。
- 循环:你可针对序列中的每个元素(如特定范围内的每个数)执行代码块,也可在条件为真时反复执行代码块。要跳过代码块中余下的代码,直接进入下一次迭代,可使用continue语句;要跳出循环,可使用break语句。另外,你还可在循环末尾添加一个else子句,它将在没有执行循环中的任何break语句时执行。
- 推导:推导并不是语句,而是表达式。它们看起来很像循环,因此我将它们放在循环中讨论。通过列表推导,可从既有列表创建出新列表,这是通过对列表元素调用函数、剔除不想要的函数等实现的。推导功能强大,但在很多情况下,使用普通循环和条件语句也可完成任务,且代码的可读性可能更高。使用类似于列表推导的表达式可创建出字典。
- pass、del、exec和eval:pass语句什么都不做,但适合用作占位符。del语句用于删除变量或数据结构的成员,但不能用于删除值。函数exec用于将字符串作为Python程序执行。函数eval计算用字符串表示的表达式并返回结果。
本章介绍的新函数
函 数 | 描 述 |
---|---|
chr(n) | 返回一个字符串,其中只包含一个字符,这个字符对应于传入的顺序值n(0 ≤ n < 256) |
eval(source[,globals[,locals]]) | 计算并返回字符串表示的表达式的结果 |
exec(source[, globals[, locals]]) | 将字符串作为语句执行 |
enumerate(seq) | 生成可迭代的索引-值对 |
ord© | 接受一个只包含一个字符的字符串,并返回这个字符的顺序值(一个整数) |
range([start,] stop[, step]) | 创建一个由整数组成的列表 |
reversed(seq) | 按相反的顺序返回seq中的值,以便用于迭代 |
sorted(seq[,cmp] [,key] [,reverse]) | 返回一个列表,其中包含seq中的所有值且这些值是经过排序的 |
xrange([start,] stop[, step]) | 创建一个用于迭代的xrange对象 |
zip(seq1, seq2,…) | 创建一个适合用于并行迭代的新序列 |