Python关于迭代器小小研究

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

0.

起因

写了一个关于数组的力扣题556:重塑矩阵,在实现的结果中,虽然执行用时已经达标(排名前20%)但是内存消耗竟然达到了13.9MB(后20%)这是无法接受的。在查阅了参考解法后发现了一些魔法,类似于使用可迭代的类来让内存占用不大。

众所周知,使用range()来迭代时会先生成一个list来储存数字,因此range的数越大占用内存越多,导致内存疯长,而使用迭代器就不会导致这个问题。

题目原题

我的答案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution(object):
def matrixReshape(self, mat, r, c):
"""
:type mat: List[List[int]]
:type r: int
:type c: int
:rtype: List[List[int]]
"""
m = len(mat)
n = len(mat)
if m*n == r*c:
empty = []
for i in mat:
empty += i
result = []
for i in range(r):
result.append(empty[:c])
del empty[:c]
return result
else:
return mat

参考解法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution:
def matrixReshape(self, mat: List[List[int]], r: int, c: int) -> List[List[int]]:

if r * c != len(mat[0]) * len(mat):
return mat

def travel():
for r in range(len(mat)):
for c in range(len(mat[0])):
yield mat[r][c]

gen = travel()
return [[next(gen) for j in range(c)] for i in range(r)]
"""
作者:tenderdanger
链接:https://leetcode.cn/problems/reshape-the-matrix/solution/python-yi-chong-qi-xing-guai-zhuang-de-z-z06d/
"""

可迭代的类

I

参考菜鸟的示例,提出了一个可迭代的类来完成斐波那契数列的迭代:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#!/usr/bin/python
# -*- coding: UTF-8 -*-

class Fab(object):

def __init__(self, max):
self.max = max
self.n, self.a, self.b = 0, 0, 1

def __iter__(self):
return self

def next(self):
if self.n < self.max:
r = self.b
self.a, self.b = self.b, self.a + self.b
self.n = self.n + 1
return r
raise StopIteration()

for n in Fab(5):
print n

有点看不懂,因为在类里面写的函数如果不通过方法名调用的话应该不会执行才对,但是这里面的代码却能直接迭代,遂继续研究。后面可以知道,如果类中加入__iter__()方法且返回self那么这个类就是可迭代的类,每次迭代的时候会自动调用__next___()函数来返回下一个数。

II

通过观察示例,搜索教程了解到,在类里面使用 __iter__(self)方法可以使类有迭代特性,如上面示例代码中的10~11行,而使用__next__(self)则是返回迭代结果的方法,其中要在init中加入一个计数器来防止越界。而且要使用rasie来结束迭代。

下面详细解释,python在调用for…in…循环进行遍历时,对于类的实例的访问过程:
——检查该类是否具有迭代性,即调用类中的iter()方法
—— 通过iter()函数获取类中的迭代器,通过调用迭代器中的next()方法,获取下一个值,并赋值给result
——遇到StopIteration的异常后循环结束

注意,__iter__()中的返回对象就是迭代化的对象,一般使用self来指向类自己,上面示例代码中也可以拆分为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 class Fruit():
def __init__(self):
self.item = list()

def add(self,name):
self.item.append(name)

# 使函数具有可迭代性
def __iter__(self):
return myIterator(self)
=======================================================
class myIterator():

def __init__(self,fru):
self.fru = fru
self.count = 0

def __next__(self):
if self.count < len(self.fru.item):
result = self.fru.item[self.count]
self.count += 1
return result
else:
raise StopIteration

上面中,return了另一个类,使得另一个类变成可迭代对象,并把Fruit类的self传入其中以便next调用、迭代。

如果本身就是迭代对象,可以不写__iter__(),但是迭代的时候得手动套一个next()在外面

参考来源

__iter__()的作用是将迭代对象本身返回出去,而__next__()的作用是为了让外部能够调用该函数来获取迭代对象的下一个元素。所以上面的类中也可以不写__iter__()函数,同样也能实现迭代。

其他描述<实际上我觉得这个描述更好,建议查看原文>

可迭代的对象首先通过 __iter__ 生成一个迭代器, 然后对该迭代器不断调用 __next__方法,逐条返回迭代数据,从而实现对数据的迭代。

iter()方法 调用的为对象内部的 __iter__()方法, next() 调用的对象内部的 __next()__方法,所以当我们自定义迭代器时,需要使用这两种方法。

迭代类的指令:

除了已经讨论过的__iter__()__next__()之外,还有一些奇妙的指令来构成迭代的全部。

yield

还是斐波那契的构造函数,但是这次我们不构造类,太复杂了,可否直接拿到一个可迭代的函数?

1
2
3
4
5
6
7
8
9
10
def fab(max): 
n, a, b = 0, 0, 1
while n < max:
yield b # 执行到这一步会直接把 b 作为迭代对象返回,并且中断函数操作
# 下一次调用时(使用next)时从这一步开始,直到再遇到yeild
a, b = b, a + b
n = n + 1

for n in fab(5):
print n

参考资料

​ 简单地讲,yield 的作用就是把一个函数变成一个 generator,带有 yield 的函数不再是一个普通函数,Python 解释器会将其视为一个 generator。

​ 调用 fab(5) 不会执行 fab 函数,而是返回一个 iterable 对象!

​ 在 for 循环执行时,每次循环都会执行 fab 函数内部的代码,执行到 yield b 时,fab 函数就返回一个迭代值,下次迭代时,代码从 yield b 的下一条语句继续执行,而函数的本地变量看起来和上次中断执行前是完全一样的,于是函数继续执行,直到再次遇到 yield。

