Python学习笔记——循环与迭代

本文最后更新于:2022年8月11日 晚上

前言

由于我实在想不出写什么前言好,所以这里我选择直截了当地概括内容!(
在本篇中,你可以学到:怎么漂亮地写Python中的循环,轻松地认识Python中的迭代器和生成器。

正文

Python中的循环

认识Python中的循环

如果是刚接触Python,那么可能会对Python中循环的写法感到奇怪,因为Python中的循环并不像其他语言那样写。先来比较一下我们熟悉的循环和Python的循环。
就以Java语言为例:

1
2
3
4
# Java语言举例循环
for (int i = a; i < n; i += s){
// ...
}

然后再来看看Python的:

1
2
# Python语言的循环,a为起始值,n为终止值,s为步长。
for i in range(a, n, s)

其他语言转换成Python中的循环,就是用range()函数,它接受三个可选参数,在上边程序段中也说明了三个参数代表什么。
哦,当然,譬如Java和C#这样的语言也提供foreach循环,可以说Python中的基本循环就是比较接近于foreach循环的,直接在容器或序列中迭代元素,无需通过索引查找,比如:

1
2
3
4
5
6
7
8
items = ['a', 'b', 'c']
for item in items:
print(item)

# 输出如下:
a
b
c

所以如果是刚刚接触Python可能还需要一段时间来适应循环的写法,熟悉后便会觉得“诶,这样写循环还挺不错挺方便的哦!”
当然这里要提的不仅仅是这点东西,我还会介绍一下range()函数、解析式、以及一些循环写法。

在Python循环中需要注意的几点!
1、 循环中不再跟踪容器的大小,也不使用运行时索引来访问元素。
2、 容器本身现在负责分发将要处理的元素。如果容器是有序的,那么所得到的元素序列也是有序的。如果容器是无序的,那么将以随机顺序返回其元素,但循环仍然会遍历所有元素。

Python中的循环的一些神必写法

好吧其实并不神必只是想取个有意思的标题
我们知道,Python的循环可以取索引,或者直接取容器中的元素,但是有的时候我们既需要索引,也需要元素,能不能做到呢?其实是可以的,利用内置的enumerate()即可:

1
2
3
4
5
6
>>> for i, item in enumerate(items):
print(f'{i}: {item}')

0: a
1: b
2: c

Python中的迭代器可以连续返回多个值。迭代器可以返回含有任意个元素的元组,然后在for语句内解包。

Python中的解析式

列表解析式是Python的一个特性,也可以看作是一种语法糖(或者是神必写法)。它可以简化(前提是能够理解)并使循环更加紧凑,举个栗子:

1
2
3
4
5
6
7
# 如果要写一个平方列表,我们可能会这么做:
squares = []
for x in range(10):
squares.append(x * x)

# 如果使用列表解析式,就会变得十分简洁紧凑:
squares = [x * x for x in range(10)]

上边可以说是最基本的列表解析式构建列表了,列表解析式能做到的还不止这样,它还能使用条件来过滤元素:

1
even_squares = [x * x for x in range(10) if x % 2 == 0]

在上面的代码段中,构建了一个从0到9所有偶数整数的平方组成的列表。
归纳一下,可以得到如下列表解析式模板:

1
2
3
4
5
6
7
8
# 一般循环:
values = []
for item in collection:
if condition:
values.append(expression)

# 使用列表解析式的循环:
values = [expression for item in collection if condition]

以上就是列表解析式的所有内容啦,不过既然有列表解析式,那Python中是否还有字典解析式呢?
你好,有的:

1
2
>>> { x: x * x for x in range(5)}
{0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

不仅是字典解析式,还有集合解析式

1
2
>>> { x * x for x in range(-9, 10) }
{64, 1, 0, 36, 4, 9, 16, 81, 49, 25}

集合是无需类型,所以在将元素加到set容器时顺序是随机的。

“不过Python解析式中有一个需要注意的地方:在熟悉了解析式之后,很容易就会编写出难以阅读的代码。”
“在经历了许多烦恼之后,我给解析式设定的限制是只能嵌套一层。在大多数情况下,多层嵌套最好直接使用for循环,这样代码更加易读且容易维护。”

Python中的列表切片及其技巧

Python的列表对象有方便的切片特性。切片可被视为方括号索引语法的扩展,通常用于访问有序集合中某一范围的元素。例如,将一个大型列表对象分成几个较小的子列表。
我们可以这么看切片语法:a_list[start:stop:step],其中start为起始索引,stop为终止索引,step为步长(也叫stride,步幅)。不过要记住一点,切片计算方法是算头不算尾,也就是切片里面包含的元素是start到stop-1
了解了语法,就可以来看看它的一些应用技巧了。首先是用切片语法创建一个原始列表的逆序副本:

1
2
3
>>> a_list = [1, 2, 3, 4, 5]
>>> a_list[::-1]
[5, 4, 3, 2, 1]

稍微分析一下,在[::-1]这里, :: 让Python提供了完整的列表,而将步长设置为-1则让Python从后到前来遍历所有元素。

这么做其实和list.reverse()或是reverse()是一样的
这里还有另一个列表切片技巧:使用 : 操作符清空列表中的所有元素,同时不会破坏列表对象本身。
这适用于在程序中有其他引用指向这个列表时清空列表。在这种情况下,通常不能用新的列表对象替换已有列表来清空列表,替换列表不会更新原列表的引用。

1
2
3
>>> del a_list[:]
>>> a_list
[]

在Python3中也可以使用a_list.clear()完成同样的工作,而且这种方式在某些情况下可读性更好。
也就是说,我们还能利用这个技巧,用切片来在不创建新列表对象的情况下替换列表中的所有元素:

1
2
3
4
5
6
7
8
9
>>> a_list = [1, 2, 3, 4, 5]
>>> b_list = a_list
>>> a_list[:] = [6, 7, 8]
>>> a_list
[6, 7, 8]
>>> b_list
[6, 7, 8]
>>> b_list is a_list
True

利用 : 操作符还可以创建现有列表的浅副本:

1
2
3
4
5
>>> copied_list = a_list[:]
>>> copied_list
[6, 7, 8]
>>> copied_list is a_list
False

创建浅副本意味着只复制元素的结构,而不复制元素本身。两个列表中的每个元素都是相同的实例。
如果需要复制所有内容(包括元素),则需要创建列表的深副本。创建深副本可以使用Python内置的copy模块。

迭代器

什么是迭代器?

“迭代器(iterator),是确使用户可在容器对象(container,例如链表或数组)上遍访的对象,设计人员使用此接口无需关心容器对象的内存分配的实现细节。其行为很像数据库技术中的光标(cursor)。”——维基百科
举个例子,列表就属于迭代器,因此列表能用于for-in循环:

1
2
3
numbers = [1, 2, 3]
for num in numbers:
print(num)

在Python中,只要对象支持__iter____next__双下划线方法(或者说支持迭代器协议),那么就能使用for-in循环。正是由于这个特点,使得我们在需要遍历一个对象中的某些内容时能通过使用迭代器而变得十分方便。
所以我们可以编写一个支持迭代器协议的类,从而对这个类创建的实例使用for-in循环。

1
2
3
4
5
6
7
8
9
class Repeater:
def __init__(self, value):
self.value = value

def __iter__(self):
return self

def __next__(self):
return self.value

这里编写了一个Repeater类,该类在迭代时类的实例会重复返回同一个值,效果如下:

1
2
3
4
5
6
7
8
9
>>> repeater = Repeater('Hello')
>>> for item in repeater:
print(item)

Hello
Hello
...
Hello
Hello

其实到这迭代器就可以算介绍完了。看到这里,我相信有些人可以理解,但是有些人会觉得挺抽象的(因为我就觉得),也没搞明白到底是个什么工作原理,怎么iter返回个self啊,next返回个value什么的,所以下面会以书上的另一种方式再次解读迭代器。

还是用到上面的Repeater类为例子,但是这里我们把它拆分为两个类(Repeater、RepeaterIterator)来分别实现迭代器协议中的两个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Repeater:
def __init__(self, value):
self.value = value

def __iter__(self):
return RepeaterIterator(self)


class RepeaterIterator:
def __init__(self, source):
self.source = source

def __next__(self):
return self.source.value

我们要关注的是上面的__iter____next__两个方法以及RepeaterIterator类。
首先是__iter____iter__创建并返回了RepeaterIterator对象,从意思上看,也能说创建了一个Repeater类的迭代器对象。
然后就是RepeaterIterator类,我们要注意以下两点:
(1)在__init__方法中,每个RepeaterIterator实例都链接到创建它的Repeater对象。这样可以持有迭代的“源”(source)对象。
(2)在RepeaterIterator.__next__中,回到“源”Repeater实例并返回与其关联的值。
如此一来,我们便可以在for-in中使用Repeater实例。那么for-in循环又是怎么工作的呢?看下面一段代码:

1
2
3
4
5
repeater = Repeater('Hello')
iterator = repeator.__iter__()
while True:
item = iterator.__next__()
print(item)

首先让repeater对象准备迭代,即调用__iter__方法来返回实际的迭代器对象。
然后循环反复调用迭代器对象的__next__方法,从中获取值。
如果有数据库相关知识,你还会发现迭代器和游标的相似之处:首先初始化游标并准备读取,然后从中逐个取出数据存入局部变量中。
无论是元素列表、字典,还是Repeater类提供的无限序列,或是其他序列类型,对于迭代器来说只是实现细节不同。迭代器能以相同的方式遍历这些对象中的元素。

在Python解释器会话中模拟循环使用迭代器协议的方式

实际上,我们可以手动模拟这个过程:

1
2
3
4
5
6
7
>>> repeater = Repeater('Hello')
>>> iterator = iter(repeater)
>>> next(iterator)
'Hello'
>>> next(iterator)
'Hello'
...

在这里,我们没有使用双下划线方法,作为替代,我们使用了Python内置函数iter()next()
这些内置函数在内部会调用相同的双下划线方法,为迭代器协议提供一个简洁的封装(facade)。

有限迭代

既然可以手动模拟迭代过程,我们不妨来学习一下其他实现了迭代器协议的对象是怎么停下迭代的。
就用开头那个列表作为例子,让我们手动模拟一下它的迭代过程:

1
2
3
4
5
6
7
8
9
10
11
12
>>> iterator = iter(numbers)
>>> next(iterator)
1
>>> next(iterator)
2
>>> next(iterator)
3
>>> next(iterator)
StopIteration
>>> next(iterator)
StopIteration
...

可以看到,在遍历完列表中的所有元素后,继续调用next()函数将会抛出StopIteration异常。
所以我们也可以在我们自己编写的支持迭代器协议的类中,通过抛出StopIteration异常来停止迭代。
下面是书上的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class BoundedRepeater:
def __init__(self, value, max_repeats):
self.value = value
self.max_repeats = max_repeats
self.count = 0

def __iter__(self):
return self

def __next__(self):
if self.count >= self.max_repeats:
raise StopIteration
self.count += 1
return self.value

生成器

什么是生成器

生成器说白了吧,就是简化版的迭代器。它不是基于类的迭代器,而是基于函数的迭代器,因此也不需要实现两个双下划线方法,而且在生成器中不是使用return语句,而是使用yield语句将数据传回给调用者:

1
2
3
def repeater(value):
while True:
yield value

这样一个简单的无限生成器就写好了,很简单对吧!虽然它像普通函数,但是要注意,调用生成器函数并不会运行该函数,而是仅仅创建并返回一个生成器对象:

1
2
>>> repeater('Hey')
<generator object repeater at 0x00000188C0A32E40>

只有在生成器对象上调用next()时才会执行生成器函数中的代码:

1
2
3
>>> generator_obj = repeater('Hey')
>>> next(generator_obj)
'Hey'

return 和 yield 的区别
1.当函数内部调用return语句时,控制权会永久性地交还给函数的调用者。在调用yield时,虽然控制权也是交还给函数的调用者,但只是暂时的。
2.return语句会丢弃函数的局部状态,而yield语句会暂停该函数并保留其局部状态。

有限生成器

在基于类的迭代器中,我们通过抛出StopIteration异常来表示迭代结束,那么在生成器中呢?
由于生成器与基于类的迭代器完全兼容,所以背后仍然使用这种方法。
或者,我们可以使用更好的接口。如果控制流从生成器函数中返回,但不是通过yield语句,那么生成器就会停止:

1
2
3
4
5
6
7
8
9
10
11
12
13
>>> def repeat_three_times(value):
yield value
yield value
yield value
>>> iterator = repeat_three_times('Hey there')
>>> next(iterator)
'Hey there'
>>> next(iterator)
'Hey there'
>>> next(iterator)
StopIteration
>>> next(iterator)
StopIteration

知道了这些知识,我们就能够转化一下BoundedRepeater类了,让它变成一个生成器:

1
2
3
4
5
6
7
def bounded_repeater(value, max_repeats):
count = 0
while True:
if count >= max_repeats:
return
count += 1
yield value

当然,还有更简便的版本,利用Python为每个函数的末尾添加一个隐式return None语句的特性,我们甚至可以这样写:

1
2
3
def bounded_repeater(value, max_repeats):
for i in range(max_repeats):
yield value

生成器表达式

生成器表达式通过一行代码来定义迭代器。看到这个,我们很快就能联想到列表解析式。是的,生成器表达式的确和列表表达式非常相似:

1
2
>>> listcomp = ['Hello' for i in range(3)]
>>> genexpr = ('Hello' for i in range(3))

和列表解析式相同,生成器表达式也可以添加条件从而来过滤一些元素:

1
2
3
4
5
6
7
8
>>> even_squares = (x * x for x in range(10) if x % 2 == 0)
>>> for x in even_squares:
... print(x)
0
4
16
36
64

和列表解析式不同,生成器表达式不会构造列表对象,而是像基于类的迭代器或生成器函数那样“即时”生成值。

1
2
3
4
>>> listcomp
['Hello', 'Hello', 'Hello']
>>> genexpr
<generator object <genexpr> at 0x00000188C0A32DD0>

我们也可以归纳一下,得出一个生成器表达式的模板:

1
genexpr = (expression for item in collection if condition)

这个模板也可以看成下边的生成器函数:

1
2
3
4
def generator():
for item in collection:
if condition:
yield expression

生成器表达式一经使用就不能重新启动或重用,所以在某些情况下生成器函数或基于类的迭代器更加合适。

内联生成器表达式

因为生成器表达式也是表达式,所以可以与其他语句一起内联使用。例如,可以定义一个迭代器并立即在for循环中使用:

1
2
for x in ('Test' for i in range(3)):
print(x)

另外还有一个语法技巧可以美化生成器表达式。如果生成器表达式是作为函数中的单个参数使用,那么可以删除生成器表达式外层的括号:

1
2
3
4
>>> sum((x * 2 for x in range(10)))
90
>>> sum(x * 2 for x in range(10))
90

迭代器链

Python中的迭代器还有另一个重要特性:可以链接多个迭代器,从而编写高效的数据处理“管道”。
普通函数只会产生一次返回值,而生成器会多次产生结果。可以认为生成器在整个生命周期中能产生值的“流”。下面用例子来说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def integers():
for i in range(1, 9):
yield i

def squared(seq):
for i in seq:
yield i * i

def negated(seq):
for i in seq:
yield -i

>>> chain = negated(squared(integers()))
>>> list(chain)
[-1, -4, -9, -16, -25, -36, -49, -64]

在这里,先是由integers()生成器产生值的“流”,然后送入squared()生成器,squared()生成器根据值再生成值,再送入negated()生成器中生成最后的值。

在生成器链中,每次只处理一个元素,这说明链中的处理步骤之间没有缓冲区。下面是详细步骤:

  1. integers生成器产生一个值,如3.
  2. 这个值“激活”squared生成器来处理,得到3 * 3 = 9,并将其传递到下一阶段。
  3. 由squared生成器产生的平方数立即送入negated生成器,将其修改为-9并再次yield。

一些无关的话

放假后真的是太懒啦,也是各种作息混乱加上在外游玩又或者干脆睡觉摆烂以至于根本没怎么学(土下座.jpg)。下次更新也是这本好书的最后一章啦,一定会在开学前学完的(确信),就这样啦~


  1. 《Python Tricks 深入理解python特性》 [德]达恩·巴德尔 人民邮电出版社

这里有一只爱丽丝

希望本文章能够帮到您~


Python学习笔记——循环与迭代
https://map1e-g.github.io/2022/07/19/python-learning-5/
作者
MaP1e-G
发布于
2022年7月19日
许可协议