迭代器:初次探索
我们以及知道for循环可以用于python中任何序列类型,包括列表、元组以及字符串,如下所示:
>>> for i in [1, 2, 3, 4]:
... print(i)
...
1
2
3
4
>>>
>>> for i in (1, 2, 3, 4):
... print(i)
...
1
2
3
4
>>>
>>> for i in 'spam':
... print(i)
...
s
p
a
m
实际上for循环可用于任何可迭代对象;
可迭代对象是python语言中比较新的概念,但是它在语言设计中普遍存在。本质上,这就是序列观念的一种通用化:如果对象是实际保存的序列或事可以在迭代工具上下文中一次产生一个结果的对象,那么就看作是可迭代的。总之,可迭代对象包括实际序列,以及能够按照需求计算的虚拟序列;
迭代协议
所有带有__next__方法的对象会前进到下一个结果,而当达到一系列结果的末尾时,__next__方法会引发StopIteration异常,这种对象在python中被称为迭代器。任何这类对象也能以for循环或其他迭代工具遍历,因为所有迭代工具内部工作起来都是在每次迭代中调用__next__方法,并通过捕捉StopIteration异常来确定何时离开。
手动迭代:iter和next
为了简化手动代码,python还提供了内置函数next,它会自动调用一个对象的__next__方法。即给定一个迭代器对象X,调用next(X)等同于X.next(),但是next(X)更简单且可移植新更好。
另有一点需要注意。for循环在开始时,会首先把可迭代对象传入内置函数iter,
for循环在开始时,会首先把可迭代对象传入内置函数iter,并由此拿到一个迭代器;而iter调用返回的迭代器对象有着所需的next方法。iter函数与next和__next__很像,在它的内部调用了__iter__方法。
完整的迭代协议
该协议基于分别用在不同的两个步骤中的对象:
- 可迭代对象:迭代的被调用对象,其__iter__方法被iter函数多调用;
- 迭代器对象:可迭代对象的返回结果,在迭代过程中实际提供值的对象。它的__next__方法被next运行,并在结束时触发StopIteration异常。
在实际代码中,我们可以通过模拟for循环内部调用的例子,来学习迭代协议中的第一步的具体工作方式:
>>> L = [1, 2, 3]
>>> type(L)
<class 'list'>
>>> I = iter(L)
>>> type(I)
<class 'list_iterator'>
>>>
>>> I.__next__()
1
>>> I.__next__()
2
>>> I.__next__()
3
>>> I.__next__()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
最初的一步对于文件来说不是必需的,因为文件对象自身就是迭代器。由于文件只支持一次迭代,文件有自己的__next__方法,因此不需要像这样返回一个不同的对象;
手动迭代
尽管python迭代工具会自动调用这些函数,我们也可以使用它们来手动地应用迭代协议。如以的交互式命令行内容展示了自动和手动迭代之间的等价性:
>>> L = [1, 2, 3]
>>>
>>> for i in L:
... print(i, end=' ')
...
1 2 3 >>>
>>>
>>>
>>> I = iter(L)
>>> while True:
... try:
... x = next(I)
... except StopIteration:
... break
... print(x, end=' ')
...
1 2 3 >>>
其他内置类型可迭代对象
除了文件以及像列表这样的物理的序列外,其他类型也有其适用的迭代器。例如字典键的经典方法是显式地获取它的键列表:
>>> D = {'a':1, 'b':2, 'c':3}
>>> for key in D.keys():
... print(key, D[key])
...
a 1
b 2
c 3
其实在python中,字典作为一个可迭代对象自带的一个迭代器,在迭代上下文中,会自动一次返回一个键:
>>> I = iter(D)
>>> next(I)
'a'
>>> next(I)
'b'
>>> next(I)
'c'
>>> next(I)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
最终效果是,我们不再需要调用keys方法来遍历字典键——for循环将使用迭代协议在每次迭代的时候获取一个键:
>>> for key in D:
... print(key, D[key])
...
a 1
b 2
c 3
列表推导:初次深入探索
我们已经了解了迭代协议的工作方式,与for循环一起使用的列表推导,是主要的迭代协议上下文之一;
有一种需求,当我们需要对列表中的每一个元素都增加10,可以用以下代码实现:
>>> L = [1, 2, 3, 4, 5]
>>> for i in range(len(L)):
... L[i] += 10
...
>>> L
[11, 12, 13, 14, 15]
上述代码虽然可以实现需求,但是并不是python中优化的‘最佳实践’。我们可以使用列表推导来代替该循环:
>>> L = [1, 2, 3, 4, 5]
>>> L = [x + 10 for x in L]
>>> L
[11, 12, 13, 14, 15]
最终的结果是一致的,但列表推导需要更少的代码,并且运行速度会大大提升。
列表推导的基础
为了从语法上了解它,我们继续看列表推导的语句:
L = [x + 10 for i in L]
列表推导写在一个方括号中,因为它们是最终构建一个新的列表的一种方式。它们以我们所组合成的一个任意的表达式开始,在这里我们使用一个循环变量组合得到表达式。
扩展的列表推导语法
筛分语句:if
作为一个特别有用的扩展,推导表达式中嵌套的for循环可以有一个关联的if分句,来过滤掉那些测试不为真的结果项。
>>> L = [1, 2, 3, 4, 5, 6]
>>>
>>> L = [i for i in L if i < 4]
>>> L
[1, 2, 3]
嵌套循环:for
如果需要的话,列表推导甚至可以变得更加复杂——例如,我们可以通过编写一系列for分句,让推导包含嵌套的循环。实际上,它们的完整语法允许任意数量的for分句,并且每一个for分句都可以带有一个可选的关联的if分句。