Python 入门指南:从新手到大师(二)
三、使用字符串
你以前见过绳子,知道怎么做。您还了解了如何通过索引和切片来访问它们的单个字符。在本章中,您将看到如何使用它们来格式化其他值(例如,用于打印),并快速浏览一下使用字符串方法可以做的有用的事情,例如拆分、连接、搜索等等。
基本字符串操作
所有标准的序列操作(索引、切片、乘法、成员、长度、最小值和最大值)都与字符串有关,正如你在前一章所看到的。但是,请记住,字符串是不可变的,因此所有类型的项或片赋值都是非法的。
>>> website = 'http://www.python.org'
>>> website[-3:] = 'com'
Traceback (most recent call last):
File "<pyshell#19>", line 1, in ?
website[-3:] = 'com'
TypeError: object doesn't support slice assignment
字符串格式:短版本
如果您是 Python 编程的新手,您可能不需要 Python 字符串格式中所有可用的选项,所以我在这里给您一个简短的版本。如果您对细节感兴趣,可以看看下面的“字符串格式:长版本”一节。否则,只需阅读本文并跳到“字符串方法”一节
将值格式化为字符串是一项非常重要的操作,而且必须满足如此多样化的需求,因此多年来,这种语言中已经加入了多种方法。历史上,主要的解决方案是使用(恰当命名的)字符串格式化操作符,百分号。这个操作符的行为模拟了 C 语言中经典的printf函数。在%的左边,放置一个字符串(格式字符串);在它的右边,放置要格式化的值。你可以使用单个值,比如一个字符串或一个数字,你可以使用一组值(如果你想格式化多个值),或者,正如我在下一章讨论的,你可以使用字典。最常见的情况是元组。
>>> format = "Hello, %s. %s enough for ya?"
>>> values = ('world', 'Hot')
>>> format % values
'Hello, world. Hot enough for ya?'
格式字符串的%s部分称为转换说明符。它们标记了要插入值的位置。s意味着这些值应该像字符串一样被格式化;如果不是,他们将被转换为str。其他说明符导致其他形式的转换;例如,%.3f会将值格式化为具有三位小数的浮点数。
这种格式化方法仍然有效,并且在很多代码中仍然非常活跃,所以您可能会遇到它。您可能会遇到的另一种解决方案是所谓的模板字符串,这是不久前出现的一种简化基本格式化机制的尝试,例如,使用类似于 UNIX shells 的语法。
>>> from string import Template
>>> tmpl = Template("Hello, $who! $what enough for ya?")
>>> tmpl.substitute(who="Mars", what="Dusty")
'Hello, Mars! Dusty enough for ya?'
带等号的参数是所谓的关键字参数——你会在第六章中听到很多。在字符串格式化的上下文中,您可以将它们看作是向命名替换字段提供值的一种方式。
当编写新代码时,选择的机制是format string 方法,它结合并扩展了早期方法的优点。每个替换字段都用花括号括起来,可能包含一个名称,以及有关如何转换和格式化为该字段提供的值的信息。
最简单的情况是字段没有名称,或者每个名称只是一个索引。
>>> "{}, {} and {}".format("first", "second", "third")
'first, second and third'
>>> "{0}, {1} and {2}".format("first", "second", "third")
'first, second and third'
不过,索引不需要像这样排序。
>>> "{3} {0} {2} {1} {3} {0}".format("be", "not", "or", "to")
'to be or not to be'
命名字段正如预期的那样工作。
>>> from math import pi
>>> "{name} is approximately {value:.2f}.".format(value=pi, name="π")
'π is approximately 3.14.'
当然,关键字参数的顺序并不重要。在这种情况下,我还提供了一个格式说明符.2f,用冒号与字段名隔开,这意味着我们想要三位小数的浮点格式。如果没有指定,结果将如下所示:
>>> "{name} is approximately {value}.".format(value=pi, name="π")
'π is approximately 3.141592653589793.'
最后,在 Python 3.6 中,如果有与相应替换字段同名的变量,可以使用一种快捷方式。在这种情况下,您可以使用所谓的 f 字符串,以前缀f编写。
>>> from math import e
>>> f"Euler's constant is roughly {e}."
"Euler's constant is roughly 2.718281828459045."
在这里,名为e的替换字段只是在构造字符串时提取同名变量的值。这相当于以下稍微更明确的表达式:
>>> "Euler's constant is roughly {e}.".format(e=e)
"Euler's constant is roughly 2.718281828459045."
字符串格式:长版本
字符串格式化工具是广泛的,所以即使这个长版本也不能完全探究它的所有细节,但是让我们看看主要的组件。想法是我们在一个字符串上调用format方法,为它提供我们想要格式化的值。该字符串包含如何执行这种格式化的信息,用模板小型语言指定。每个值都被拼接到几个替换字段之一的字符串中,每个替换字段都用花括号括起来。如果希望在最终结果中包含文字大括号,可以在格式字符串中使用双大括号来指定,即{{或}}。
>>> "{{ceci n'est pas une replacement field}}".format()
"{ceci n'est pas une replacement field}"
格式字符串最令人兴奋的部分是在替换字段的内部,由以下部分组成,所有这些部分都是可选的:
- 字段名。索引或标识符。这告诉我们哪个值将被格式化并拼接到这个特定的字段中。除了命名对象本身,我们还可以命名值的特定部分,比如一个列表的元素。
- 转换标志。感叹号,后跟一个字符。目前支持的有
r(代表repr)s(代表str)或者a(代表ascii)。如果提供了此标志,它将覆盖对象自身的格式化机制,并在进一步格式化之前使用指定的函数将其转换为字符串。 - 格式说明符。冒号,后跟格式规范小型语言中的表达式。这让我们可以指定最终格式的细节,包括格式的类型(例如,字符串、浮点或十六进制数)、字段的宽度和数字的精度、如何显示符号和千位分隔符,以及各种形式的对齐和填充。
让我们更详细地看看其中的一些元素。
替换字段名称
在最简单的情况下,您只需向format提供未命名的参数,并在格式字符串中使用未命名的字段。然后,字段和参数按照给定的顺序配对。您还可以为参数提供名称,然后在替换字段中使用名称来请求这些特定的值。这两种策略可以自由混合。
>>> "{foo} {} {bar} {}".format(1, 2, bar=4, foo=3)
'3 1 4 2'
未命名参数的索引也可用于无序请求它们。
>>> "{foo} {1} {bar} {0}".format(1, 2, bar=4, foo=3)
'3 2 4 1'
然而,混合手动和自动字段编号是不允许的,因为这样会很快变得非常混乱。
但是您不必使用提供的值本身——您可以访问它们的一部分,就像在普通的 Python 代码中一样。这里有一个例子:
>>> fullname = ["Alfred", "Smoketoomuch"]
>>> "Mr {name[1]}".format(name=fullname)
'Mr Smoketoomuch'
>>> import math
>>> tmpl = "The {mod.__name__} module defines the value {mod.pi} for π"
>>> tmpl.format(mod=math)
'The math module defines the value 3.141592653589793 for π'
如您所见,我们可以对导入模块中的方法、属性或变量以及函数同时使用索引和点符号。(奇怪的__name__变量包含给定模块的名称。)
基本转换
一旦指定了字段应该包含的内容,就可以添加如何设置格式的说明。首先,您可以提供一个转换标志。
>>> print("{pi!s} {pi!r} {pi!a}".format(pi="π"))
π 'π' '\u03c0'
三个标志(s、r和a)分别使用str、repr和ascii进行转换。str函数通常创建一个看起来自然的字符串版本的值(在这种情况下,它对输入字符串不做任何事情);repr字符串试图创建给定值的 Python 表示(在本例中是一个字符串文字),而ascii函数坚持创建一个仅包含 ASCII 编码中允许的字符的表示。这类似于repr在 Python 2 中的工作方式。
您还可以指定要转换的值的类型,或者更确切地说,您希望它被视为哪种类型的值。例如,您可能提供了一个整数,但希望将其视为十进制数。您可以通过在格式规范中使用f字符(表示定点)来实现这一点,即在冒号分隔符之后。
>>> "The number is {num}".format(num=42)
'The number is 42'
>>> "The number is {num:f}".format(num=42)
'The number is 42.000000'
或者你更愿意把它格式化成二进制数字?
>>> "The number is {num:b}".format(num=42)
'The number is 101010'
有几个这样的类型说明符。列表见表 3-1 。
表 3-1。
String Formatting Type Specifiers
| 类型 | 意义 | | --- | --- | | `b` | 将整数格式化为二进制数字。 | | `c` | 将整数解释为 Unicode 码位。 | | `d` | 将整数格式化为十进制数字。整数的默认值。 | | `e` | 用`e`格式化科学记数法中的十进制数以指示指数。 | | `E` | 与`e`相同,但使用`E`表示指数。 | | `f` | 格式化具有固定小数位数的十进制数。 | | `F` | 与`f`相同,但将特殊值(`nan`和`inf`)格式化为大写。 | | `g` | 自动在固定记数法和科学记数法之间选择。十进制数的默认值,只是默认版本至少有一个小数。 | | `G` | 与`g`相同,但大写的是指数指示器和特殊值。 | | `n` | 与`g`相同,但插入了与地区相关的数字分隔符。 | | `o` | 将整数格式化为八进制数字。 | | `s` | 按原样格式化字符串。字符串的默认值。 | | `x` | 将整数格式化为带有小写字母的十六进制数字。 | | `X` | 与`x`相同,但有大写字母。 | | `%` | 将数字格式化为百分比(乘以 100,由`f`格式化,后跟`%`)。 |宽度、精度和千位分隔符
当格式化浮点数(或其他更特殊的十进制数类型)时,默认是在小数点后显示六位数字,并且在所有情况下,默认是让格式化值正好具有显示它所需的宽度,没有任何类型的填充。当然,这些缺省值可能并不完全是您想要的,您可以根据自己的喜好,在格式规范中增加关于宽度和精度的细节。
宽度由整数表示,如下所示:
>>> "{num:10}".format(num=3)
' 3'
>>> "{name:10}".format(name="Bob")
'Bob '
如你所见,数字和字符串的对齐方式不同。我们将在下一节回到对齐。
精度也是由整数指定的,但是它前面有一个句点,暗指小数点。
>>> "Pi day is {pi:.2f}".format(pi=pi)
'Pi day is 3.14'
这里,我已经明确指定了f类型,因为默认情况下处理精度的方式有点不同。(请参阅 Python 库参考以了解精确的规则。)当然可以把宽度和精度结合起来。
>>> "{pi:10.2f}".format(pi=pi)
' 3.14'
您实际上也可以对其他类型使用 precision,尽管您可能不经常需要这样做。
>>> "{:.5}".format("Guido van Rossum")
'Guido'
最后,您可以使用逗号来表示您想要千位分隔符。
>>> 'One googol is {:,}'.format(10**100)
'One googol is 10,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000'
当与其他格式元素一起使用时,此逗号应该位于表示精度的宽度和句点之间。 1
符号、对齐和零填充
相当多的格式化机器是针对格式化数字的,例如,打印出一个精确对齐的数值表。宽度和精度使我们基本上达到了目的,但是如果我们包含负数,我们漂亮的输出仍然会被丢弃。正如你所看到的,字符串和数字的对齐方式不同;也许我们想改变这一点,例如,在一列数字中间加入一段文字?在宽度和精度数字之前,您可以放置一个“标志”,它可以是零、加号、减号或空白。零意味着该数字将被零填充。
>>> '{:010.2f}'.format(pi)
'0000003.14'
分别用<、>和^指定左对齐、右对齐和居中对齐。
>>> print('{0:<10.2f}\n{0:¹⁰.2f}\n{0:>10.2f}'.format(pi))
3.14
3.14
3.14
您可以用填充字符来增加对齐说明符,填充字符用来代替空格字符。
>>> "{:$¹⁵}".format(" WIN BIG ")
'$$$ WIN BIG $$$'
还有更专业的说明符=,它在符号和数字之间放置任何填充字符。
>>> print('{0:10.2f}\n{1:10.2f}'.format(pi, -pi))
3.14
-3.14
>>> print('{0:10.2f}\n{1:=10.2f}'.format(pi, -pi))
3.14
- 3.14
如果还想包含正数的符号,可以使用说明符+(在对齐说明符之后,如果有的话),而不是默认的-。如果使用空格字符,正数将插入一个空格,而不是一个+。
>>> print('{0:-.2}\n{1:-.2}'.format(pi, -pi)) # Default
3.1
-3.1
>>> print('{0:+.2}\n{1:+.2}'.format(pi, -pi))
+3.1
-3.1
>>> print('{0: .2}\n{1: .2}'.format(pi, -pi))
3.1
-3.1
最后一个组件是 hash ( #)选项,它位于符号和宽度之间(如果它们存在的话)。这触发了另一种形式的转换,不同类型之间的细节有所不同。例如,对于二进制、八进制和十六进制转换,会添加一个前缀。
>>> "{:b}".format(42)
'101010'
>>> "{:#b}".format(42)
'0b101010'
对于各种类型的十进制数,它强制包含小数点(对于g,它保留十进制零)。
>>> "{:g}".format(42)
'42'
>>> "{:#g}".format(42)
'42.0000'
在清单 3-1 所示的例子中,我在相同的字符串上使用了两次字符串格式——第一次是将字段宽度插入到最终的格式说明符中。因为这些信息是由用户提供的,所以我不能硬编码字段宽度。
# Print a formatted price list with a given width
width = int(input('Please enter width: '))
price_width = 10
item_width = width - price_width
header_fmt = '{{:{}}}{{:>{}}}'.format(item_width, price_width)
fmt = '{{:{}}}{{:>{}.2f}}'.format(item_width, price_width)
print('=' * width)
print(header_fmt.format('Item', 'Price'))
print('-' * width)
print(fmt.format('Apples', 0.4))
print(fmt.format('Pears', 0.5))
print(fmt.format('Cantaloupes', 1.92))
print(fmt.format('Dried Apricots (16 oz.)', 8))
print(fmt.format('Prunes (4 lbs.)', 12))
print('=' * width)
Listing 3-1.String Formatting Example
以下是该程序的运行示例:
Please enter width: 35
===================================
Item Price
-----------------------------------
Apples 0.40
Pears 0.50
Cantaloupes 1.92
Dried Apricots (16 oz.) 8.00
Prunes (4 lbs.) 12.00
===================================
字符串方法
您已经在列表中遇到了方法。字符串有更丰富的方法集,部分原因是字符串从string模块“继承”了它们的许多方法,在 Python 的早期版本中它们是作为函数存在的(如果你觉得有必要,你仍然可以在那里找到它们)。
因为有这么多的字符串方法,所以这里只描述一些最有用的方法。有关完整的参考,请参见附录 b。在字符串方法的描述中,您可以在本章(标有“另请参阅”)或附录 b 中找到对其他相关字符串方法的参考。
But String Isn’t Dead
尽管字符串方法已经完全抢了string模块的风头,但该模块仍然包含一个few常量和函数,它们不能作为字符串方法使用。以下是string 2 中一些有用的常数:
string.digits:包含数字 0-9 的字符串string.ascii_letters:包含所有 ASCII 字母(大写和小写)的字符串string.ascii_lowercase:包含所有小写 ASCII 字母的字符串string.printable:包含所有可打印 ASCII 字符的字符串string.punctuation:包含所有 ASCII 标点字符的字符串string.ascii_uppercase:包含所有大写 ASCII 字母的字符串
尽管显式地处理 ASCII 字符,但这些值实际上是(未编码的)Unicode 字符串。
中心
center 方法通过用给定的填充字符(默认情况下是空格)填充字符串的任意一侧来使字符串居中。
>>> "The Middle by Jimmy Eat World".center(39)
' The Middle by Jimmy Eat World '
>>> "The Middle by Jimmy Eat World".center(39, "*")
'*****The Middle by Jimmy Eat World*****'
附录 B 中:ljust、rjust、zfill。
发现
find方法在一个更大的字符串中查找子字符串。它返回找到子字符串的最左边的索引。如果没有找到,则返回-1。
>>> 'With a moo-moo here, and a moo-moo there'.find('moo')
7
>>> title = "Monty Python's Flying Circus"
>>> title.find('Monty')
0
>>> title.find('Python')
6
>>> title.find('Flying')
15
>>> title.find('Zirquss')
-1
在第二章中,我们第一次遇到会员,我们使用表达式'$$$' in subject创建了垃圾邮件过滤器的一部分。我们也可以使用find(在 Python 2.3 之前也可以使用,当时in只能在检查字符串中的单个字符成员时使用)。
>>> subject = '$$$ Get rich now!!! $$$'
>>> subject.find('$$$')
0
Note
字符串方法find不返回布尔值。如果find像这里一样返回 0,这意味着它已经在索引 0 处找到了子串。
您还可以提供搜索的起点,也可以选择结束点。
>>> subject = '$$$ Get rich now!!! $$$'
>>> subject.find('$$$')
0
>>> subject.find('$$$', 1) # Only supplying the start
20
>>> subject.find('!!!')
16
>>> subject.find('!!!', 0, 16) # Supplying start and end
-1
请注意,由起始值和终止值(第二个和第三个参数)指定的范围包括第一个索引,但不包括第二个索引。这是 Python 中的常见做法。
附录 B 中:rfind、index、rindex、count、startswith、endswith。
加入
一个很重要的字符串方法,join是split的逆。它用于连接一个序列的元素。
>>> seq = [1, 2, 3, 4, 5]
>>> sep = '+'
>>> sep.join(seq) # Trying to join a list of numbers
Traceback (most recent call last):
File "<stdin>", line 1, in ?
TypeError: sequence item 0: expected string, int found
>>> seq = ['1', '2', '3', '4', '5']
>>> sep.join(seq) # Joining a list of strings
'1+2+3+4+5'
>>> dirs = '', 'usr', 'bin', 'env'
>>> '/'.join(dirs)
'/usr/bin/env'
>>> print('C:' + '\\'.join(dirs))
C:\usr\bin\env
如您所见,要连接的序列元素必须都是字符串。请注意,在最后两个例子中,我使用了一个目录列表,并简单地通过使用不同的分隔符(并在 DOS 版本中添加一个驱动器名)根据 UNIX 和 DOS/Windows 的约定对它们进行了格式化。
又见:split。
降低
lower方法返回字符串的小写版本。
>>> 'Trondheim Hammer Dance'.lower()
'trondheim hammer dance'
如果您想要编写不区分大小写的代码,即忽略大写字母和小写字母之间差异的代码,这可能会很有用。例如,假设您想检查一个用户名是否在列表中。如果你的列表包含字符串'gumby'并且用户输入他的名字为'Gumby',你将找不到它。
>>> if 'Gumby' in ['gumby', 'smith', 'jones']: print('Found it!')
...
>>>
当然,如果你存储了'Gumby',用户写了'gumby',甚至是'GUMBY',也会发生同样的事情。解决这个问题的方法是在存储和搜索时将所有的名字都转换成小写。代码看起来会像这样:
>>> name = 'Gumby'
>>> names = ['gumby', 'smith', 'jones']
>>> if name.lower() in names: print('Found it!')
...
Found it!
>>>
另见:islower、istitle、isupper、translate。
附录 B 中:capitalize、casefold、swapcase、title、upper。
Title Casing
与lower相关的一个是title方法(参见附录 B ),它的标题是一个字符串——也就是说,所有单词都以大写字符开头,其他所有字符都是小写。然而,单词边界的定义方式可能会产生一些不自然的结果。
>>> "that's all folks".title()
"That'S All, Folks"
另一种选择是来自string模块的capwords函数。
>>> import string
>>> string.capwords("that's all, folks")
That's All, Folks"
当然,如果你想要一个真正大写正确的标题(这取决于你使用的风格——可能是小写冠词、并列连词、少于五个字母的介词等等),你基本上只能靠自己了。
替换
replace方法返回一个字符串,其中一个字符串的所有出现被另一个替换。
>>> 'This is a test'.replace('is', 'eez')
'Theez eez a test'
如果你曾经使用过文字处理程序的“搜索和替换”功能,你无疑会看到这种方法的用处。
又见:translate。
附录 B 中:expandtabs。
使分离
一个非常重要的字符串方法,split是join的逆方法,用于将一个字符串拆分成一个序列。
>>> '1+2+3+4+5'.split('+')
['1', '2', '3', '4', '5']
>>> '/usr/bin/env'.split('/')
['', 'usr', 'bin', 'env']
>>> 'Using the default'.split()
['Using', 'the', 'default']
请注意,如果没有提供分隔符,默认情况下将对所有连续的空白字符(空格、制表符、换行符等)进行拆分。
又见:join。
附录 B 中:partition、rpartition、rsplit、splitlines。
剥夺
strip方法返回一个字符串,其中左边和右边(而不是内部)的空白已经被去除。
>>> ' internal whitespace is kept '.strip()
'internal whitespace is kept'
与lower一样,strip在比较输入值和存储值时也很有用。让我们回到关于lower一节的用户名例子,假设用户无意中在他的名字后面输入了一个空格。
>>> names = ['gumby', 'smith', 'jones']
>>> name = 'gumby '
>>> if name in names: print('Found it!')
...
>>> if name.strip() in names: print('Found it!')
...
Found it!
>>>
您还可以通过在字符串参数中列出所有字符来指定要去除的字符。
>>> '*** SPAM * for * everyone!!! ***'.strip(' *!')
'SPAM * for * everyone'
仅在末尾执行剥离,因此内部星号不会被删除。
附录 B 中:lstrip,rstrip。
翻译
与replace类似,translate替换字符串的一部分,但与replace不同,translate只处理单个字符。它的优势在于可以同时进行多种替换,而且比replace更有效率。
这种方法有相当多的技术性用法(比如翻译换行符或其他依赖于平台的特殊字符),但是让我们考虑一个更简单(虽然有点傻)的例子。假设你想把一篇普通的英语文章翻译成带有德国口音的文章。为此,您必须用 k 替换字符 c,用 z 替换 s。
但是,在使用translate之前,必须先做一个翻译表。该转换表包含关于哪些 Unicode 码位应该转换成哪些码位的信息。您使用字符串类型str本身的maketrans方法来构建这样一个表。该方法采用两个参数:两个长度相等的字符串,其中第一个字符串中的每个字符都应该由第二个字符串中相同位置的字符替换。 3 在我们这个简单的例子中,代码如下所示:
>>> table = str.maketrans('cs', 'kz')
如果我们愿意,我们可以查看表内部,尽管我们只能看到 Unicode 码位之间的映射。
>>> table
{115: 122, 99: 107}
一旦有了翻译表,就可以将它用作translate方法的参数。
>>> 'this is an incredible test'.translate(table)
'thiz iz an inkredible tezt'
可选的第三个参数可以提供给maketrans,指定应该删除的字母。例如,如果你想模仿一个语速很快的德国人,你可以删除所有的空格。
>>> table = str.maketrans('cs', 'kz', ' ')
>>> 'this is an incredible test'.translate(table)
'thizizaninkredibletezt'
又见:replace,lower。
是我的弦…
有很多以is开头的字符串方法,比如isspace、isdigit或isupper,它们决定你的字符串是否有某些属性(比如全部是空白、数字或大写),在这种情况下,这些方法返回True。否则,他们当然返回False。
附录 B 中:isalnum、isalpha、isdecimal、isdigit、isidentifier、islower、isnumeric、isprintable、isspace、istitle、isupper。
快速总结
在本章中,你看到了使用字符串的两种重要方式。
- 字符串格式化:模运算符(
%)可用于将值拼接成包含转换标志的字符串,例如%s。您可以使用它以多种方式格式化值,包括右对齐或左对齐、设置特定的字段宽度和精度、添加符号(加号或减号)或用零进行左填充。 - 字符串方法:字符串有很多方法。其中有一些极其有用(比如
split和join),而另一些使用频率较低(比如istitle或capitalize)。
本章的新功能
| 功能 | 描述 | | --- | --- | | `string.capwords(s[, sep])` | 用`split`拆分`s`(使用`sep`),将项目大写,并用一个空格连接 | | `ascii(obj)` | 构造给定对象的 ASCII 表示形式 |什么现在?
列表、字符串和字典是 Python 中三种最重要的数据类型。你已经看到了列表和字符串,猜猜接下来是什么?在下一章中,我们将看到字典不仅支持整数索引,还支持其他类型的键(如字符串或元组)。他们也有一些方法,虽然没有字符串那么多。
Footnotes 1
如果您想要一个依赖于地区的千位分隔符,您应该使用n类型。
2
有关该模块的更详细描述,请查看 Python 库参考( https://docs.python.org/3/library/string.html )的第 6.1 节。
3
你也可以提供一个字典,你将在下一章中学习到,将字符映射到其他字符,或者映射到None,如果它们要被删除的话。
四、字典:当索引不好用的时候
您已经看到,当您想要将值分组到一个结构中并通过数字引用每个值时,列表非常有用。在本章中,您将学习一种可以通过名称引用每个值的数据结构。这种类型的结构称为映射。Python 中唯一的内置映射类型是字典。字典中的值没有任何特定的顺序,而是存储在一个键下,这个键可以是一个数字、一个字符串甚至一个元组。
字典用途
名字字典应该给你一个关于这个结构用途的线索。一本普通的书是用来从头到尾阅读的。如果你愿意,你可以快速打开它到任何给定的页面。这有点像 Python 列表。另一方面,字典——无论是真正的字典还是它们的 Python 等价物——都是这样构建的,以便您可以轻松地查找特定的单词(键)来找到它的定义(值)。
在许多情况下,字典比列表更合适。以下是使用 Python 字典的一些示例:
- 表示游戏棋盘的状态,每个键是一个坐标元组
- 以文件名为关键字存储文件修改时间
- 数字电话/地址簿
假设你有一份名单。
>>> names = ['Alice', 'Beth', 'Cecil', 'Dee-Dee', 'Earl']
如果你想创建一个小数据库来存储这些人的电话号码,你会怎么做?一种方法是再列一个清单。假设您只存储了他们的四位数扩展名。然后你会得到这样的结果:
>>> numbers = ['2341', '9102', '3158', '0142', '5551']
一旦你创建了这些列表,你可以查找塞西尔的电话号码如下:
>>> numbers[names.index('Cecil')]
'3158'
有效果,但是有点不实用。您真正想要做的是类似下面这样的事情:
>>> phonebook['Cecil']
'3158'
你猜怎么着?如果phonebook是一本字典,你就可以这么做。
创建和使用词典
字典是这样写的:
phonebook = {'Alice': '2341', 'Beth': '9102', 'Cecil': '3258'}
字典由键对(称为项)和它们对应的值组成。在这个例子中,名字是键,电话号码是值。每个键用冒号(:)和它的值分开,项目用逗号分开,整个用花括号括起来。一个空字典(没有任何条目)只写了两个花括号,像这样:{}。
Note
键在一个字典中是惟一的(以及任何其他类型的映射)。值不需要在字典中是唯一的。
字典函数
您可以使用dict函数 1 从其他映射(例如,其他字典)或从(key, value)对的序列构建字典。
>>> items = [('name', 'Gumby'), ('age', 42)]
>>> d = dict(items)
>>> d
{'age': 42, 'name': 'Gumby'}
>>> d['name']
'Gumby'
它也可以与关键字参数一起使用,如下所示:
>>> d = dict(name='Gumby', age=42)
>>> d
{'age': 42, 'name': 'Gumby'}
虽然这可能是dict最有用的应用,但是您也可以将它与一个映射参数一起使用,以创建一个包含与映射相同的条目的字典。(如果不带任何参数使用,它返回一个新的空字典,就像其他类似的函数如list、tuple和str。)如果另一个映射是一个字典(毕竟这是唯一的内置映射类型),您可以使用字典方法copy来代替,如本章后面所述。
基本字典操作
字典的基本行为在许多方面反映了序列的基本行为。
len(d)返回d中项目(键值对)的数量。d[k]返回与键k相关联的值。d[k] = v将值v与键k相关联。del d[k]删除键为k的项目。k in d检查d中是否有具有关键字k的项目。
虽然字典和列表有一些共同的特征,但也有一些重要的区别:
- 键类型:字典键不一定是整数(尽管它们可能是)。它们可以是任何不可变的类型,比如浮点(实)数、字符串或元组。
- 自动添加:您可以给一个键赋值,即使这个键一开始就不在字典中;在这种情况下,将创建一个新项目。不能给列表范围之外的索引赋值(不使用
append或类似的东西)。 - 成员:表达式
k in d(其中d是一个字典)寻找一个键,而不是一个值。另一方面,表达式v in l(其中l是一个列表)寻找一个值,而不是一个索引。这可能看起来有点不一致,但当你习惯了,这其实是很自然的。毕竟,如果字典有给定的键,检查相应的值就很容易。
Tip
在字典中检查键成员比在列表中检查成员更有效。数据结构越大,差异就越大。
第一点——键可以是任何不可变的类型——是字典的主要优势。第二点也很重要。请看这里的区别:
>>> x = []
>>> x[42] = 'Foobar'
Traceback (most recent call last):
File "<stdin>", line 1, in ?
IndexError: list assignment index out of range
>>> x = {}
>>> x[42] = 'Foobar'
>>> x
{42: 'Foobar'}
首先,我尝试将字符串'Foobar'赋给空列表中的位置 42——这显然是不可能的,因为该位置不存在。为了实现这一点,我必须用[None] * 43或别的什么来初始化x,而不是简单地初始化[]。然而,下一次尝试非常成功。这里我将'Foobar'赋值给一个空字典的键 42。你可以看到这里没有问题。一个新条目被简单地添加到字典中,我开始工作了。
清单 4-1 显示了电话簿示例的代码。
# A simple database
# A dictionary with person names as keys. Each person is represented as
# another dictionary with the keys 'phone' and 'addr' referring to their phone
# number and address, respectively.
people = {
'Alice': {
'phone': '2341',
'addr': 'Foo drive 23'
},
'Beth': {
'phone': '9102',
'addr': 'Bar street 42'
},
'Cecil': {
'phone': '3158',
'addr': 'Baz avenue 90'
}
}
# Descriptive labels for the phone number and address. These will be used
# when printing the output.
labels = {
'phone': 'phone number',
'addr': 'address'
}
name = input('Name: ')
# Are we looking for a phone number or an address?
request = input('Phone number (p) or address (a)? ')
# Use the correct key:
if request == 'p': key = 'phone'
if request == 'a': key = 'addr'
# Only try to print information if the name is a valid key in
# our dictionary:
if name in people: print("{}'s {} is {}.".format(name, labels[key], people[name][key]))
Listing 4-1.Dictionary Example
以下是该程序的运行示例:
Name: Beth
Phone number (p) or address (a)? p
Beth's phone number is 9102.
用字典格式化字符串
在第三章中,您看到了如何使用字符串格式化来格式化作为format方法的单独(命名或未命名)参数提供的值。有时候,以字典的形式收集一组命名的值可以使事情变得更容易。例如,字典可能包含各种信息,而您的格式字符串只会挑选出它需要的任何信息。您必须通过使用format_ map来指定您正在提供一个映射。
>>> phonebook
{'Beth': '9102', 'Alice': '2341', 'Cecil': '3258'}
>>> "Cecil's phone number is {Cecil}.".format_map(phonebook)
"Cecil's phone number is 3258."
当像这样使用字典时,你可以有任意数量的转换说明符,只要所有给定的键都能在字典中找到。这种字符串格式在模板系统中非常有用(在这种情况下使用 HTML)。
>>> template = '''<html>
... <head><title>{title}</title></head>
... <body>
... <h1>{title}</h1>
... <p>{text}</p>
... </body>'''
>>> data = {'title': 'My Home Page', 'text': 'Welcome to my home page!'}
>>> print(template.format_map(data))
<html>
<head><title>My Home Page</title></head>
<body>
<h1>My Home Page</h1>
<p>Welcome to my home page!</p>
</body>
字典方法
就像其他内置类型一样,字典也有方法。虽然这些方法非常有用,但您可能不像 list 和 string 方法那样经常需要它们。您可能想先浏览一下这一部分,以了解哪些方法是可用的,如果您需要确切地了解给定方法是如何工作的,稍后再回来。
清楚的
方法从字典中删除所有的条目。这是一个就地操作(像list.sort),所以它不返回任何东西(或者说,None)。
>>> d = {}
>>> d['name'] = 'Gumby'
>>> d['age'] = 42
>>> d
{'age': 42, 'name': 'Gumby'}
>>> returned_value = d.clear()
>>> d
{}
>>> print(returned_value)
None
这为什么有用?让我们考虑两种情况。这是第一个:
>>> x = {}
>>> y = x
>>> x['key'] = 'value'
>>> y
{'key': 'value'}
>>> x = {}
>>> x = {}
{'key': 'value'}
这是第二种情况:
>>> x = {}
>>> y = x
>>> x['key'] = 'value'
>>> y
{'key': 'value'}
>>> x.clear()
>>> y
{}
在这两个场景中,x和y最初指的是同一个字典。在第一个场景中,我通过分配一个新的空字典来“清空”x。那一点也不影响y,它还是指原词典。这可能是您想要的行为,但是如果您真的想要删除原始字典中的所有元素,您必须使用clear。正如您在第二个场景中看到的,y之后也是空的。
复制
copy方法返回一个具有相同键值对的新字典(浅拷贝,因为值本身是相同的,而不是拷贝)。
>>> x = {'username': 'admin', 'machines': ['foo', 'bar', 'baz']}
>>> y = x.copy()
>>> y['username'] = 'mlh'
>>> y['machines'].remove('bar')
>>> y
{'username': 'mlh', 'machines': ['foo', 'baz']}
>>> x
{'username': 'admin', 'machines': ['foo', 'baz']}
如您所见,当您替换副本中的值时,原始值不受影响。但是,如果您修改一个值(就地,而不是替换它),原始值也会被更改,因为相同的值存储在那里(就像本例中的'machines'列表)。
避免该问题的一种方法是进行深层复制,复制值、它们包含的任何值等等。您可以使用copy模块中的函数deepcopy来完成这个任务。
>>> from copy import deepcopy
>>> d = {}
>>> d['names'] = ['Alfred', 'Bertrand']
>>> c = d.copy()
>>> dc = deepcopy(d)
>>> d['names'].append('Clive')
>>> c
{'names': ['Alfred', 'Bertrand', 'Clive']}
>>> dc
{'names': ['Alfred', 'Bertrand']}
方法
fromkeys方法用给定的键创建一个新字典,每个键都有一个默认的对应值None。
>>> {}.fromkeys(['name', 'age'])
{'age': None, 'name': None}
这个例子首先构建一个空字典,然后调用这个字典的fromkeys方法来创建另一个字典——这是一个有点多余的策略。相反,您可以直接在dict上调用该方法,它(如前所述)是所有字典的类型。(类型和类的概念在第七章中有更详细的讨论。)
>>> dict.fromkeys(['name', 'age'])
{'age': None, 'name': None}
如果你不想使用None作为默认值,你可以提供你自己的默认值。
>>> dict.fromkeys(['name', 'age'], '(unknown)')
{'age': '(unknown)', 'name': '(unknown)'}
得到
get方法是访问字典条目的一种宽松方式。通常,当您试图访问字典中不存在的条目时,事情会变得非常糟糕。
>>> d = {}
>>> print(d['name'])
Traceback (most recent call last):
File "<stdin>", line 1, in ?
KeyError: 'name'
而get却不是这样。
>>> print(d.get('name'))
None
如您所见,当您使用get访问一个不存在的键时,没有异常。相反,你得到的是值None。你可以提供你自己的“默认”值,然后用它来代替None。
>>> d.get('name', 'N/A')
'N/A'
如果键在那里,get就像普通的字典查找一样工作。
>>> d['name'] = 'Eric'
>>> d.get('name')
'Eric'
清单 4-2 显示了清单 4-1 的程序的修改版本,它使用get方法来访问“数据库”条目。
# A simple database using get()
# Insert database (people) from Listing 4-1 here.
labels = {
'phone': 'phone number',
'addr': 'address'
}
name = input('Name: ')
# Are we looking for a phone number or an address?
request = input('Phone number (p) or address (a)? ')
# Use the correct key:
key = request # In case the request is neither 'p' nor 'a'
if request == 'p': key = 'phone'
if request == 'a': key = 'addr'
# Use get to provide default values:
person = people.get(name, {})
label = labels.get(key, key)
result = person.get(key, 'not available')
print("{}'s {} is {}.".format(name, label, result))
Listing 4-2.Dictionary Method Example
下面是这个程序的一个运行示例。请注意get al增加的灵活性如何让程序给出有用的响应,即使用户输入了我们没有准备好的值。
Name: Gumby
Phone number (p) or address (a)? batting average
Gumby's batting average is not available.
项目
items方法将字典中的所有条目作为条目列表返回,其中每个条目的形式都是(key, value)。这些项目不会以任何特定的顺序返回。
>>> d = {'title': 'Python Web Site', 'url': 'http://www.python.org', 'spam': 0}
>>> d.items()
dict_items([('url', 'http://www.python.org'), ('spam', 0), ('title', 'Python Web Site')])
返回值是一种称为字典视图的特殊类型。字典视图可以用于迭代(更多信息见第五章)。此外,您可以确定它们的长度并检查成员资格。
>>> it = d.items()
>>> len(it)
3
>>> ('spam', 0) in it
True
关于视图的一个有用之处是它们不复制任何东西;它们总是反映底层字典,即使您修改了它。
>>> d['spam'] = 1
>>> ('spam', 0) in it
False
>>> d['spam'] = 0
>>> ('spam', 0) in it
True
但是,如果您更愿意将项目复制到一个列表中(这是在旧版本的 Python 中使用项目时发生的情况),您总是可以自己做。
>>> list(d.items())
[('spam', 0), ('title', 'Python Web Site'), ('url', 'http://www.python.org')]
键
keys方法返回字典中键的字典视图。
流行音乐
pop方法可用于获取对应于给定键的值,然后从字典中移除键-值对。
>>> d = {'x': 1, 'y': 2}
>>> d.pop('x')
1
>>> d
{'y': 2}
popitem
popitem方法类似于list.pop,弹出列表的最后一个元素。然而,与list.pop不同的是,popitem弹出一个任意的条目,因为字典没有“最后一个元素”或任何顺序。如果您想以一种高效的方式逐个删除和处理项目(无需首先检索键列表),这可能非常有用。
>>> d = {'url': 'http://www.python.org', 'spam': 0, 'title': 'Python Web Site'}
>>> d.popitem()
('url', 'http://www.python.org')
>>> d
{'spam': 0, 'title': 'Python Web Site'}
虽然popitem类似于列表方法pop,但是没有与append(在列表末尾添加一个元素)等价的字典。因为字典没有顺序,这样的方法没有任何意义。
Tip
如果你想让popitem方法遵循一个可预测的顺序,看看来自collections模块的OrderedDict类。
设置默认值
setdefault方法有点类似于get,因为它检索与给定键相关联的值。除了get功能之外,setdefault设置与给定键相对应的值(如果它不在字典中)。
>>> d = {}
>>> d.setdefault('name', 'N/A')
'N/A'
>>> d
{'name': 'N/A'}
>>> d['name'] = 'Gumby'
>>> d.setdefault('name', 'N/A')
'Gumby'
>>> d
{'name': 'Gumby'}
如您所见,当缺少键时,setdefault返回默认值并相应地更新字典。如果键存在,则返回它的值,字典保持不变。默认是可选的,和get一样;如果它被遗漏了,就使用None。
>>> d = {}
>>> print(d.setdefault('name'))
None
>>> d
{'name': None}
Tip
如果您想要整个字典的全局缺省值,请查看来自collections模块的defaultdict类。
更新
方法用另一个字典的条目更新一个字典。
>>> d = {
... 'title': 'Python Web Site',
... 'url': 'http://www.python.org',
... 'changed': 'Mar 14 22:09:15 MET 2016'
... }
>>> x = {'title': 'Python Language Website'}
>>> d.update(x)
>>> d
{'url': 'http://www.python.org', 'changed':
'Mar 14 22:09:15 MET 2016', 'title': 'Python Language Website'}
所提供的字典中的条目被添加到旧字典中,用相同的键替换旧字典中的任何条目。
正如本章前面所讨论的,调用update方法的方式与调用dict函数(或类型构造函数)的方式相同。这意味着可以用映射、(key, value)对的序列(或其他可迭代对象)或关键字参数来调用update。
价值观念
values方法返回字典中值的字典视图。与keys不同,values返回的视图可能包含重复。
>>> d = {}
>>> d[1] = 1
>>> d[2] = 2
>>> d[3] = 3
>>> d[4] = 1
>>> d.values()
dict_values([1, 2, 3, 1])
快速总结
在本章中,您学习了以下内容:
- 映射:映射使您能够用任何不可变的对象来标记其元素,最常见的类型是字符串和元组。Python 中唯一的内置映射类型是字典。
- 字典的字符串格式化:您可以通过使用
format_map将字符串格式化操作应用于字典,而不是使用带有format的命名参数。 - 字典方法:字典有相当多的方法,它们的调用方式与列表和字符串方法相同。
本章的新功能
| 功能 | 描述 | | --- | --- | | `dict(seq)` | 从`(key, value)`对(或映射或关键字参数)创建字典 |什么现在?
您现在已经对 Python 的基本数据类型以及如何使用它们来构成表达式有了很多了解。你可能还记得第一章的内容,计算机程序有另一个重要的组成部分——语句。下一章会详细介绍它们。
Footnotes 1
dict函数根本不是一个真正的函数。它是一个类,就像list、tuple、str一样。
五、条件、循环和其他一些语句
现在,我相信你已经有点不耐烦了。好吧——所有这些数据类型都很好,但是你真的不能用它们做很多事情,不是吗?
让我们加快一点速度。我们已经遇到了一些语句类型(print语句、import语句和赋值)。在进入条件和循环的世界之前,让我们先看看使用它们的更多方法。然后我们将看到列表理解是如何像条件和循环一样工作的,即使它们是表达式,最后我们将看看pass、del和exec。
关于打印和导入的更多信息
随着您对 Python 了解的越来越多,您可能会注意到,您认为自己了解的 Python 的某些方面隐藏着一些功能,等待着给您惊喜。让我们来看看print和import中的几个这样的好特性。虽然print确实是一个函数,但它曾经是一个独立的语句类型,这就是我在这里讨论它的原因。
Tip
对于许多应用,日志记录(使用logging模块)将比使用print更合适。详见第十九章。
打印多个参数
您已经看到了如何使用print来打印一个表达式,它要么是一个字符串,要么自动转换成一个字符串。但是您实际上可以打印多个表达式,只要用逗号分隔它们:
>>> print('Age:', 42)
Age: 42
如您所见,每个参数之间插入了一个空格字符。如果您想要组合文本和变量值,而不使用字符串格式的全部功能,此行为会非常有用。
>>> name = 'Gumby'
>>> salutation = 'Mr.'
>>> greeting = 'Hello,'
>>> print(greeting, salutation, name)
Hello, Mr. Gumby
如果greeting字符串没有逗号,如何在结果中得到逗号?你不能只使用
print(greeting, ',', salutation, name)
因为这会在逗号前引入一个空格。一种解决方案如下:
print(greeting + ',', salutation, name)
它只是将逗号添加到问候语中。如果需要,您可以指定自定义分隔符:
>>> print("I", "wish", "to", "register", "a", "complaint", sep="_")
I_wish_to_register_a_complaint
您还可以指定一个自定义的结束字符串来替换默认的换行符。例如,如果您提供一个空字符串,您可以稍后在同一行继续打印。
print('Hello,', end='')
print('world!')
这个程序打印出Hello, world!。1
将某物作为另一物导入
通常,当你从一个模块中导入一些东西时,你可以使用
import somemodule
或者使用
from somemodule import somefunction
或者
from somemodule import somefunction, anotherfunction, yetanotherfunction
或者
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
Note
有些模块,比如os.path,是分层排列的(互相在里面)。有关模块结构的更多信息,请参见第十章中的封装章节。
分配魔法
不起眼的赋值语句也有一些窍门。
序列解包
您已经看到了很多赋值的例子,包括变量和部分数据结构(比如列表中的位置和片,或者字典中的槽),但是还有更多。您可以同时执行几项不同的任务。
>>> x, y, z = 1, 2, 3
>>> print(x, y, z)
1 2 3
听起来没什么用?你可以用它来交换两个(或更多)变量的内容。
>>> x, y = y, x
>>> print(x, y, z)
2 1 3
其实我这里做的叫做序列解包(或者 iterable 解包)。我有一个值序列(或者一个任意的可迭代对象),我把它分解成一个变量序列。让我说得更明白些。
>>> 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 "<stdin>", line 1, in <module>
ValueError: need more than 2 values to unpack
>>> x, y, z = 1, 2, 3, 4
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: too many values to unpack
您可以使用星号运算符(*)收集多余的值,而不是确保值的数量完全匹配。例如:
>>> 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')
同样的聚集也可以用在函数参数列表中(见第六章)。
链式分配
当您想要将几个变量绑定到同一个值时,可以使用链式赋值作为一种快捷方式。这看起来有点像上一节中的同步赋值,只是这里只处理一个值:
x = y = somefunction()
和这个一样:
y = somefunction()
x = y
请注意,前面的语句可能与
x = somefunction()
y = somefunction()
有关这方面的更多信息,请参阅本章后面关于标识运算符(is)的部分。
扩充分配
不用写x = x + 1,你可以只把表达式操作符(在这个例子中是+)放在赋值操作符(=)前面,然后写x += 1。这就是所谓的扩充赋值,它适用于所有的标准操作符,比如*、/、%等等。
>>> x = 2
>>> x += 1
>>> x *= 2
>>> x
6
它也适用于其他数据类型(只要二元运算符本身适用于这些数据类型)。
>>> fnord = 'foo'
>>> fnord += 'bar'
>>> fnord *= 2
>>> fnord
'foobarfoobar'
扩充赋值可以使你的代码更加紧凑和简洁,在很多情况下,可读性更好。
积木:缩进的快乐
块并不是一种真正的语句类型,而是在处理接下来的两节时需要用到的东西。
块是一组在条件为真时可以执行的语句(条件语句),可以执行多次(循环),等等。块是通过缩进代码的一部分来创建的,也就是说,在它的前面加上空格。
Note
您也可以使用制表符来缩进您的块。Python 将制表符解释为移动到下一个制表位,每八个空格有一个制表位,但是标准和优选的样式是只使用空格,不使用制表符,特别是每一级缩进有四个空格。
块中的每一行都必须缩进相同的量。以下是伪代码(不是真正的 Python 代码),展示了缩进是如何工作的:
this is a line
this is another line:
this is another block
continuing the same block
the last line of this block
phew, there we escaped the inner block
在许多语言中,一个特殊的单词或字符(例如,begin或{)用于开始一个块,另一个(例如,end或})用于结束它。在 Python 中,冒号(:)用来表示一个块即将开始,然后该块中的每一行都缩进(相同的量)。当你回到与某个封闭块相同的缩进量时,你就知道当前块已经结束了。(许多编程编辑和 ide 都知道这种块缩进是如何工作的,并且可以帮助您不费吹灰之力就得到它。)
现在,让我们来看看这些积木可以用来做什么。
条件和条件语句
到目前为止,您已经编写了一个接一个地执行每个语句的程序。是时候超越这一点,让您的程序选择是否执行语句块了。
这就是那些布尔值的用途
现在你终于需要那些你反复遇到的真值(也叫布尔值,以乔治·布尔命名,他对真值做了很多聪明的事情)。
Note
如果你一直在密切关注,你会注意到第一章的边栏,“先睹为快:If 语句”,它描述了if语句。直到现在我才真正正式地介绍它,正如你将看到的,它比我到目前为止告诉你的要多一点。
当作为布尔表达式求值时(例如,作为if语句的条件),解释器认为下列值为假:
False None 0 "" () [] {}
换句话说,标准值False和None、所有类型(包括 float、complex 等等)的数字零、空序列(比如空字符串、元组、列表)和空映射(比如字典)都被认为是假的。其他的 2 都解释为真,包括特殊值True。 3
明白了吗?这意味着 Python 中的每一个值都可以被解释为真值,这一开始可能会有点混乱,但也可能极其有用。尽管你有所有这些真值可以选择,但“标准”真值是True和False。在某些语言中(比如 2.3 版之前的 C 和 Python),标准的真值是 0(代表假)和 1(代表真)。事实上,True和False并没有什么不同——它们只是 0 和 1 的美化版本,看起来不同,但行为相同。
>>> True
True
>>> False
False
>>> True == 1
True
>>> False == 0
True
>>> True + False + 42
43
所以现在,如果你看到一个逻辑表达式返回1或者0(很可能是老版本的 Python),你就会知道真正的意思是True或者False。
布尔值True和False属于bool类型,可以用来(就像例如list、str和tuple)转换其他值。
>>> bool('I think, therefore I am')
True
>>> bool(42)
True
>>> bool('')
False
>>> bool(0)
False
因为任何值都可以用作布尔值,所以您很可能很少(如果有的话)需要这样的显式转换(也就是说,Python 会自动为您转换值)。
Note
虽然[]和""都是假的(也就是bool([]) == bool("") == False),但是不相等(也就是[] != "")。这同样适用于其他不同类型的虚假物体(例如,可能更明显的例子() != False)。
条件执行和 if 语句
真值可以组合,我们将回到如何做,但让我们先看看你可以用它们做什么。尝试运行以下脚本:
name = input('What is your name? ')
if name.endswith('Gumby'):
print('Hello, Mr. Gumby')
这是if语句,它允许您进行条件执行。这意味着,如果条件(在if之后,冒号之前的表达式)的计算结果为真(如前所述),则执行下面的块(在本例中,是一个单独的print语句)。如果条件为假,那么块就不会被执行(但是你猜到了,不是吗?).
Note
在第一章的侧栏“先睹为快:if 语句”中,该语句写在一行上。这相当于使用单行块,如前面的示例所示。
else 子句
在上一节的例子中,如果您输入一个以“Gumby”结尾的名字,方法name.endswith返回True,使得if语句进入该块,并且打印出问候语。如果您愿意,您可以添加一个替代语句,即else子句(之所以称为子句,是因为它实际上不是一个单独的语句,只是if语句的一部分)。
name = input('What is your name?')
if name.endswith('Gumby'):
print('Hello, Mr. Gumby')
else:
print('Hello, stranger')
在这里,如果第一个块没有被执行(因为条件被评估为 false),您将进入第二个块。这确实说明了阅读 Python 代码是多么容易,不是吗?只要大声朗读代码(从if开始),它听起来就像一个正常(或者不太正常)的句子。
还有一个if语句的近亲,叫做条件表达式。这是 Python 版本的 c 中的三元运算符。这是一个使用if和else来确定其值的表达式:
status = "friend" if name.endswith("Gumby") else "stranger"
每当条件(紧跟在if之后的)为真时,表达式的值是提供的第一个值(在本例中为"friend"),否则是最后一个值(在本例中为"stranger")。
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')
嵌套块
让我们加入一些花哨的东西。您可以在其他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子句,则不以“先生”或“夫人”开头的姓名将被忽略(假设姓名是“Gumby”)。如果去掉外层的else子句,陌生人会被忽略。
更复杂的条件
这就是关于if语句的全部知识。现在让我们回到条件本身,因为它们是条件执行中真正有趣的部分。
比较运算符
也许条件中使用的最基本的操作符是比较操作符。它们被用来(惊奇,惊讶)比较事物。表 5-1 总结了比较运算符。
表 5-1。
The Python Comparison Operators
| 表示 | 描述 | | --- | --- | | `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`。 |Comparing Incompatible Types
理论上,您可以比较任意两个对象x和y的相对大小(使用运算符,如<和<=),并获得一个真值。然而,这样的比较只有在x和y属于相同或密切相关的类型(比如两个整数或一个整数和一个浮点数)时才有意义。
就像把一个整数加到一个字符串上没有多大意义一样,检查一个整数是否小于一个字符串看起来也是相当没有意义的。奇怪的是,在 Python 之前的版本中,你可以这样做。即使您使用的是较旧的 Python,您也应该远离这种比较,因为结果完全是任意的,并且可能在程序的每次执行之间发生变化。在 Python 3 中,不再允许以这种方式比较不兼容的类型。
在 Python 中比较可以被链接起来,就像赋值一样——你可以把几个比较操作符放在一个链中,就像这样:0 < age < 100。
其中一些操作符值得特别注意,将在下面的部分中描述。
相等运算符
如果你想知道两个事物是否相等,使用等式操作符,写成双等号,==。
>>> "foo" == "foo"
True
>>> "foo" == "bar"
False
双倍?为什么不能像数学中那样,只用一个等号呢?我相信你足够聪明,可以自己解决这个问题,但是让我们试试吧。
>>> "foo" = "foo"
SyntaxError: can't assign to literal
单等号就是赋值运算符,用来换东西,这不是你比较东西的时候想做的。
is:标识运算符
is操作符很有趣。好像和==一样工作,其实不然。
>>> x = y = [1, 2, 3]
>>> z = [1, 2, 3]
>>> x == y
True
>>> x == z
True
>>> x is y
True
>>> x is z
False
直到最后一个例子,这看起来很好,但然后你会得到那个奇怪的结果:x不是z,尽管它们是相等的。为什么呢?因为is测试的是身份,而不是平等。变量x和y已经被绑定到同一个列表,而z只是被绑定到另一个列表,该列表恰好以相同的顺序包含相同的值。它们可能是相等的,但它们不是同一个对象。
这看起来不合理吗?考虑这个例子:
>>> x = [1, 2, 3]
>>> y = [2, 4]
>>> x is not y
True
>>> del x[2]
>>> y[1] = 1
>>> y.reverse()
在这个例子中,我从两个不同的列表开始,x和y。如你所见,x is not y(正好是x is y的逆),这你已经知道了。我稍微改变了一下列表,虽然它们现在是相等的,但是它们仍然是两个独立的列表。
>>> x == y
True
>>> x is y
False
在这里,很明显,这两个列表是相等的,但并不完全相同。
总结一下,用==看两个对象是否相等,用is看是否相同(同一个对象)。
Caution
避免将is用于基本的、不可变的值,如数字和字符串。由于 Python 内部处理这些对象的方式,结果是不可预测的。
in:成员运算符
我已经介绍了in操作符(在第二章,在“成员资格”一节)。它可以用在条件中,就像所有其他比较运算符一样。
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
排序是按字母顺序的,但是字母表都是 Unicode 的,按代码点排序。
>>> "<"
True
实际上,字符是按序数值排序的。字母的序数值可以用ord函数找到,它的逆函数是chr:
>>> ord("
128585
>>> ord("
128586
>>> chr(128584)
'
这种方法非常合理且一致,但有时可能与您自己的排序方式背道而驰。例如,大写字母可能不符合您的要求。
>>> "a" < "B"
False
一个技巧是忽略大写和小写字母的区别,使用字符串方法lower。这里有一个例子(见第三章):
>>> "a".lower() < "B".lower()
True
>>> 'FnOrD'.lower() == 'Fnord'.lower()
True
其他序列也以同样的方式进行比较,除了用其他类型的元素代替字符。
>>> [1, 2] < [2, 1]
True
如果序列包含其他序列作为元素,则相同的规则适用于这些序列元素。
>>> [2, [1, 4]] < [2, [1, 5]]
True
布尔运算符
现在,你有很多返回真值的东西。(事实上,鉴于所有值都可以解释为真值,所有表达式都返回它们。)但是您可能想要检查不止一个条件。例如,假设您要编写一个程序,它读取一个数字并检查它是否在 1 和 10 之间(包括 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!')
Note
我本可以(而且很可能应该)通过使用下面的链式比较使这个例子更加简单:1 <= number <= 10。
and运算符是所谓的布尔运算符。它接受两个真值,如果都为真,则返回 true,否则返回 false。您还有两个这样的操作符,or和not。有了这三个,你可以以任何你喜欢的方式组合真值。
if ((cash > price) or customer_has_good_credit) and not out_of_stock:
give_goods()
Short-Circuit Logic and Conditional Expressions
布尔运算符有一个有趣的特性:它们只计算需要计算的内容。比如表达式x and y要求x和y都为真;所以如果x为假,表达式立即返回假,不用担心y。实际上,如果x为假,则返回x;否则返回y。(你能看出这是如何给出预期含义的吗?)这种行为被称为短路逻辑(或懒惰求值):布尔运算符通常被称为逻辑运算符,正如您所看到的,第二个值有时会“短路”。这也适用于or。在表达式x或y中,如果x为真,则返回;否则,返回y。(你能看出这有什么意义吗?)注意,这意味着布尔运算符之后的任何代码(比如函数调用)都可能根本不会被执行。您可能会在如下代码中看到这种行为:
name = input('Please enter your name: ') or '<unknown>'
如果没有输入姓名,or表达式的值为'<unknown>'。在许多情况下,您可能希望使用条件表达式,而不是这种简单的技巧,尽管像前面这样的语句确实有它们的用途。
断言
有一个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
环
现在你知道了如果一个条件为真(或假)该如何做某件事,但是你如何多次做某件事呢?例如,您可能想要创建一个程序来提醒您每月支付租金,但是使用我们到目前为止所看到的工具,您需要编写这样的程序(伪代码):
send mail
wait one month send mail
wait one month send mail
wait one month
(... and so on)
但是,如果您希望它继续这样做,直到您停止它呢?基本上,您想要这样的东西(同样,伪代码):
while we aren't stopped:
send mail
wait one month
或者,我们举个更简单的例子。假设你想打印出从 1 到 100 的所有数字。再说一次,你可以用愚蠢的方法。
print(1)
print(2)
print(3)
...
print(99)
print(100)
但你不是因为想做傻事才开始用 Python 的吧?
while 循环
为了避免前面示例中繁琐的代码,这样做是很有用的:
x = 1
while x <= 100:
print(x)
x += 1
现在,你如何在 Python 中做到这一点?你猜对了——你就是这样做的。没那么复杂吧。您还可以使用循环来确保用户输入姓名,如下所示:
name = ''
while not name:
name = input('Please enter your name: ')
print('Hello, {}!'.format(name))
试着运行这个,然后在被要求输入你的名字时按下回车键。你会看到问题又出现了,因为name还是一个空字符串,计算结果为 false。
Tip
如果您只输入一个空格字符作为您的姓名,会发生什么情况?试试看。它被接受是因为带有一个空格字符的字符串不是空的,因此不是 false。这肯定是我们的小程序中的一个缺陷,但很容易纠正:只需将while not name改为while not name or name.isspace()或者,也许是while not name.strip()。
对于循环
while语句非常灵活。它可用于在任何条件为真时重复代码块。虽然一般来说这可能很好,但有时您可能想要一些适合您特定需求的东西。一个这样的需求是为一组值(或者,实际上,序列或其他可迭代对象)的每个元素执行一个代码块。
Note
基本上,可迭代对象是任何可以迭代的对象(也就是说,在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 开始,如果您只提供一个限制(这将是最后一个),这实际上是假定的。
>>> range(10)
range(0, 10)
下面的程序写出从 1 到 100 的数字:
for number in range(1,101):
print(number)
请注意,这比我之前使用的while循环要紧凑得多。
Tip
如果您可以使用for循环而不是while循环,您可能应该这样做。
遍历字典
要循环遍历字典的键,可以使用普通的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)
Note
和往常一样,字典元素的顺序是未定义的。换句话说,当迭代一个字典的键或值时,您可以确定您将处理所有的键或值,但是您不知道以什么顺序。如果顺序很重要,可以将键或值存储在一个单独的列表中,例如,在迭代之前进行排序。如果您希望您的映射记住其条目的插入顺序,您可以使用来自collections模块的类OrderedDict。
一些迭代工具
Python 有几个函数在迭代序列(或其他可迭代对象)时很有用。其中一些可以在itertools模块中获得(在第十章中提到),但也有一些内置函数非常方便。
并行迭代
有时候你想同时迭代两个序列。假设您有以下两个列表:
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,它将序列“压缩”在一起,返回一个元组序列。返回值是一个特殊的 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会做什么:当最短的序列用完时它会停止。
>>> list(zip(range(5), range(100000000)))
[(0, 0), (1, 1), (2, 2), (3, 3), (4, 4)]
编号迭代
在某些情况下,您希望迭代一系列对象,同时能够访问当前对象的索引。例如,您可能想要替换字符串列表中包含子字符串'xxx'的每个字符串。当然有很多方法可以做到这一点,但是假设您想按照下面的思路做一些事情:
for string in strings:
if 'xxx' in string:
index = strings.index(string) # Search for the string in the list of strings
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]'
该函数允许您迭代索引-值对,其中索引是自动提供的。
反向排序迭代
让我们看看另外两个有用的函数: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 方法。为了执行这些任务,只需用list转换返回的对象。
Tip
我们可以使用小写的技巧来得到正确的字母排序。例如,您可以使用str.lower作为sort或sorted的key参数。例如,sorted("aBc", key=str.lower)返回['a', 'B', 'c']。
打破循环
通常,循环只是执行一个块,直到它的条件变为假,或者直到它用完了所有的序列元素。但是有时您可能想要中断循环,开始一个新的迭代(执行块的一个“回合”),或者简单地结束循环。
破裂
要结束一个循环,可以使用break。假设您想要找到小于 100 的最大平方(整数自乘的结果)。然后从 100 开始向下迭代到 0。当你找到一个方块时,没有必要继续,所以你只需break退出循环。
from math import sqrt
for n in range(99, 0, -1):
root = sqrt(n)
if root == int(root):
print(n)
break
如果你运行这个程序,它会打印出81并停止。请注意,我为range添加了第三个参数——这是步长,序列中每对相邻数字之间的差。它可以用来向下迭代,就像我在这里做的那样,步长值为负,它可以用来跳过数字。
>>> range(0, 10, 2)
[0, 2, 4, 6, 8]
继续
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一起使用,这将在下一节解释。
while True/break 成语
Python 中的for和while循环非常灵活,但是每隔一段时间,您可能会遇到一个让您希望拥有更多功能的问题。例如,假设当用户在提示下输入单词时,您想要做一些事情,而当没有提供单词时,您想要结束循环。一种方法是这样的:
word = 'dummy'
while word:
word = input('Please enter a word: ')
# do something with the 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:
# do something with the 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
# do something with the word:
print('The word was ', word)
while True部分给你一个永远不会自行终止的循环。相反,您将条件放在循环内部的一个if语句中,当条件满足时,该语句将调用break。因此,您可以在循环内部的任何地方终止循环,而不仅仅是在开始处(就像普通的while循环一样)。if/break代码自然地将循环分成两部分:第一部分负责设置(这部分将被普通的while循环复制),另一部分利用第一部分的初始化,前提是循环条件为真。
尽管您应该小心不要在代码中过于频繁地使用break(因为这会使您的循环更难阅读,尤其是如果您在一个循环中放入了不止一个break),但是这种特殊的技术是如此的常见,以至于大多数 Python 程序员(包括您自己)可能都能够理解您的意图。
循环中的 else 子句
当你在循环中使用break语句时,通常是因为你已经“发现”了一些东西,或者因为一些事情已经“发生”了。爆发的时候很容易去做一件事(比如print(n)),但有时候没爆发的时候可能会想做一件事。但是你怎么知道呢?你可以使用一个布尔变量,在循环之前将它设置为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 didn't break out!")
更简单的方法是在循环中添加一个else子句——只有在没有调用break的情况下才会执行。让我们重复使用上一节关于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来测试else子句。如果你运行这个程序,它会打印出“没找到!”因为(正如你在break一节中看到的)100 以下的最大正方形是 81。您可以在for循环和while循环中使用continue、break和else子句。
理解——有点不靠谱
列表理解是从其他列表中制作列表的一种方式(类似于集合理解,如果你知道数学中的这个术语的话)。它的工作方式类似于for循环,实际上非常简单。
>>> [x * x for x in range(10)]
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
该列表由range(10)中每个x的x*x组成。很简单吗?如果只想打印出那些能被 3 整除的方块呢?然后你可以使用模操作符——当y被 3 整除时y % 3返回零。(注意只有在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循环构建了相同的列表:
result = []
for x in range(3):
for y in range(3)
result.append((x, y))
这可以与一个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']
这就给出了名字首字母相同的男女生对。
A Better Solution
男孩/女孩配对的例子不是特别有效,因为它检查每一个可能的配对。在 Python 中有很多方法可以解决这个问题。亚历克斯·马尔泰利建议如下:
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字典方法在前一章中有描述。)在构建了这个字典之后,list comprehension 循环遍历所有的男孩,并且查找其名字以与当前男孩相同的字母开头的所有女孩。这样,列表理解不需要尝试男孩和女孩的每一种可能的组合,并检查第一个字母是否匹配。
使用普通的括号而不是方括号不会给你一个“元组理解”——你将最终得到一个生成器。更多信息请参见第九章中的边栏“多圈发电机”。但是,您可以使用花括号来执行字典理解。
>>> squares = {i:"{} squared is {}".format(i, i**2) for i in range(10)}
>>> squares[8]
'8 squared is 64'
在for前面不是一个单一的表达式,而是用冒号分隔的两个表达式。这些将成为键和它们相应的值。
三个在路上
为了结束这一章,让我们快速看一下另外三个语句:pass、del和exec。
什么都没发生!
有时候你什么都不需要做。这可能不经常发生,但当它发生时,知道您有pass语句是很好的。
>>> pass
>>>
这里没什么事情。
现在,你到底为什么想要一份什么都不做的声明?当您编写代码时,它可以作为一个占位符。例如,您可能已经编写了一个if语句,并且想要尝试一下,但是您缺少其中一个块的代码。请考虑以下几点:
if name == 'Ralph Auldus Melish':
print('Welcome!')
elif name == 'Enid':
# Not finished yet ...
elif name == 'Bill Gates':
print('Access Denied')
这段代码不会运行,因为空块在 Python 中是非法的。要解决这个问题,只需在中间的块中添加一个pass语句。
if name == 'Ralph Auldus Melish':
print('Welcome!')
elif name == 'Enid':
# Not finished yet ...
pass
elif name == 'Bill Gates':
print('Access Denied')
Note
注释和pass语句组合的另一种方法是简单地插入一个字符串。这对未完成的函数(见第六章)和类(见第七章)特别有用,因为它们将充当文档字符串(在第六章中解释)。
用 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获得。但是当我也将None赋值给robin时,字典突然在电脑的内存中浮动,而且没有名字。我无法检索或使用它,所以 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 = ["Hello", "world"]
>>> y = x
>>> y[1] = "Python"
>>> x
['Hello', 'Python']
您可能认为删除了x,也就删除了y,但事实并非如此。
>>> del x
>>> y
['Hello', 'Python']
这是为什么呢?x和y指的是同一个列表,但是删除x一点也不影响y。这是因为您只删除了名称,而不是列表本身(值)。事实上,在 Python 中没有办法删除值——您也不需要这样做,因为每当您不再使用该值时,Python 解释器就会自动删除。
用 exec 和 eval 执行和计算字符串
有时,您可能希望“动态地”创建 Python 代码,并将其作为一条语句来执行,或者作为一个表达式来求值。这有时可能接近于黑魔法——考虑你自己被警告了。exec和eval都是函数,但是exec曾经是一个独立的语句类型,而eval与它密切相关,所以我在这里讨论它们。
Caution
在本节中,您将学习执行存储在字符串中的 Python 代码。这是一个巨大的潜在安全漏洞。如果您执行一个字符串,其中部分内容是由用户提供的,那么您很少或根本无法控制您正在执行的代码。这在网络应用中尤其危险,比如通用网关接口(CGI)脚本,你将在第十五章中了解到。
高级管理人员
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函数主要在动态构建代码串时有用。如果字符串是由从其他地方(可能是从用户那里)获得的部分构建的,那么您很难确定它将包含什么。所以为了安全起见,你给它一个字典,作为它的名称空间。
Note
名称空间或范围的概念是一个重要的概念。你将在下一章深入研究它,但是现在,你可以把一个名称空间想象成一个保存你的变量的地方,就像一个看不见的字典。因此,当你执行一个类似于x = 1的赋值时,你将键x和值1存储在当前的名称空间中,这个名称空间通常是全局名称空间(到目前为止,我们大部分时间都在使用这个名称空间),但也不是必须如此。
通过添加第二个参数可以做到这一点,这个参数是一个字典,它将作为代码字符串的名称空间。 4
>>> from math import sqrt
>>> scope = {}
>>> exec('sqrt = 1', scope)
>>> sqrt(4)
2.0
>>> scope['sqrt']
1
如您所见,潜在的破坏性代码没有覆盖sqrt函数。该函数就像它应该的那样工作,从exec赋值产生的sqrt变量可以从作用域中获得。
注意,如果你试图打印出scope,你会看到它包含了很多东西,因为名为__builtins__的字典是自动添加的,包含了所有的内置函数和值。
>>> len(scope)
2
>>> scope.keys()
['sqrt', '__builtins__']
evaluate 评价
一个类似于exec的内置函数是eval(代表“evaluate”)。正如exec执行一系列 Python 语句一样,eval计算一个 Python 表达式(用字符串编写)并返回结果值。(exec不返回任何东西,因为它本身就是一个语句。)例如,您可以使用以下代码制作一个 Python 计算器:
>>> eval(input("Enter an arithmetic expression: "))
Enter an arithmetic expression: 6 + 18 * 2
42
您可以用eval提供一个名称空间,就像用exec一样,尽管表达式很少像语句通常做的那样重新绑定变量。
Caution
尽管表达式通常不重新绑定变量,但它们肯定可以(例如,通过调用重新绑定全局变量的函数)。因此,对一段不受信任的代码使用 eval 并不比使用 exec 更安全。目前,在 Python 中没有执行不可信代码的安全方法。一种替代方法是使用 Python 的实现,比如 Jython(参见第十七章)并使用一些本地机制,比如 Java 沙箱。
Priming the Scope
为 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
你可以用这种方式构建相当复杂的程序,但是……你可能不应该这么做。
快速总结
在本章中,您看到了几种陈述。
- 打印:您可以使用
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函数计算字符串中的表达式并返回结果。
本章的新功能
| 功能 | 描述 | | --- | --- | | `chr(n)` | 当传递序数`n` (0 ≤ n < 256)时,返回一个单字符字符串 | | `eval(source[, globals[, locals]])` | 将字符串作为表达式计算并返回值 | | `exec(source[, globals[, locals]])` | 将字符串作为语句进行计算和执行 | | `enumerate(seq)` | 产生适合迭代的`(index, value)`对 | | `ord(c)` | 返回单字符字符串的整数序数值 | | `range([start,] stop[, step])` | 创建整数列表 | | `reversed(seq)` | 以逆序产生`seq`的值,适合迭代 | | `sorted(seq[, cmp][, key][, reverse])` | 返回一个列表,其中包含按排序顺序排列的值`seq` | | `xrange([start,] stop[, step])` | 创建一个`xrange`对象,用于迭代 | | `zip(seq1, seq2,…)` | 创建适合并行迭代的新序列 |什么现在?
现在你已经清除了基础。你可以实现任何你能想到的算法;您可以读入参数并打印出结果。在接下来的几章中,你将学到一些东西,这些东西将帮助你编写更大的程序,而不会丢失大局。这种东西叫做抽象。
Footnotes 1
这将只在脚本中工作,而不是在交互式 Python 会话中。在交互会话中,每个语句都将被单独执行(并打印其内容)。
2
至少当我们谈论内置类型时——正如你在第九章看到的,你可以影响你自己构造的对象被解释为真还是假。
3
正如 Python 老手 Laura Creighton 所说,这种区别更接近于有与无,而不是真与假。
4
事实上,您可以为 exec 提供两个名称空间,一个全局名称空间和一个本地名称空间。全局的必须是字典,但是本地的可以是任何映射。这同样适用于 eval。