使用了yield的函数将会变成一个generator对象,自动获得next()方法,你可以使用函数名.next()来获得下一个迭代数,或者使用for遍历的方式获得所有的迭代对象。

很好的发言

【简单点说,yield就是return的兄弟,只不过二者有点小区别】

【在调用生成器函数的过程中,每次遇到 yield 时函数会暂停并保存当前所有的运行信息(保 留局部变量),返回yield的值, 并在下一次执行next()方法时从当前位置继续运行,直到生 成器被全部遍历完。】

你当然可以放置多个yield来多步返回结果!

使用next()函数对其迭代,越界后会报StopIteration异常,但是使用for遍历会自动处理异常,正常结束循环

生成器的内置方法:

生成器本身有几个内置方法,可以以函数名.方法名的方式调用。

  • generator.__next__():执行 for 时调用此方法,每次执行到 yield 就会停止,然后返回 yield 后面的值,如果没有数据可迭代,抛出 StopIterator 异常,for 循环结束
  • generator.send(value):外部传入一个值到生成器内部,改变 yield 前面的值
  • generator.throw(type[, value[, traceback]]):外部向生成器抛出一个异常
  • generator.close():关闭生成器

首先得实例化迭代函数

1
2
3
4
5
6
7
def gen(n):
for i in range(n):
print('yield before')
yield i
print('yield after')

g = gen(3) # 创建一个生成器

然后直接使用g.<def>的方法就可以调用上述的方法。

g.__next__()其实就是要求生成器进行一次迭代,跟next()差不多

重要功能

使用yeild还可以控制迭代:

1
2
3
4
5
6
7
def gen():
i = 1
while True:
j = yield i
i *= 2
if j == -1:
break

如果用for i in gen()来迭代,那么就算一个无限循环的比值为2的等比数列。停止的方法就是通过g.sen( -1 )的方式把-1传入,则满足j=-1的条件,可以退出迭代。当然,你也可以用类似的方式,throw()一个异常进去。

如果使用了*.close()则迭代器关闭,无法继续迭代,不过很少使用。

判断是否为生成器(generator)

使用以下代码来判断是否是一个生成器:

1
2
3
from inspect import isgeneratorfunction

isgeneratorfunction(def)

就可以判断def是否为生成器

其他方法:

1
2
3
4
5
>>>import types 
>>> isinstance(fab, types.GeneratorType)
False
>>> isinstance(fab(5), types.GeneratorType)
True

注意

如上面的示例代码中,使用yeild后的函数fab有一点区别:

fab是一个生成器函数(generator function),但是fab(5)是调用fab返回的生成器对象(generator)

生成器函数本身是无法迭代的,但是生成器对象是可以迭代的

还有一个好处是每次调用生成器函数时都会生成一个新的可迭代对象,不用担心指针跑到底导致无法再遍历。

其他

一般生成器函数不使用return,如果使用,则遇到return时自动抛出StopIteration,否则等待函数执行完毕后抛出。

应用

==懒得写了==

I – 大文件读取

在文件读取中,如果是小文件,一般的read()已经够用,但是当不知道打开的文件有多大时,直接read()可能导致内存爆栈,使用yield是个小心读取的好办法

II – 大集合生成

如果你想生成一个非常大的集合,如果使用 list 创建一个集合,这会导致在内存中申请一个很大的存储空间

III – 简洁多逻辑代码

参考代码解析

通过以上的学习,已经知道什么是可迭代类了,现在再看这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution:
def matrixReshape(self, mat: List[List[int]], r: int, c: int) -> List[List[int]]:

if r * c != len(mat[0]) * len(mat):
return mat

def travel():
for r in range(len(mat)):
for c in range(len(mat[0])):
yield mat[r][c]

gen = travel()
return [[next(gen) for j in range(c)] for i in range(r)]

I

先从这里看:

1
2
if r * c != len(mat[0]) * len(mat):
return mat

作者是倒置的,如果矩阵面积不同则返回原始矩阵,而我是矩阵面积相同执行替换,这个无需多言

II

定义了一个类里面的函数(生成器函数),先遍历行(R),然后迭代地返回每一个列(mat[r][c])值:

1
2
3
4
def travel():
for r in range(len(mat)):
for c in range(len(mat[0])):
yield mat[r][c]

III

这里是最复杂的部分,因为嵌套了多层:

1
2
gen = travel()
return [[next(gen) for j in range(c)] for i in range(r)]

第一句是很正常的生成一个迭代对象,整个函数最后的返回值是一坨奇怪的东西。

[next(gen) for j in range(c)]:跟之前的列表迭代生成不同,这个jnext根本没有关系,编译器也提醒我这个junused的,但是这个结构实际上能自动的迭代c次,迭代结果是一个列表。

实际上就是:把c个值塞到一个列表里,而travel中因为是双层遍历,返回的都是正确的矩阵值,不会导致越界,所以是很聪明的办法!

因此最大一层的迭代[…… for i in range(r)]其实也是一个意思,让里面的迭代体……循环R

==虽然我还是不知道为啥 i 跟 j 会提示WARN==

IV – 问题修复

编译器指出这里有错误,因此跟着指示查询了Python:S1481规则(我也不知道叫什么),从中找到了这种如果不需要使用 i 或 j 时迭代需要的标志,全部使用_即可,因此代码应该改为[[next(gen) for _ in range(c)] for _ in range(r)]就不会提示WARN

S1481


Python关于迭代器小小研究
https://qlozin.top/2022/08/14/Python高级玩意/
作者
QLozin
发布于
2022年8月15日
更新于
2024年8月11日
许可协议