Python中迭代器&生成器的“奇技淫巧“
Posted 山河已无恙
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python中迭代器&生成器的“奇技淫巧“相关的知识,希望对你有一定的参考价值。
写在前面
- 和小伙伴们分享一些Python
迭代器
和生成器
的笔记 - 博文为
《Python Cookbook》
读书笔记整理 - 博文内容涉及:
- 不用for循环手动访问迭代器中的元素
- 委托代理迭代(自定义可迭代对象如何迭代)
- 用生成器创建新的迭代模式
- 如何实现一个迭代协议
- 反向迭代
- 定义自定义行为的生成器函数
- 对迭代器做切片操作
- 对可迭代对象自定义行为过滤
- 迭代所有可能的组合或排列
- 以索引-值对的形式迭代序列
- 同时迭代多个可迭代对象
- 在不同的可迭代对象中进行合并迭代
- 解构迭代(扁平化处理嵌套型的可迭代对象)
- 合并多个有序迭代对象,再对整个有序迭代对象进行迭代
- 用迭代器取代while循环
- 食用方式:
- 了解Python基本语法即可
- 理解不足小伙伴帮忙指正
一厢情愿,就得愿赌服输。 ——八月长安《最好的我们》
迭代器和生成器
关于迭代器小伙伴们应该不陌生,但是生成器貌似是python特有的,
Python 的迭代器语法简单,部分思想和Java8 Stream API有类似的地方(当然,Python要比Java年长),引入lambda表达式,predicate,函数式编程,行为参数化等可以做很多事情,同时和JAVA一样,对迭代行为进行了语法封装。但是本质上还是通过调用可迭代对象的迭代器来实现。
Python 的生成器yield,通过yield
、yield from
语法,可以很简单处理一些深度遍历的问题。
学习环境版本
┌──[root@vms81.liruilongs.github.io]-[~]
└─$python3
Python 3.6.8 (default, Nov 16 2020, 16:55:22)
[GCC 4.8.5 20150623 (Red Hat 4.8.5-44)] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>
手动访问迭代器中的元素
当你希望遍历一个可迭代对象中的所有元素,但是却不想使用 for 循环。
为了手动的遍历可迭代对象,使用 next() 函数并在代码中捕获 StopIteration 异常
。比如,下面的例子手动读取一个文件中的所有行:
#!/usr/bin/env python3
# -*- encoding: utf-8 -*-
"""
@File : Untitled-1.py
@Time : 2022/05/23 00:18:55
@Author : Li Ruilong
@Version : 1.0
@Contact : 1224965096@qq.com
@Desc : None
"""
# here put the import lib
def manual_iter():
with open('/etc/passwd') as f:
try:
while True:
line = next(f)
print(line, end='')
except StopIteration:
pass
manual_iter()
执行输出
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
StopIteration
用来指示迭代的结尾。然而,如果你手动使用上面演示的next()
函数的话,你还可以通过返回一个指定值来标记结尾,比如 None 。
with open('/etc/passwd') as f:
while True:
line = next(f)
if line is None:
break
print(line, end='')
下面的列表在数据的末尾插入一个None值,那么你可以利用上面的if line is None: break
来提早的结束迭代,避免异常的捕获
>>> items = [1, 2, 3,None]
>>> it = iter(items)
>>> next(it)
1
>>> next(it)
2
>>> next(it)
3
>>> next(it)
>>> next(it)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>>
关于迭代器的原理,有三个必不可少的元素,
- 一个需要迭代的列表items
- 通过
iter()
方法来获取一个可迭代对象的迭代器 - 通过
next()
方法来获取当前可迭代的元素
>>> items = [1, 2, 3]
>>> it = iter(items)
>>> next(it)
1
>>> next(it)
2
>>> next(it)
3
>>> next(it)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
委托代理迭代
当你构建了一个自定义容器对象,里面包含有列表、元组或其他可迭代对象。你想直接在你的这个新容器对象上执行迭代操作如何处理
所谓的委托代理迭代,即通过重写迭代对象的 __iter__
魔法方法,增加新的迭代行为。而所谓的新迭代行为即将迭代操作代理到容器内部的对象上去。
#!/usr/bin/env python3
# -*- encoding: utf-8 -*-
"""
@File : Untitled-1.py
@Time : 2022/05/24 00:20:38
@Author : Li Ruilong
@Version : 1.0
@Contact : 1224965096@qq.com
@Desc : None
"""
# here put the import lib
class Node:
def __init__(self, value):
self._value = value
self._children = []
def __repr__(self):
return 'Node(!r)'.format(self._value)
def add_child(self, node):
self._children.append(node)
def __iter__(self):
return iter(self._children)
# Example
if __name__ == '__main__':
root = Node(0)
child1 = Node(1)
child2 = Node(2)
root.add_child(child1)
root.add_child(child2)
# Outputs Node(1), Node(2)
for ch in root:
print(ch)
通过运行我们可以看到
┌──[root@vms81.liruilongs.github.io]-[~/python_cookbook]
└─$./622.py
Node(1)
Node(2)
Python的迭代器协议
需要 __iter__
方法返回一个实现了next()方法
的迭代器对象
。
如果你只是迭代遍历其他容器的内容,你无须担心底层是怎样实现的。你所要做的只是传递迭代请求既可。
这里的iter()函数
的使用简化了代码,iter()
只是简单的通过调用s.__iter__()
方法来返回对应的迭代器对象,就跟1en(s)会调用s.len()原理是一样的。
用生成器创建新的迭代模式
实现一个自定义迭代模式
,跟普通的内置函数比如range() , reversed()
不一样。
如果想实现一种新的迭代模式,使用一个生成器函数
来定义它。下面是一个生产某个范围内浮点数的生成器:
>>> def frange(start, stop, increment):
... x = start
... while x < stop:
... yield x
... x += increment
...
>>> for n in frange(0, 4, 0.5):
... print(n)
...
0
0.5
1.0
1.5
2.0
2.5
3.0
3.5
>>>
一个函数中需要有一个 yield 语句
即可将其转换为一个生成器。跟普通函数不同的是,生成器只能用于迭代操作。可以把生成器理解为函数中途的retuen, 函数块中的代码可以看做是一个流水线,那么yield就是流水线中某个环境给调用方法者的反馈,但是他并不会影响流水线。
在来看一个Demo
>>> def countdown(n):
... print('Starting to count from', n)
... while n > 0:
... yield n
... n -= 1
... print('Done!')
...
>>> c = countdown(3)
>>> next(c)
Starting to count from 3
3
>>> next(c)
2
>>> next(c)
1
>>> next(c)
Done!
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>> c
<generator object countdown at 0x7fd33ac44200>
>>>
一个生成器函数主要特征是它只会回应在迭代中使用到的 next 操作
。一旦生成器函数返回退出,迭代终止。
实现迭代协议
构建一个能支持迭代操作的自定义对象,并希望找到一个能实现迭代协议的简单方法
#!/usr/bin/env python3
# -*- encoding: utf-8 -*-
"""
@File : Untitled-1.py
@Time : 2022/05/24 22:41:58
@Author : Li Ruilong
@Version : 1.0
@Contact : 1224965096@qq.com
@Desc : None
"""
class Node:
def __init__(self, value):
self._value=value
self._children=[]
def __repr__(self):
return ' Node(!r)'.format(self._value)
def add_child(self, node):
self._children. append(node)
def __iter__(self):
return iter(self._children)
def depth_first(self):
yield self
for c in self:
yield from c.depth_first()
# Example
if __name__ == '__main__':
root = Node(0)
child1 = Node(1)
child2 = Node(2)
root.add_child(child1)
root.add_child(child2)
child1.add_child(Node(3))
child1.add_child(Node(4))
child2.add_child(Node(5))
for ch in root.depth_first():
print(ch)
在这段代码中,depth_first()
方法是点睛之笔。它首先返回自己本身
并迭代每一个子节点并通过调用子节点的depth_first()
方法 (使用yield from
语句) 返回对应元素
def depth_first(self):
yield self
for c in self:
yield from c.depth_first()
┌──[root@vms81.liruilongs.github.io]-[~/python_cookbook]
└─$vim 644.py
┌──[root@vms81.liruilongs.github.io]-[~/python_cookbook]
└─$chmod +x 644.py
┌──[root@vms81.liruilongs.github.io]-[~/python_cookbook]
└─$./644.py
Node(0)
Node(1)
Node(3)
Node(4)
Node(2)
Node(5)
Python 的迭代协议
要求一个iter ()
方法返回一个特殊的迭代器对象
,这个迭代器对象实现了next ()
方法并通过 StopIteration
异常标识迭代的完成。
反向迭代
反方向迭代一个序列
使用内置的reversed()
函数,
>>> a= [1,2,3,4]
>>> for i in reversed(a):
... print(i)
...
4
3
2
1
>>>
反向迭代
仅仅当对象的大小可预先确定或者对象实现了 __reversed__()
的特殊方法时才能生效。如果两者都不符合,那你必须先将对象转换为一个列表才行.
>>> f = open('/etc/passwd')
>>> for line in reversed(list(f)):
... print(line,end='')
...
opensips:x:997:993:OpenSIPS SIP Server:/var/run/opensips:/sbin/nologin
oprofile:x:16:16:Special user account to be used by OProfile:/var/lib/oprofile:/sbin/nologin
nfsnobody:x:65534:65534:Anonymous NFS User:/var/lib/nfs:/sbin/nologin
rpcuser:x:29:29:RPC Service User:/var/lib/nfs:/sbin/nologin
rpc:x:32:32:Rpcbind Daemon:/var/lib/rpcbind:/sbin/nologin
......
自定义实现反向迭代,通过在自定义类上实现__reversed()__
方法来实现反向迭代。
#!/usr/bin/env python3
# -*- encoding: utf-8 -*-
"""
@File : Untitled-1.py
@Time : 2022/05/24 22:41:58
@Author : Li Ruilong
@Version : 1.0
@Contact : 1224965096@qq.com
@Desc : None
"""
# here put the import lib
class Countdown:
def __init__(self, start):
self.start = start
# Forward iterator
def __iter__(self):
n = self.start
while n > 0:
yield n
n -= 1
# Reverse iterator
def __reversed__(self):
n = 1
while n <= self.start:
yield n
n += 1
print(format('逆序','*>20'))
for rr in reversed(Countdown(5)):
print(rr)
print(format('正序','*>20'))
for rr in Countdown(5):
print(rr)
简单分析一下这个逆序的迭代器,魔法方法__iter__
返回一个可迭代的对象,这里通过生成器来实现,当n>0的时候,通过生成器返回迭代元素。魔法方法__reversed__
实现一个逆序的迭代器,原理和默认迭代器基本相同,不同的是,默认迭代器是默认调用,而逆向迭代器是主动调用
┌──[root@vms81.liruilongs.github.io]-[~/python_cookbook]
└─$./653.py
******************逆序
1
2
3
4
5
******************正序
5
4
3
2
1
定义带有额外状态的生成器函数
定义一个生成器函数,但是它会调用某个你想暴露给用户使用的外部状态值。
如果想让生成器暴露外部状态给用户,可以简单的将它实现为一个类,然后把生成器函数
放到__iter__()
方法中过去,简单来讲就是上面我们演示的代码,通过生成器来模拟next()
方法行为
#!/usr/bin/env python3
# -*- encoding: utf-8 -*-
"""
@File : Untitled-1.py
@Time : 2022/05/24 22:41:58
@Author : Li Ruilong
@Version : 1.0
@Contact : 1224965096@qq.com
@Desc : None
"""
# 这里的`deque(maxlen=N)创建了一个固定长度的双端队列
from collections import deque
def count(n):
while True:
yield n
n += 1
class linehistory:
def __init__(self, lines, histlen=3):
self.lines = lines
self.history = deque(maxlen=histlen)
def __iter__(self):
for lineno, line in enumerate(self.lines, 1):
self.history.append((lineno, line))
yield line
def clear(self):
self.history.clear()
if __name__ == "__main__":
with open('/etc/services') as f:
lines = linehistory(f)
for line in lines:
if '8080' in line:
for lineno, hline in lines.history:
print(':'.format(lineno, hline), end='')
这里的deque(maxlen=N)创建了一个固定长度的双端队列
,用于存放要保留的数据,把文件的所有的行数据存放到lines里,默认队列的大小是3,然后通过for循环迭代,在获取迭代器的方法里,我们可以看到通过enumerate
来获取迭代对象和索引,然后放到队列里,通过yield模拟next方法返回迭代元素,所以队列里存放的默认为当前元素的前两个元素,
┌──[root@vms81.liruilongs.github.io]-[~/python_cookbook]
└─$./662.py
553:xfs 7100/tcp font-service # X font server
554:tircproxy 7666/tcp # Tircproxy
555:webcache 8080/tcp http-alt # WWW caching service
554:tircproxy 7666/tcp # Tircproxy
555:webcache 8080/tcp http-alt # WWW caching service
556:webcache 8080/udp http-alt # WWW caching service
┌──[root@vms81.liruilongs.github.io]-[~/python_cookbook]
└─$(cat -n /etc/services | grep -B2 -m 1 8080;cat -n /etc/services | grep -B1 -m 2 8080)
553 xfs 7100/tcp font-service # X font server
554 tircproxy 7666/tcp # Tircproxy
555 webcache 8080/tcp http-alt # WWW caching service
554 tircproxy 7666/tcp # Tircproxy
555 webcache 8080/tcp http-alt # WWW caching service
556 webcache 8080/udp http-alt # WWW caching service
如果你在迭代操作时不使用 for 循环
语句,那么你得先调用iter()
函数获取迭代器,然后通过next来获取迭代元素
对迭代器做切片操作
得到一个由迭代器生成的切片对象,但是标准切片操作并不能做到。
函数itertools.islice()
正好适用于在迭代器和生成器上做切片操作
>>> def count(n):
... while True:
... yield n
... n += 1
...
>>> c = count(0)
>>> c[10:20]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'generator' object is not subscriptable
>>> import itertools
>>> for x in itertools.islice(c,5,10):
... print(x)
...
5
6
7
8
9
>>>
迭代器和生成器
不能使用标准的切片操作
,因为它们的长度事先我们并不知道
(并且也没有实现索引)。函数islice()
返回一个可以生成指定元素的迭代器,它通过遍历并丢弃直到切片开始索引位置的所有元素。然后才开始一个个的返回元素,并直到切片结束索引位置。islice()
会消耗掉传入的迭代器中的数据。必须考虑到迭代器是不可逆的这个事实。
跳过可迭代对象中的前一部分元素
遍历一个可迭代对象,但是它开始的某些元素你并不感兴趣,想跳过它们
itertools 模块
中有一些函数可以完成这个任务。 首先介绍的是itertools.dropwhile()
函数。使用时,你给它传递一个函数对象和一个可迭代对象。它会返回一个迭代器对象,丢弃原有序列中直到函数返回 True 之前的所有元素,然后返回后面所有元素。
类似一个过滤器,返回满足条件的数据
┌──[root@vms81.liruilongs.github.io]-[~/python_cookbook]
└─$tee temp.txt <<- EOF
> #sdfsdf
> #dsfsf
> #sdfsd
> sdfdsf
> sdfs
> EOF
#sdfsdf
#dsfsf
#sdfsd
sdfdsf
sdfs
>>> with open('temp.txt') as f:
... for line in dropwhile(lambda line: line.startswith('#'),f):
... print(line,end=' ')
...
sdfdsf
sdfs
>>>
如果你已经明确知道了要跳过的元素的个数的话,那么可以使用 itertools.islice()
来代替
>>> from itertools import islice
>>> items = ['a', 'b', 'c', 1, 4, 10, 15]
>>> for x in islice(items, 3, None):
... print(x)
...
1
4
10
15
>>>
islice() 函数最后那个 None 参数指定了你要获取从第 3 个到最后的所有元素,如果 None 和 3 的位置对调,意思就是仅仅获取前三个元素恰恰相反,(这个跟切片的相反操作 [3:] 和 [:3] 原理是一样的)。
迭代所有可能的组合或排列
想迭代遍历一个集合中元素的所有可能的排列或组合
itertools模块
提供了三个函数来解决这类问题。其中一个是itertools.permutations()
,它接受一个集合并产生一个元组序列
,每个元组由集合中所有元素的一个可能排列
组成。也就是说通过打乱集合中元素排列顺序生成一个元组
>以上是关于Python中迭代器&生成器的“奇技淫巧“的主要内容,如果未能解决你的问题,请参考以下文章