本系列文章的代码都存放在Github项目:。
这一部分内容是《Fluent Python》目前为止最长的篇幅,我也花了大半天时间来阅读,内容的确庞杂,所以在提炼整理上可能会有所疏漏,请多包涵。
迭代技术无疑在Python中占有相当的地位。平时我们在写代码的时候,大多数时间也是话费在for
或者foreach
之类的循环语句上,而Python更进一步,在语言结构中直接整合了迭代技术,让我们可以更容易地在不同类型间使用类似的简单语法就可以进行迭代操作。
接下来我们通过Python迭代技术的核心概念:可迭代对象、迭代器和生成器来深入理解。
可迭代对象和迭代器
在中我们简单说明过可迭代对象和迭代器,现在我们从语言设计层面来理解这两个概念,先来看一下UML类图:
相当简单,Iterable
抽象类代表可迭代对象,Iterator
抽象类代表迭代器。
其中可迭代对象仅有一个抽象方法,返回一个迭代器。而迭代器有两个方法,抽象方法__next__
作为主要的迭代逻辑,将依次返回元素,完成迭代。而继承自可迭代对象的__iter__
方法被重写,用这样的方式实现:
def __iter__(self):
return self
这是因为我们在Python中会大量使用for/in
或者__init__
之类的情况来接收和使用可迭代对象,在进行迭代的时候后解释器会调用可迭代对象的__iter__
获取一个迭代器,进行具体迭代工作,而这个迭代器本身自然也可以作为一个可迭代对象来使用,所以才会是以上的这种类结构。
下面我们具体分析一下可迭代对象和迭代器。
可迭代对象
__iter__
上面我们说了,可迭代对象必须要实现__iter__
,而我们之前在实现过序列协议,只实现了__getitem__
和__len__
,但是依然可以迭代。
这是因为“Python偏爱序列”。
实际上是因为Python为了兼容旧的序列协议作出的妥协,对于没有实现__iter__
方法的序列,解释器会自动尝试通过__getitem__
并使用从零开始的下标来构建一个迭代器。
我们用下面这个例子说明:
import re
import reprlib
class Sentence():
RE_WORD = re.compile('\w+')
def __init__(self, text) -> None:
self.text = text
self.words = Sentence.RE_WORD.findall(text)
def __getitem__(self, index):
return self.words[index]
def __len__(self):
return len(self.words)
Sentence
是一个简单序列,接收字符串,并分割成单词。
测试一下:
from sentence import Sentence
s = Sentence("Today is a good day!")
for word in s:
print(word)
sIter = iter(s)
print(sIter)
for word in sIter:
print(word)
# Today
# is
# a
# good
# day
# <iterator object at 0x000002870BB7B4C0>
# Today
# is
# a
# good
# day
可以看到,虽然Sentence
没有实现__iter__
方法,但依然可以正常迭代,并且还可以用iter
函数获取到迭代器,获取到的迭代器同样可以正常用于迭代。
但需要注意的是,虽然Python通过这种方式兼容了序列,但是在事实上序列和可迭代对象是没有继承关系的,我们可以通过下面的代码进行验证:
from collections import abc
print(isinstance(s,abc.Iterable))
print(issubclass(Sentence,abc.Iterable))
# False
# False
迭代器
Python的迭代器相当简洁,只有两个方法,使用的时候也是同样简单:
sIter = iter(s)
already = []
while True:
print("{!s}^{!s}".format(already, sIter))
item = next(sIter)
already.append(item)
# []^<iterator object at 0x000001ABA4B02F70>
# ['Today']^<iterator object at 0x000001ABA4B02F70>
# ['Today', 'is']^<iterator object at 0x000001ABA4B02F70>
# ['Today', 'is', 'a']^<iterator object at 0x000001ABA4B02F70>
# ['Today', 'is', 'a', 'good']^<iterator object at 0x000001ABA4B02F70>
# ['Today', 'is', 'a', 'good', 'day']^<iterator object at 0x000001ABA4B02F70>
# Traceback (most recent call last):
# File "D:\workspace\python\python-learning-notes\note31\test.py", line 29, in <module>
# item = next(sIter)
# StopIteration
这里用
^
表示当前迭代指针的位置。
我们使用next
逐步从迭代器中获取元素,在获取完最后一个元素后,会抛出一个StopIteration
异常,如果我们使用的是for/in
语句,解释器会自动处理这个异常并退出迭代,如果是上面这样手动迭代,就需要处理这个异常:
sIter = iter(s)
already = []
while True:
print("{!s}^{!s}".format(already, sIter))
try:
item = next(sIter)
except StopIteration:
break
already.append(item)
# []^<iterator object at 0x000001D67CD6B4F0>
# ['Today']^<iterator object at 0x000001D67CD6B4F0>
# ['Today', 'is']^<iterator object at 0x000001D67CD6B4F0>
# ['Today', 'is', 'a']^<iterator object at 0x000001D67CD6B4F0>
# ['Today', 'is', 'a', 'good']^<iterator object at 0x000001D67CD6B4F0>
# ['Today', 'is', 'a', 'good', 'day']^<iterator object at 0x000001D67CD6B4F0>
还有一点我们需要注意,Python的迭代器仅实现了一个__next__
方法,很简洁,这是优点。但同时意味着功能单一,比如很多其他语言中的迭代器支持“重置”操作,我们可以随时重置迭代器然后从头重新迭代,但Python不行,当一个迭代器迭代完毕后就不能继续使用了,如果你还需要,只能再次使用iter
函数获取一个新的迭代器。
迭代器和可迭代对象其实是一种设计模式:迭代器模式。只不过Python通过深入内置这种设计模式让它和Python融为一体。
同样的,我们可以使用“粗苯”的方式来手动实现一个迭代器模式来说明这其中的机制。
迭代器模式
from ast import Index
import re
class SentenceV2():
RE_WORD = re.compile('\w+')
def __init__(self, text) -> None:
self.text = text
self.words = SentenceV2.RE_WORD.findall(text)
def __iter__(self):
return StenceIterator(self)
class StenceIterator():
def __init__(self, sentence) -> None:
self.sentence = sentence
self.index = 0
def __next__(self):
try:
result = self.sentence.words[self.index]
except IndexError:
raise StopIteration
self.index += 1
return result
def __iter__(self):
return self
我们实现了经典的迭代器模式,这里的StenceIterator
就是一个具体的迭代器。
我们看下测试结果:
from sentence_v2 import SentenceV2,StenceIterator
s = SentenceV2("Today is a good day!")
sIter = iter(s)
already = []
while True:
print("{!s}^{!s}".format(already, sIter))
try:
item = next(sIter)
except StopIteration:
break
already.append(item)
print(issubclass(SentenceV2, abc.Iterable))
print(isinstance(s, abc.Iterable))
print(issubclass(StenceIterator, abc.Iterator))
print(isinstance(sIter, abc.Iterator))
# []^<sentence_v2.StenceIterator object at 0x000002200527E550>
# ['Today']^<sentence_v2.StenceIterator object at 0x000002200527E550>
# ['Today', 'is']^<sentence_v2.StenceIterator object at 0x000002200527E550>
# ['Today', 'is', 'a']^<sentence_v2.StenceIterator object at 0x000002200527E550>
# ['Today', 'is', 'a', 'good']^<sentence_v2.StenceIterator object at 0x000002200527E550>
# ['Today', 'is', 'a', 'good', 'day']^<sentence_v2.StenceIterator object at 0x000002200527E550>
# True
# True
# True
# True
可以正常迭代,而且StenceV2
和SentenceIterator
的类对象和实例都通过了issubclass
和isinstance
函数的类型检查。
这事因为我们在中介绍过的subclasshook
。
简单地说就是通过subclasshook
告诉解释器实现了__iter__
方法的类都被视为Iterable
的子类,实现了__next__
和__iter__
的类都被视为Iterator
的子类。
对
subclasshook
如何定义感兴趣的可以阅读。
事实上上面的例子只是说明迭代器模式的机制,对于将Sentence
改造为纯粹的可迭代对象其实很简单:
import re
class SentenceV3():
RE_WORD = re.compile('\w+')
def __init__(self, text) -> None:
self.text = text
self.words = SentenceV3.RE_WORD.findall(text)
def __iter__(self):
return iter(self.words)
这里是通过委托给列表的迭代器的方式实现。
我们早在中就学习过生成器,已经知道生成器最大的用途是节省空间开支,在调用时候才会生成当前访问的元素。
现在我们开始系统学习生成器。
生成器
生成器实质上是一种特殊的迭代器,它具有迭代器所有的特点,并且类型检查也会被认为是一个迭代器。
与迭代器最大的不同是,迭代器只关心能否获取到一系列数据,至于这些数据来源于内存全部加载的一个完整容器还是在访问时候临时生成的一个元素,它并不关心。而后者正是生成器所具有的特点。
在Python中构建生成器的具体方式分为生成器函数和生成器表达式。
生成器函数
从之前的学习我们已经知道,生成器函数和普通的函数有所不同,它使用yield
而非return
来返回信息,而且其工作机制也不同。事实上这两种函数也的确差别很大,以至于Python社区中曾经有一些人呼吁使用新的关键字而非def
来命名生成器函数,以和普通函数区分,但Python官方基于各种原因最终没有采纳。
事实上生成器函数内也可以使用
return
,只不过那样做并没有什么意义。
工作原理
生成器函数实质上是一个产生生成器的工厂方法。
def genTest():
print('before 1')
yield 1
print('before 2')
yield 2
print('before 3')
yield 3
print('end')
gen = genTest()
print(gen)
print(isinstance(gen, abc.Iterator))
while True:
item = next(gen)
print('get:',item)
# <generator object genTest at 0x000001CFCC21B120>
# True
# before 1
# get: 1
# before 2
# get: 2
# before 3
# get: 3
# end
# Traceback (most recent call last):
# File "D:\workspace\python\python-learning-notes\note31\test.py", line 79, in <module>
# item = next(gen)
# StopIteration
可以看到,生成器函数返回的是generator
类型,通过类型检查也能判断是迭代器。
通过输出我们可以看到,生成器函数会在迭代过程(这里是next
)中逐步执行,遇到yield
语句就会返回,剩下的语句不会继续执行,会一直等到下一次迭代后从上次挂起的地方继续执行,以此往复。一直到最后所有的yield
语句执行完毕后,再次迭代的时候会执行剩下的语句,并且在退出生成器函数后抛出一个StopIteration
异常。
介绍完生成器函数的工作原理后,我们可以用生成器函数来改造Sentence
类,以节约空间开支。
Sentence的惰性实现
import re
RE_WORD = re.compile('\w+')
class SentenceV4():
def __init__(self, text) -> None:
self.text = text
self.wordIterator = RE_WORD.finditer(self.text)
def __iter__(self):
for mached in self.wordIterator:
yield mached.group()
RE_WORD.finditer
返回的是一个生成器,生成器中的元素类型是Match Object
,可以通过group
方法获取其中匹配到的字符串。
yield from
yield from
是Python3新加入的语法。
我们来看这个例子:
l = [['a','b','c'],[1,2,3]]
def genTest2(l):
for i in l:
for j in i:
yield j
for i in genTest2(l):
print(i)
# a
# b
# c
# 1
# 2
# 3
对于这种两层迭代器,我们如果要用生成器函数生成其中的底层元素,就要进行两层for/in
遍历。
但是如果使用yield from
就会简单一些:
l = [['a','b','c'],[1,2,3]]
def genTest2(l):
for i in l:
yield from i
for i in genTest2(l):
print(i)
# a
# b
# c
# 1
# 2
# 3
这里的yield from
的用途是将第二层迭代委托给i
这个迭代器,省略了一层for/in
遍历。
生成器表达式
生成器表达式可以看做是生成器函数的“缩略版”,它没有yield
语句以及复杂的逻辑控制,而是一个简洁的表达式。
相应的,也更加灵活,你可以把它嵌入到任何可以试用表达式的地方,但同样的,你没法像生成器函数那样重复使用,毕竟代码复用正是函数产生的目的之一。
下面我们使用生成器表达式改造之前的Sentence
。
Sentence
的生成器表达式版本
import re
RE_WORD = re.compile('\w+')
class SentenceV5():
def __init__(self, text) -> None:
self.text = text
self.wordIterator = RE_WORD.finditer(self.text)
def __iter__(self):
return (mached.group() for mached in self.wordIterator)
很简单,直接在__iter__
函数中返回一个生成器表达式。因为生成器表达式会产生一个生成器,自然也可以当做迭代器来使用。
from sentence_v5 import SentenceV5
s = SentenceV5("Today is a good day!")
for word in s:
print(word)
# Today
# is
# a
# good
# day
测试结果也表明这样做并无问题。
与生成器函数的比较
之前也说了,生成器表达式比生成器函数更为灵活,但同样的,并不能进行复用。
此外生成器表达式也不能用于构建逻辑复杂的生成器,那个时候就要用生成器函数来构建。
但是总的来说,逻辑简单的生成器函数是很有可能用生成器表达式来代替的。
标准库中的生成器函数
这里介绍一些标准库中的生成器函数,目的是在了解的情况下在使用用进行复用,尽量避免自己“发明”重复的轮子。
这里遵循《Fluent Python》一书的原则,按用途进行分类后介绍。
需要说明的是,大部分生成器函数都包含在
itertools
模块。
过滤
这一类函数通常是用于将给定可迭代对象进行筛选过滤后生成一组元素。
compress
其官方文档的说明在。
如官方文档所说,compress(data,selectors)
接受两个可迭代对象,前边一个为用于筛选的对象,筛选标准以后一个可迭代对象中的元素的bool
值为基准,如果是True
则会出现在最后产生的生成器中,否则就会被丢弃。
需要注意的是,如果data
和selectors
的长度不相等,则会在处理完较短的可迭代对象后停止产生元素,这点和zip
相似。
我们来进行测试:
import itertools
data = [i for i in range(6)]
selectors = [True, False, False, True]
for result in itertools.compress(data,selectors):
print(result)
# 0
# 3
官方文档的示例中
selectors
是包含整数的列表,这可能是对Python中非布尔型依然可以通过实现__bool__
来转化为布尔型的特点进行的强调。
dropwhile
其官方文档见。
dropwhile
可能理解起来有一些费解,我们直接看示例:
numbers = [i for i in range(10)]
for result in itertools.dropwhile(lambda x:x<5,numbers):
print(result)
# 5
# 6
# 7
# 8
# 9
dropwhile(predicate,iterable)
的作用是对于可迭代对象,指定一个条件,只要可迭代对象的元素满足该条件(结果为True
),立即丢弃。但如果一旦不满足条件,其后的元素都将作为生成器的元素输出。
就像上面的示例中的那样,条件是是否小于5,而对于一个9以内的等差数列,显然是输出5-9这几个元素,前边小于5的元素都会被丢弃。
需要注意的是,丢弃行为是集中在开始阶段的,一旦开始输出,就不会再有丢弃行为,即使之后的元素满足丢弃条件也是如此。这点在这个示例中并没有很好体现。
takewhile
takewhile
与dropwhile
意义相反,从字面意义上就能看出,dropwhile
是满足条件丢弃,takewhile
是满足条件输出,相同的是,它们的丢弃、输出行为都是集中在开始部分,一旦因为条件不再满足,就会终止这种行为。
example = [1,2,3,4,5,4,3,2,1]
for result in itertools.takewhile(lambda x: x<=3,example):
print(result)
# 1
# 2
# 3
filter
关于filter
的官方文档见。
filter(function,iterable)
很好理解,用给定的函数function
对一个可迭代对象iterable
进行筛选,如果满足条件就将元素输出到产生的生成器中。
这是一个内置函数,而非
itertools
模块中。
numbers = [i for i in range(10)]
for result in filter(lambda x: x%2==0,numbers):
print(result)
# 0
# 2
# 4
# 6
# 8
这个示例也很简单,用匿名函数筛选出等差数列中的偶数。
filterfalse
关于filterfalse
的官方文档见。
filterfalse
也很好理解,相当于filter
的反面,即筛选出不满足filter
的元素。
其实我们完全可以用逻辑运算取反结合
filter
函数来实现。与
filter
不同,filterfalse
属于itertools
模块。
for result in itertools.filterfalse(lambda x: x%2==0,range(10)):
print(result)
# 1
# 3
# 5
# 7
# 9
几乎没有改动,我们只需要将filter
替换为filterfalse
就能筛选出奇数。
islice
islice
可以看做是生成器版本的切片函数,这点从命名就可以看出(i-iterator)。和普通的切片方式最大的不同当然是作为生成器,其切片结果空间开销更少,适合对海量数据切片的时候使用。
其官方文档的相关介绍见。
numbers = [number*2 for number in range(10)]
for result in itertools.islice(numbers,5):
print(result)
# 0
# 2
# 4
# 6
# 8
这里展示了通过islice
获取前5个元素。
需要注意的是,islice
和切片或者range
函数的参数签名顺序有所不同,islice
的签名分两种方式:
-
islice(iterable,stop)
-
islice(iterable,start,stop[,step])
这点在使用中需要注意。
映射
accumulate
accumulate这个单词的意思是“积聚”,其用途和之前所介绍的高阶函数reduce
颇为相似,都是针对两个元素使用给定的函数不断进行向后迭代运算,但不同的是accumulate
函数输出的是一个生成器,包含所有的过程元素,而reduce
仅仅会输出一个最终结果。
accumulate
的函数签名是accumulate(iterable[,func,*,initial=None])
,可以看到这和reduce
的签名也很相似,同样的,对于initial
我们需要注意的是,这个初始值需要符合所进行的运算的“幂等性”。即如果func
是加运算,那initial
就可以设置为0,如果是乘法,就可以设置为1。
此外,一般来说输出的生成器中的元素个数与给定的可迭代对象是相同的,且第一个元素的值也相同。但是如果我们指定了initial
,输出的元素个数会多一个,且第一个元素就是initial
给定的初始值。
官方文档中对于
accumulate
的介绍见。
import operator
for result in itertools.accumulate(range(1,11),operator.mul,initial=1):
print(result)
# 1
# 1
# 2
# 6
# 24
# 120
# 720
# 5040
# 40320
# 362880
# 3628800
这段示例展示了1-10的阶乘结果,因为指定了initial
,所以结果多出一个,这里其实完全没有必要,仅仅是用于说明如何指定初始值。
enumerate
enumerate这个单词的意思是“枚举”。很多主流语言都支持枚举类型,比如Java或C++。但Python并不支持,但Python提供这个内置函数从某种程度上提供了对枚举类型的支持。
如果你在其他语言中使用过枚举类型,应该知道,枚举类型的本质无非是给一组给定的元素赋予不同的整形数值(一般都是从0开始的一系列整形值),从而实现一组值唯一的元素,用于一些特定的场合。
而Python的内置函数enumerate
就实现了这个功能,它可以对给定的可迭代对象中的元素一一赋予不同的整形值,从而把一个可迭代对象变成一组枚举类型。
关于
enumerate
的官方文档见。
enumSuites = {}
for value,suit in enumerate(['方块','梅花','红桃','黑桃'],start=1):
print("花色:{},值:{}".format(suit,value))
enumSuites[value] = suit
newSuit = 3
newSuitName = enumSuites[newSuit]
print(newSuitName)
# 花色:方块,值:1
# 花色:梅花,值:2
# 花色:红桃,值:3
# 花色:黑桃,值:4
# 红桃
这里使用enumerate
创建了一个扑克牌花色的枚举类型,并且通过指定start
的方式设定枚举值从1开始生成。并且我们创建了一个字典来映射枚举值和枚举类型,然后就可以像其他语言中使用枚举类型那样使用了。
需要注意的是enumerate
创建的生成器中的元素是一个元组,包含两个元素,第一个是生成的枚举值,第二个来自可迭代对象中的元素,这种设置也是很自然的,毕竟枚举类型的本质就是将枚举值和给定元素进行绑定。
这里其实有点“脱了裤子放屁”的意思,因为
['方块','梅花','红桃','黑桃']
这个列表本身就可以看做是一个枚举值,可以通过下标进行访问,这里只是为了说明。
map
map(function,iterable,...)
的用法也相当简单明了,就是用给定函数处理可迭代对象中的元素,并输出到产生的生成器中。
需要注意的是map
可以处理多个可迭代对象,但相应的,function
也必须能接受同样数目的参数。此时map
就会像zip
函数那样进行并行读取和处理,当处理完最短的可迭代对象后将结束整个过程。
for result in map(operator.add,range(10),range(6)):
print(result)
# 0
# 2
# 4
# 6
# 8
# 10
关于
map
的官方文档见。
starmap
starmap(function,iterable)
的命名和签名都和map
极为类似,但是不同的是,starmap
仅接受一个可迭代对象,而且function
函数直接处理的并非可迭代对象中的元素,而是将可迭代对象中的元素拆包后再由function
来处理。这中间的区别就像function(a,b)
和function(*c)
。
我们用类似map
中的示例来说明:
example = [(i,i) for i in range(6)]
for result in itertools.starmap(operator.add, example):
print(result)
# 0
# 2
# 4
# 6
# 8
# 10
我想这其中的差别应该很清楚了。
关于
startmap
的官方文档见。
合并
chain
单词"chain"的意思是锁链,而chain(*iterables)
的用途也正是像锁链那样,把多个可迭代对象串到一起。
for result in itertools.chain('abc',range(6)):
print(result)
# a
# b
# c
# 0
# 1
# 2
# 3
# 4
# 5
关于
chain
的官方文档说明见。
chain.from_iterable
chain.from_iterable
与chain
的差别就像starmap
和map
的差别,所以不难理解。
example = enumerate('abcde',start=1)
for result in itertools.chain.from_iterable(example):
print(result)
# 1
# a
# 2
# b
# 3
# c
# 4
# d
# 5
# e
这里利用enumerate
和chain.from_iterable
创建了一个数字和字母夹杂的输出。同时可以发现,example
不单单是一个简单的可迭代对象,这还是一个生成器,实质上我们是用一个生成器作为chai.from_iterable
函数的参数来构建另一个生成器,这其实在Python中是常见和可行的。这样做依然可以保证生成器最小空间开销的优点,不管通过多少个“中间生成器”来构造我们的最终生成器,只要不切实使用最终生成器进行迭代操作,这些生成器都是没有空间开销的,只有在真正开始迭代操作才会产生少量空间开销。
关于
chai.from_iterable
的官方文档见。
product
product(*iterables,repeat=1)
的用途是将给定的多个可迭代对象进行笛卡尔积,并将结果输出到产生的生成器中。
suites = ['红桃','黑桃','方块','梅花']
numbers = [i for i in range(2,11)]+['J','Q','K','A']
for result in itertools.product(suites,numbers):
print(result)
# ('红桃', 2)
# ('红桃', 3)
# ('红桃', 4)
# ('红桃', 5)
# ('红桃', 6)
# ('红桃', 7)
# ('红桃', 8)
# ('红桃', 9)
# ('红桃', 10)
# ('红桃', 'J')
# ('红桃', 'Q')
# ('红桃', 'K')
# ('红桃', 'A')
# ('黑桃', 2)
# ('黑桃', 3)
# ('黑桃', 4)
# ('黑桃', 5)
# ('黑桃', 6)
# ('黑桃', 7)
# ('黑桃', 8)
# ('黑桃', 9)
# ('黑桃', 10)
# ('黑桃', 'J')
# ('黑桃', 'Q')
# ('黑桃', 'K')
# ('黑桃', 'A')
# ('方块', 2)
# ('方块', 3)
# ('方块', 4)
# ('方块', 5)
# ('方块', 6)
# ('方块', 7)
# ('方块', 8)
# ('方块', 9)
# ('方块', 10)
# ('方块', 'J')
# ('方块', 'Q')
# ('方块', 'K')
# ('方块', 'A')
# ('梅花', 2)
# ('梅花', 3)
# ('梅花', 4)
# ('梅花', 5)
# ('梅花', 6)
# ('梅花', 7)
# ('梅花', 8)
# ('梅花', 9)
# ('梅花', 10)
# ('梅花', 'J')
# ('梅花', 'Q')
# ('梅花', 'K')
# ('梅花', 'A')
这个示例展示了利用product
产生一套扑克牌。
如果需要将可迭代对象自己和自己笛卡尔积,可以指定repeat=2
。
suites = ['红桃','黑桃','方块','梅花']
for result in itertools.product(suites, repeat=2):
print(result)
# ('红桃', '红桃')
# ('红桃', '黑桃')
# ('红桃', '方块')
# ('红桃', '梅花')
# ('黑桃', '红桃')
# ('黑桃', '黑桃')
# ('黑桃', '方块')
# ('黑桃', '梅花')
# ('方块', '红桃')
# ('方块', '黑桃')
# ('方块', '方块')
# ('方块', '梅花')
# ('梅花', '红桃')
# ('梅花', '黑桃')
# ('梅花', '方块')
# ('梅花', '梅花')
至于repeat>2
并且有多个可迭代对象的情况,我的脑子思考不能,所以就不做讨论了。
关于
product
的官方文档见。
zip
zip
的用途和使用方式已经在前文说过了,所以这里不做过多介绍,更多信息见。
zip_longest
同zip
,更多信息见。
排列组合
这是一组和数学概念中的排列组合概念完全相同的函数,所以这里单独归类。
combinations
combinations(iterable,r)
用自然语言很难解释清楚,但如果用数学语言就很简单了:
C=iterable\\ n=r\\ m=len(iterable)\\ combinations = C_m^n
$$
这样应该就很清楚了,combinations
的作用就是实现数学概念中的组合。
需要注意的是,因为离散数学中的组合这一概念本身就是集合的拓展,所以自然是去重的,所以combinations
的结果也是去重后的组合结果,相当于先把iterable
转化为set
类型再进行组合操作。
suites = ['红桃','黑桃','方块','梅花']
for result in itertools.combinations(suites,2):
print(result)
# ('红桃', '黑桃')
# ('红桃', '方块')
# ('红桃', '梅花')
# ('黑桃', '方块')
# ('黑桃', '梅花')
# ('方块', '梅花')
这里我们继续用扑克牌花色做说明,我们知道组合的数学公式为:
C_m^n=\frac{m!}{n!(m-n)!}
$$
套用这个公式不难计算出,C_4^2的值为6,所以相应的组合结果应该有6种。上面的示例输出正符合这个结果。
关于
combinations
的官方文档见。
combinations_with_replacement
combinations_with_replacement(iterable,r)
是对combinations
函数的一种补充,我们前边说过了,combinations
函数遵循数学中组合概念的规则,会进行去重,而如果不去重直接组合,就是combinations_with_replacement
的效果,这种行为也可以看作是把给定的可迭代对象中的元素视作天然不同的元素(不管其值是否真的不同),然后进行组合。
print('='*10)
suites = ['红桃','黑桃','方块','梅花']
for result in itertools.combinations_with_replacement(suites,2):
print(result)
# ==========
# ('红桃', '红桃')
# ('红桃', '黑桃')
# ('红桃', '方块')
# ('红桃', '梅花')
# ('黑桃', '黑桃')
# ('黑桃', '方块')
# ('黑桃', '梅花')
# ('方块', '方块')
# ('方块', '梅花')
# ('梅花', '梅花')
我们用几乎相同的示例进行测试,可以看到结果从6个增长到10个,这是因为相同元素的组合现在也被认为是合法的了,所以会多出4个。
关于
combinations_with_replacement
的官方文档见。
permulations
既然有组合,当然也有排列,同样的,这一概念也来自数学领域。
permulations(iterable,r=None)
:
A=iterable\\ m=len(iterable)\\ n=r\\ permulations=A_m^n\\ A_m^n=\frac{m!}{(m-n)!}
$$
print('='*15)
suites = ['红桃','黑桃','方块','梅花']
for result in itertools.permutations(suites,2):
print(result)
# ===============
# ('红桃', '黑桃')
# ('红桃', '方块')
# ('红桃', '梅花')
# ('黑桃', '红桃')
# ('黑桃', '方块')
# ('黑桃', '梅花')
# ('方块', '红桃')
# ('方块', '黑桃')
# ('方块', '梅花')
# ('梅花', '红桃')
# ('梅花', '黑桃')
# ('梅花', '方块')
输出结果与数学公式得出的数目一致,这里不再赘述。
既然组合有
cobinations_with_replacement
,那排列有没有permulations_with_replacement
呢?并没有,是不是很吃惊?其实
with_replacement
版本的排列就是可迭代对象自身与自身的笛卡尔积,也就是说可以通过product(iterable,repeat=2)
来实现。
扩展
count
count(start,step=1)
用于构建一个无限的等差数列。
因为是无限的,所以如果我们直接在程序中list(itertools.count(0))
,内存马上就会被塞满。
所以使用的时候一定要结合其他takewhile
或者islice
函数进行有限度地使用。
for result in itertools.takewhile(lambda x:x<10, itertools.count(0,1.5)):
print(result)
# 0
# 1.5
# 3.0
# 4.5
# 6.0
# 7.5
# 9.0
关于
count
的官方文档见。
cycle
和count
类似,cycle(iterable)
同样会产生一个无限序列,只不过它是通过将给定的可迭代对象进行无限循环构建的。
for result in itertools.islice(itertools.cycle("ABC"),10):
print(result)
# A
# B
# C
# A
# B
# C
# A
# B
# C
# A
这个示例展示了从一个“ABC”无限循环的序列上截取前10个元素。
关于
cycle
的官方文档见。
repeat
repeat(object[,times])
用于将给定的元素重复指定times
次数,如果没有给定times
,将无限产出。
repeat
最常见的用途是用于map
、zip
等函数的一个参数,比如:
for result in map(operator.add,range(1,11),itertools.repeat(10)):
print(result,end=' ')
print()
# 11 12 13 14 15 16 17 18 19 20
重新排列元素
groupby
groupby(iterable,key=None)
用于将给定的可迭代对象中的元素进行分组,但是和SQL中的grou by
语句不通的是,groupby
函数“比较懒”,它只会分组相邻的元素,这就是说,如果你想要保证SQL中那样完整的分组结果,那就必须先把可迭代元素尽心个排序,然后再交给grouby
函数进行分组。
函数签名中的key
与sort
中的参数类似,是作为分组依据存在的。
example = ['aaa','bbb','cccccc','dd','eee','ffffff']
example.sort(key=len)
for num,grouper in itertools.groupby(example,key=len):
print("num:{},countents:".format(num),end='')
for grouperItem in grouper:
print(grouperItem,end=' ')
print()
# num:2,countents:dd
# num:3,countents:aaa bbb eee
# num:6,countents:cccccc ffffff
这里groupby
函数返回的生成器中的元素是(num,itertools.grouper)
的形式,其中grouper
是一个可迭代对象,所以示例中用for/in
遍历输出其中分组的元素。
关于
groupby
的官方文档见。
reversed
reversed(seq)
方法我们之前经常使用,用于将一个给定序列“逆序化”。
此外需要注意的是作为参数的seq
只能是有限序列,因为要进行逆序,自然要知道序列最后一个元素,无限序列显然是无法逆序的。
对于reversed
这里就不做过多说明,可以阅读。
tee
tee(iterable,n=2)
的解释很简单:根据给定的可迭代对象,生成指定书目n
个生成器。
但其效果相当强大,这就好像西游记里孙猴子的分身一样。
记得前边我说过的Python迭代器的局限性吗,不能重置,如果要再次使用只能是重新创建,但我们现在有了tee
函数不是吗。
iter1,iter2 = itertools.tee(range(10),2)
for num in iter1:
print(num,end=' ')
print()
for num in iter2:
print(num, end=' ')
print()
# 0 1 2 3 4 5 6 7 8 9
# 0 1 2 3 4 5 6 7 8 9
关于
tee
的官方文档见。
归约函数
归约函数和上面列举的那些生成器函数不同,这类函数并不产生一个生成器作为结果,而是对于给定的一个或多个可迭代对象,产生出一个单一的结果。
all
all(iterable)
很好理解,用物理知识来比喻就是串联电路,只有可迭代对象中的所有元素都为True
,结果才为True
,否则为False
。
结合今天介绍的迭代器相关知识,all
拥有一个特点:旁路。即在通过迭代iterable
以计算结果的过程中,如果一旦有元素为False
,就会结束迭代直接返回False
,这当然也符合程序效率最优的规律。我们可以通过一个示例来观察:
旁路,或者说短路,同样是一个物理学概念。当然离散数学中有类似概念,不过我已经交还给老师了。
def showIteratorRemains(iterator):
for item in iterator:
print(item,end=' ')
print()
example = [1,2,3,0,0,1,5,0,3]
expIterator = iter(example)
print(all(expIterator))
showIteratorRemains(expIterator)
# False
# 0 1 5 0 3
这里通过showIteratorRemains
函数输出了迭代器剩余的元素。
关于
all
的官方文档见。
any
any
与all
的概念对立,用物理学概念来类比就是并联了。如果给定的可迭代对象元素中有一个为True
则结果为True
,反之为False
。
同样的,其存在旁路特征,这里就不做展示了,和all
的示例很类似。
关于
any
的官方文档见。
max
max
很好理解,从一堆数据中选出一个最大的。需要注意的是max
有两种签名:
-
max(iterable,*[,key,default])
-
max(arg1,arg2,*args[,key])
前者接受一个可迭代对象,后者接受普通参数。同时max
接受类似sort
中的参数key
来作为比较基准。default
的作用是如果可迭代对象为空的时候指定一个默认值进行返回。
关于
max
的官方文档见。
min
min
与max
意义相反,选取一个最小元素。其它部分与max
完全相同,所以不再赘述。
关于
min
的官方文档见。
reduce
之前已经多次使用和介绍过reduce
了,所以这里不再赘述,官方文档见。
sum
同样很简单,将多个元素相加,官方文档见。
iter函数的另类用法
我们之前说过了,iter(object[,sentinel])
函数的用途是用于获取可迭代对象的迭代器,但如果第一个参数变成可执行对象,第二个参数sentinel
指定一个结束标识,则就会直接“凭空”产生一个生成器。
import random
def roll():
return random.randint(1,6)
rollIterator = iter(roll,6)
for result in rollIterator:
print(result,end=' ')
print()
# 1 5 1 2 2 5 4 2
这是一个掷骰子的例子,我们通过iter
构建了一个生成器,可以随机生成1-6,如果一旦生成6,生成器就会结束。
协程
从Python2的某个版本开始,Python支持了协程,这是一个有趣的特性,通过“改造”生成器函数,我们不仅可以不断地从其中获取数据,还可以将数据传入,进行完整交互。这就是协程。
但本质上来说生成器和协程是两种截然不同的东西,所以同样的,Python社区有人建议使用单独的关键字将两者区分,但同样因为种种原因并没有。
我们将在后续部分继续探索协程,这里只是顺带一提。
好了,Python中迭代相关的技术全部介绍完毕,谢谢阅读。
学习加写这篇文章,几乎花了两天时间...真是一个大工程。
参考资料
文章评论