Python学习笔记31:迭代技术

本文详细介绍了Python中的迭代技术,包括可迭代对象、迭代器、生成器及其相关概念。重点讲述了迭代器的工作原理、生成器函数和生成器表达式,以及标准库中的常用生成器函数,如filter、map、enumerate等。此外,还讨论了归约函数如all、any、max、min。文章通过实例解析了迭代器模式,展示了如何通过迭代技术优化内存使用和提高代码效率。
摘要由CSDN通过智能技术生成

Python学习笔记31:迭代技术

  • 本系列文章的代码都存放在Github项目:python-learning-notes
  • 这一部分内容是《Fluent Python》目前为止最长的篇幅,我也花了大半天时间来阅读,内容的确庞杂,所以在提炼整理上可能会有所疏漏,请多包涵。

迭代技术无疑在Python中占有相当的地位。平时我们在写代码的时候,大多数时间也是话费在for或者foreach之类的循环语句上,而Python更进一步,在语言结构中直接整合了迭代技术,让我们可以更容易地在不同类型间使用类似的简单语法就可以进行迭代操作。

接下来我们通过Python迭代技术的核心概念:可迭代对象、迭代器和生成器来深入理解。

可迭代对象和迭代器

Python学习笔记27:类序列对象中我们简单说明过可迭代对象和迭代器,现在我们从语言设计层面来理解这两个概念,先来看一下UML类图:

image-20210511182009294

相当简单,Iterable抽象类代表可迭代对象,Iterator抽象类代表迭代器。

其中可迭代对象仅有一个抽象方法,返回一个迭代器。而迭代器有两个方法,抽象方法__next__作为主要的迭代逻辑,将依次返回元素,完成迭代。而继承自可迭代对象的__iter__方法被重写,用这样的方式实现:

def __iter__(self):
	return self

这是因为我们在Python中会大量使用for/in或者__init__之类的情况来接收和使用可迭代对象,在进行迭代的时候后解释器会调用可迭代对象的__iter__获取一个迭代器,进行具体迭代工作,而这个迭代器本身自然也可以作为一个可迭代对象来使用,所以才会是以上的这种类结构。

下面我们具体分析一下可迭代对象和迭代器。

可迭代对象

__iter__

上面我们说了,可迭代对象必须要实现__iter__,而我们之前Python学习笔记27:类序列对象在实现过序列协议,只实现了__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

可以正常迭代,而且StenceV2SentenceIterator的类对象和实例都通过了issubclassisinstance函数的类型检查。

这事因为我们在Python学习笔记28:从协议到抽象基类中介绍过的subclasshook

简单地说就是通过subclasshook告诉解释器实现了__iter__方法的类都被视为Iterable的子类,实现了__next____iter__的类都被视为Iterator的子类。

subclasshook如何定义感兴趣的可以阅读Python学习笔记28:从协议到抽象基类

事实上上面的例子只是说明迭代器模式的机制,对于将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学习笔记16:生成器中就学习过生成器,已经知道生成器最大的用途是节省空间开支,在调用时候才会生成当前访问的元素。

现在我们开始系统学习生成器。

生成器

生成器实质上是一种特殊的迭代器,它具有迭代器所有的特点,并且类型检查也会被认为是一个迭代器。

与迭代器最大的不同是,迭代器只关心能否获取到一系列数据,至于这些数据来源于内存全部加载的一个完整容器还是在访问时候临时生成的一个元素,它并不关心。而后者正是生成器所具有的特点。

在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则会出现在最后产生的生成器中,否则就会被丢弃。

需要注意的是,如果dataselectors的长度不相等,则会在处理完较短的可迭代对象后停止产生元素,这点和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

takewhiledropwhile意义相反,从字面意义上就能看出,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的签名分两种方式:

  1. islice(iterable,stop)
  2. 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_iterablechain的差别就像starmapmap的差别,所以不难理解。

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

这里利用enumeratechain.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 = i t e r a b l e n = r m = l e n ( i t e r a b l e ) c o m b i n a t i o n s = C m n C=iterable\\ n=r\\ m=len(iterable)\\ combinations = C_m^n C=iterablen=rm=len(iterable)combinations=Cmn
这样应该就很清楚了,combinations的作用就是实现数学概念中的组合。

需要注意的是,因为离散数学中的组合这一概念本身就是集合的拓展,所以自然是去重的,所以combinations的结果也是去重后的组合结果,相当于先把iterable转化为set类型再进行组合操作。

suites = ['红桃','黑桃','方块','梅花']
for result in itertools.combinations(suites,2):
    print(result)
# ('红桃', '黑桃')
# ('红桃', '方块')
# ('红桃', '梅花')
# ('黑桃', '方块')
# ('黑桃', '梅花')
# ('方块', '梅花')

这里我们继续用扑克牌花色做说明,我们知道组合的数学公式为:
C m n = m ! n ! ( m − n ) ! C_m^n=\frac{m!}{n!(m-n)!} Cmn=n!(mn)!m!
套用这个公式不难计算出,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 = i t e r a b l e m = l e n ( i t e r a b l e ) n = r p e r m u l a t i o n s = A m n A m n = m ! ( m − n ) ! A=iterable\\ m=len(iterable)\\ n=r\\ permulations=A_m^n\\ A_m^n=\frac{m!}{(m-n)!} A=iterablem=len(iterable)n=rpermulations=AmnAmn=(mn)!m!

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最常见的用途是用于mapzip等函数的一个参数,比如:

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函数进行分组。

函数签名中的keysort中的参数类似,是作为分组依据存在的。

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

anyall的概念对立,用物理学概念来类比就是并联了。如果给定的可迭代对象元素中有一个为True则结果为True,反之为False

同样的,其存在旁路特征,这里就不做展示了,和all的示例很类似。

关于any的官方文档见这里

max

max很好理解,从一堆数据中选出一个最大的。需要注意的是max有两种签名:

  1. max(iterable,*[,key,default])
  2. max(arg1,arg2,*args[,key])

前者接受一个可迭代对象,后者接受普通参数。同时max接受类似sort中的参数key来作为比较基准。default的作用是如果可迭代对象为空的时候指定一个默认值进行返回。

关于max的官方文档见这里

min

minmax意义相反,选取一个最小元素。其它部分与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中迭代相关的技术全部介绍完毕,谢谢阅读。

学习加写这篇文章,几乎花了两天时间…真是一个大工程。

迭代技术

参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值