2.3 序列
序列是数据值的顺序容器。序列在计算机科学中是强大而基本的抽象。序列不是特定的抽象数据类型,而是不同类型共有的一组行为。也就是说,它们是许多序列种类,但是都有一定的属性。以下为特别属性:
长度。序列拥有有限的长度。空的序列长度为0。
元素选择。序列的每个元素都拥有相应的非负整数作为下标,它小于序列长度,以第一个元素的 0 开始。
Python中有各种序列的数据类型,其中最重要的一种是list列表。
2.3.1 列表
列表值是可以任意长度的序列。列表有大量的内置行为,并带有表达这些行为的特定的语法。我们已经介绍过list literal,它计算了一个列表实例,以及元素选择表达式。内置的len函数返回序列的长度。下面,'digits'是一个有四个元素的列表。序号索引3中的元素是8。
>>> digits = [1, 8, 2, 8]
>>> len(digits)
4
>>> digits[3]
8
此外,列表可以相加或者于整数做乘法。对于序列,加法或乘法不会增添元素,而是组合或者复制序列本身。也就是说,运算符模块中的add函数生成了一个列表,它连接了add的参数。操作符中的mul函数可以使用一个列表和一个整数k来返回包含k次重复项的原始列表。
>>> [2, 7] + digits * 2
[2, 7, 1, 8, 2, 8, 1, 8, 2, 8]
任何值都可以包含在列表中,包括另一个列表。元素选择可以多次应用于,在包含列表的列表中选择深层嵌套的元素。
>>> pairs = [[10, 20], [30, 40]]
>>> pairs[1]
[30, 40]
>>> pairs[1][0]
30
2.3.2 序列迭代
在大多数情况下,我们希望能遍历序列中的元素,并依次为每个元素执行一些计算。这种模式非常常见,Python有一个额外的控制语句来处理顺序数据:'for'语句。
现在我们需要考虑一个值在序列中出现次数的问题。我们可以使用'while'循环来实现一个计数函数。
>>> def count(s, value):
"""Count the number of occurrences of value in sequence s."""
total, index = 0, 0
while index < len(s):
if s[index] == value:
total = total + 1
index = index + 1
return total
>>> count(digits, 8)
2
Python 中的'for'语句可以通过直接遍历元素值来简化该函数体,而无需引入名称索引'index'。
>>> def count(s, value):
"""Count the number of occurrences of value in sequence s."""
total = 0
for elem in s:
if elem == value:
total = total + 1
return total
>>> count(digits, 8)
2
一个'for'语句由一个以下形式的简单语句组成:
for <name> in <expression>:
<suite>
一个for语句由以下步骤执行:
- 评估开始的<expression>表达式,它必须能生成可迭代的值。
- 对可迭代值中的每个元素,依次:在当前帧中绑定<name>到该值上;执行<suite>。
这个执行过程是指可迭代的值。列表是一种序列,序列是可迭代的值。它们的元素按顺序被依次考虑。Python包含其他可迭代类型,但我们现在将重点聚焦序列;“iterable”这个术语的定义出现在第4章的迭代器部分。
这个评估过程的一个重要结果是< name >将被绑定到执行语句之后的序列的最后一个元素上。'for'循环还引入了另一种可以通过语句更新环境的方法。
序列拆封。程序中一个常见的模式是,有一个元素序列,这些元素本身是序列,但都是固定长度的。'for'语句可能包括在其头中的多个名称,以“解压缩”每个元素序列到其各自的元素中。例如,我们可能有一个两元素列表的列表。
>>> pairs = [[1, 2], [2, 2], [2, 3], [4, 4]]
我们希望能找到拥有相同第一和第二元素的pair的数目。
>>> same_count = 0
下面的for语句的header有两个名称,它们分别将x和y绑定在每个pair的第一和第二个元素上。
>>> for x, y in pairs:
if x == y:
same_count = same_count + 1
>>> same_count
2
这种将多个名称绑定到固定长度序列中的多个值的模式称为序列解压缩;这与我们在赋值语句中看到的绑定多个名称到多个值的模式相同。
范围(range)。范围是Python中另一个内置类型的序列,他表示一个整数范围。范围是用范围创建的,它需要两个整数参数:第一个数字和一个大于所需区间的数字。
>>> range(1, 10) # Includes 1, but not 10
range(1, 10)
在一个范围内调用列表构造函数,以与范围相同的元素来计算列表,这样就可以很容易地检查元素。
>>> list(range(5, 8))
[5, 6, 7]
如果只给出一个参数,那么它将被解释为从0开始的范围的最后一个值。
>>> list(range(4))
[0, 1, 2, 3]
范围通常显示在一个for语句的开头,他用来表示该循环被执行的次数:一个常见的约定是如果在循环体中未使用名称,则在for语句的开头中使用单个下划线字符。
>>> for _ in range(3):
print('Go Bears!')
Go Bears!
Go Bears!
Go Bears!
这个下划线在解释器中是环境中的另一个名称,但是它在程序员中有一个约定俗成的说法,它表示这个名称不会出现在以后任何的表达式中。
2.3.3 序列处理
序列是复合数据的一种常见形式,整个程序通常围绕这个抽象进行。有序列作为输入和输出的模块组件可以混合和匹配来执行数据处理。复杂的组件可以通过链接到一个序列处理操作的管道来定义,每一个过程都是简单而集中的。
列表表达式。可以通过对序列中每个元素的固定表达式求值,并在结果序列中收集结果值来表示许多序列处理操作。在Python中,列表表达式是执行这种计算的表达式。
>>> odds = [1, 3, 5, 7, 9]
>>> [x+1 for x in odds]
[2, 4, 6, 8, 10]
上面的关键字不是for语句的一部分,而是列表表达式的一部分,因为它包含在方括号内。子表达式x + 1被求值,同时x与奇数的每个元素相结合,结果的值被收集到一个列表中。
另一个常见的顺序处理操作是选择满足某些条件的值的子集。列表表达式也可以表达这个模式,例如选择所有的几率平均划分为25的元素。
>>> [x for x in odds if 25 % x == 0]
[1, 5]
列表表达式的通用形式是:
[<map expression> for <name> in <sequence expression> if <filter expression>]
为了对列表表达式求值,Python对<序列表达式>进行了求值,它必须返回一个可迭代的值。然后,对于按顺序的每个元素,元素值被绑定到< name >,筛选器表达式被求值,如果它产生一个真实值,则对映射表达式进行评估。将map表达式的值收集到一个列表中。
聚合。序列处理中的第三个常见模式是将序列中的所有值聚合为单个值。内置函数sum、min和max都是聚合函数的示例。
通过结合评估每个元素表达式的模式,选择元素的子集和聚合元素,我们可以使用序列处理方法来解决问题。
一个完美数是一个正整数,等于它的除数的和。n的除数是小于n的正整数,除以n。列出n的除数可以用一个列表表达式来表示。
>>> def divisors(n):
return [1] + [x for x in range(2, n) if n % x == 0]
>>> divisors(4)
[1, 2]
>>> divisors(12)
[1, 2, 3, 4, 6]
使用除数,我们可以用另一个列表表达式来计算从1到1000的所有完美数。(1通常被认为是一个完美数,但它不符合我们对除数的定义。)
>>> [n for n in range(1, 1000) if sum(divisors(n)) == n]
[6, 28, 496]
我们可以重新用我们对除数的定义来解决另一个问题:已知矩形面积,求边长都为整数的矩形的最小周长。矩形的面积等于高度与宽度的乘积。因此,如果已知面积和高度,我们可以计算矩形的宽度。
>>> def width(area, height):
assert area % height == 0
return area // height
矩形的周长等于其边长之和。
>>> def perimeter(width, height):
return 2 * width + 2 * height
具有整数边长的矩形的高度必须是其面积的除数。我们可以通过考虑所有高度来计算最小周长。
>>> def minimum_perimeter(area):
heights = divisors(area)
perimeters = [perimeter(width(area, h), h) for h in heights]
return min(perimeters)
>>> area = 80
>>> width(area, 5)
16
>>> perimeter(16, 5)
42
>>> perimeter(10, 8)
36
>>> minimum_perimeter(area)
36
>>> [minimum_perimeter(n) for n in range(1, 10)]
[4, 6, 8, 8, 12, 10, 16, 12, 12]
高阶函数。在序列处理中我们看到的情况常常被应用于高阶函数。首先,对一个序列里的每个元素进行求值也可以表达成对每个元素调用函数的形式。
>>> def apply_to_all(map_fn, s):
return [map_fn(x) for x in s]
选择表达式为真的元素,然后对其调用函数。
>>> def keep_if(filter_fn, s):
return [x for x in s if filter_fn(x)]
最后,很多种形式的聚合可以被表达成:反复地对reduced应用二个参数的函数。
>>> def reduce(reduce_fn, s, initial):
reduced = initial
for x in s:
reduced = reduce_fn(reduced, x)
return reduced
例如,reduce可以对一个序列里的所有元素进行连乘。用mul作为reduce_fn,1作为initial值,reduce可以被一个序列的元素进行连乘。
>>> reduce(mul, [2, 4, 8], 1)
64
我们也可以用高阶函数来找到完美数字。
>>> def divisors_of(n):
divides_n = lambda x: n % x == 0
return [1] + keep_if(divides_n, range(2, n))
>>> divisors_of(12)
[1, 2, 3, 4, 6]
>>> from operator import add
>>> def sum_of_divisors(n):
return reduce(add, divisors_of(n), 0)
>>> def perfect(n):
return sum_of_divisors(n) == n
>>> keep_if(perfect, range(1, 1000))
[1, 6, 28, 496]
默认名。在计算机科学的社区中,'apply_to_all'更常被命名为'map','keep_if'更常被命名为'filter'。在python中,内置函数'map'和'filter'是不返回列表的函数的一般化。这些函数将在第4章讨论。上面的定义等同于将list构造函数应用于内置map和filter调用的结果。
>>> apply_to_all = lambda map_fn, s: list(map(map_fn, s))
>>> keep_if = lambda filter_fn, s: list(filter(filter_fn, s))
reduce
函数位于Python标准库的functools
模块。在这里,参数initial
是可选非必需的。
>>> from functools import reduce
>>> from operator import mul
>>> def product(s):
return reduce(mul, s)
>>> product([1, 2, 3, 4, 5])
120
在Python程序中,使用列表表达式比使用高阶函数更为常见,然而这两种进行序列处理的方式使用都非常广泛。
2.3.4 序列抽象
我们引入了两种满足序列抽象的原生数据类型:list
列表和range
范围。 两者都满足使用条件:同时具有长度和元素选择两种功能。 Python中包含多于两个序列类型的行为,它们扩展了序列抽象。
Membership
成员资格。 一个value
值可以用于测试一个序列中的成员资格。 Python中有两个运算符in
和not in
,它们根据一个元素是否出现在序列中而做出True或False的判断。
>>> digits
[1, 8, 2, 8]
>>> 2 in digits
True
>>> 1828 not in digits
True
Slicing
切片。序列中包含较小的序列。 一小段序列可以是原始序列的任何连续跨度,由一对整数来构造规定。 与range
范围构造函数一样,第一个数是切片的起始索引,第二个是超出结束索引的一个整数。
在Python中,与元素选择相似,我们使用方括号表示序列切片,用一个冒号来分隔开始和结束索引。 任何被忽略的边界被假定为极端值:起始索引为0,结束索引为整个序列长度。
>>> digits[0:2]
[1, 8]
>>> digits[1:]
[8, 2, 8]
列举Python序列抽象的这些额外行为使我们有机会反思数据抽象的一般构成。抽象的丰富(即包含多少行为)可以引出很多结果。对于使用抽象的用户,其他行为可能会有所帮助。另一方面,用新数据类型满足抽象的要求可能是具有挑战性的。丰富抽象的另一个负面后果是用户学习需要更长的时间。
序列具有丰富的抽象,因为它们在计算中非常普遍,所以学习一些复杂的行为是合理的。但通常,大多数用户定义的抽象应尽可能简单。
进一步阅读。切片符号容许各种特殊情况,例如负的起始值,结束值和步长。完整的描述出现在Dive Into Python 3
中的切片列表小节中。在本章中,我们将仅使用上述基本特征。
2.3.5 字符串
对于计算机科学而言,文本值可能比数字更重要。 比方说,Python程序被编写并存储为文本。 Python中文本的本地数据类型称为字符串string
,并对应于构造函数str
。
在Python中有很多关于如何表示,表达和操纵字符串的细节。 字符串是丰富抽象的另一个例子,这需要程序员掌握一定的知识。 本节仅仅对基本字符串行为进行简要介绍。
字符串文字可以用于表示任意文本,它由单引号或双引号包围。
>>> 'I am string!'
'I am string!'
>>> "I've got an apostrophe"
"I've got an apostrophe"
>>> '您好'
'您好'
我们已经看到字符串出现在我们的代码中,比如在print
打印时被调用,以及在assert
语句中的错误消息中出现。
字符串满足我们在本节开头介绍的序列的两个基本条件:它拥有长度并支持元素选择功能。
>>> city = 'Berkeley'
>>> len(city)
8
>>> city[3]
'k'
一个字符串的元素是它本身的情况是该字符串为单字符。 字符可以是字母表元素,标点符号或其他符号。 与许多其他编程语言不同,Python没有单独的字符类型; 任何文本都是字符串,而表示单个字符的字符串长度为1。
像列表一样,字符串也可以通过加法和乘法组合。
>>> 'Berkeley' + ', CA'
'Berkeley, CA'
>>> 'Shabu ' * 2
'Shabu Shabu '
Membership
成员资格。字符串的行为与Python中的其他序列类型不同。 字符串抽象与list
列表和range
范围不同。 比如in
适用于字符串,但用法完全不同。 它匹配子串而不是元素。
>>> 'here' in "Where's Waldo?"
True
多行文字。 字符串不限于一行。 三重引号可以分隔跨越多行的字符串文字。 我们已经广泛地使用这个三重引用来处理文档。
>>> """The Zen of Python
claims, Readability counts.
Read more: import this."""
'The Zen of Python\nclaims, "Readability counts."\nRead more: import this.'
在上面的打印结果中,\ n
(发音为“backslash en”)是表示新行的单个元素。 尽管它显示为两个字符(反斜杠和“n”),但它被认为是用于长度和元素选择的单个字符。
字符串强制转换。 通过以对象值作为参数调用str构造函数,可以从Python中的任何对象创建一个字符串。 字符串的这一特性对于从各种类型的对象构造描述性字符串非常有用。
>>> str(2) + ' is an element of ' + str(digits)
'2 is an element of [1, 8, 2, 8]'
进一步阅读。 在计算机中编写文本是一个复杂的话题。 在本章中,我们将抽象出字符串如何表示的细节。 但是,对于许多应用程序而言,计算机如何编码字符串的具体细节是必不可少的知识。 Dive Into Python 3的字符串章节提供了字符编码和Unicode的描述。
2.3.6 Trees树
我们使用列表作为其他列表中元素的方法为我们的编程语言提供了一种新的组合方式。 这种方法被称为数据类型的闭包属性。 通常,如果组合的结果本身可以使用相同的方法进行组合,则组合数据值的方法具有闭包属性。 闭合是以任何方式进行组合的关键,因为它允许我们创建分层结构 - 由部分组成的结构,它们本身由部分组成,等等。
我们可以使用框和指针表示法在环境图中显示列表。 列表被描述为包含列表元素的相邻框。 原始值(如数字,字符串,布尔值和无)出现在元素框中。 复合值(如函数值和其他列表)由箭头表示。