今天分享一下我本身的itertools学习体验,itertools是一个Python的自带库,内含多种很是实用的方法,经过基础了解后,我发现能够大大提高工做效率。html
首先,有关itertools的详细介绍,我参考的是Python 3.7官方文档:itertools — Functions creating iterators for efficient looping,你们感兴趣能够去看看,目前尚未中文版本,十分遗憾,这里不得不吐槽一句,为啥有日语,韩语,中文的版本没有跟上呢?python
书规正传,itertools 我认为是Python3里最酷的东西!算法
若是你尚未据说过它,那么你就错过了Python3标准库的一个最大隐藏宝藏,是的,我很快就抛弃了刚刚分享的collections模块:小白的Python 学习笔记(七)神奇宝藏 Collections,毕竟男人都是大猪蹄子bash
网上有不少优秀的资源可用于学习itertools模块中的功能。但我认为官方文档老是一个很好的起点。这篇文章即是基本基于文档概括整理而来。app
我在学习后的总体感觉是,关于itertools只知道它包含的函数功能足矣,不必定非要较真。真正的强大之处在于组合这些功能以建立快速,占用内存效率极少,漂亮优雅的代码。函数
在这篇很长的文章里,我会全面回顾个人学习历程,争取全面复制每个细节,在开始以前,若是朋友们还不太知道迭代器和生成器是什么,能够参考如下科普扫盲:oop
坐好扶稳,咱们准备上车了,根据官方文档的定义:post
This module implements a number of iterator building blocks inspired by constructs from APL, Haskell, and SML. Each has been recast in a form suitable for Python.学习
翻译过来大概就是它是一个实现了许多迭代器构建的模块,它们受到来自APL,Haskell和SML的构造的启发......能够提升效率等等,测试
这主要意味着itertools中的函数是在迭代器上“操做”以产生更复杂的迭代器。 例如,考虑内置的zip()函数,该函数将任意数量的iterables做为参数,并在其相应元素的元组上返回迭代器:
print(list(zip([1, 2, 3], ['a', 'b', 'c'])))
Out:[(1, 'a'), (2, 'b'), (3, 'c')]
复制代码
这里zip究竟是如何工做的?
与全部其余list同样,[1,2,3] 和 ['a','b','c'] 是可迭代的,这意味着它们能够一次返回一个元素。 从技术上讲,任何实现:
.__ iter __()
或 .__ getitem __()
方法的Python对象都是可迭代的。若是对这方面有疑问,你们能够看前言部分提到的教程
有关iter()这个内置函数,当在一个list或其余可迭代的对象 x 上调用时,会返回x本身的迭代器对象:
iter([1, 2, 3, 4])
iter((1,2,3,4))
iter({'a':1,'b':2})
Out:<list_iterator object at 0x00000229E1D6B940>
<tuple_iterator object at 0x00000229E3879A90>
<dict_keyiterator object at 0x00000229E1D6E818>
复制代码
实际上,zip()函数经过在每一个参数上调用iter(),而后使用next()推动iter()返回的每一个迭代器并将结果聚合为元组来实现。 zip()返回的迭代器遍历这些元组
而写到这里不得不回忆一下,以前在 小白的Python 学习笔记(五)map, filter, reduce, zip 总结 中给你们介绍的map()内置函数,其实某种意义上也是一个迭代器的操做符而已,它以最简单的形式将单参数函数一次应用于可迭代的sequence的每一个元素:
list(map(len, ['xiaobai', 'at', 'paris']))
Out: [7, 2, 5]
复制代码
参考map模板,不难发现:map()函数经过在sequence上调用iter(),使用next()推动此迭代器直到迭代器耗尽,并将func 应用于每步中next()返回的值。在上面的例子里,在['xiaobai', 'at', 'paris']的每一个元素上调用len(),从而返回一个迭代器包含list中每一个元素的长度
因为迭代器是可迭代的,所以能够用 zip()和 map()在多个可迭代中的元素组合上生成迭代器。 例如,如下对两个list的相应元素求和:
a = [1, 2, 3]
b = [4, 5, 6]
list(map(sum, zip(a,b)))
Out: [5, 7, 9]
复制代码
这个例子很好的解释了如何构建itertools中所谓的 “迭代器代数” 的函数的含义。咱们能够把itertools视为一组构建砖块,能够组合起来造成专门的“数据管道”,就像这个求和的例子同样。
其实在Python 3里,若是咱们用过了map() 和 zip() ,就已经用过了itertools,由于这两个函数返回的就是迭代器!
咱们使用这种 itertools 里面所谓的 “迭代器代数” 带来的好处有两个:
可能有朋友对这两个好处有所疑问,不要着急,咱们能够分析一个具体的场景:
如今咱们有一个list和正整数n,编写一个将list 拆分为长度为n的组的函数。为简单起见,假设输入list的长度可被n整除。例如,若是输入= [1,2,3,4,5,6] 和 n = 2,则函数应返回 [(1,2),(3,4),(5,6)]。
咱们首先想到的解决方案可能以下:
def naive_grouper(lst, n):
num_groups = len(lst) // n
return [tuple(lst[i*n:(i+1)*n]) for i in range(num_groups)]
复制代码
咱们进行简单的测试,结果正确:
nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
naive_grouper(nums, 2)
Out: [(1, 2), (3, 4), (5, 6), (7, 8), (9, 10)]
复制代码
可是问题来了,若是咱们试图传递一个包含1亿个元素的list时会发生什么?咱们须要大量内存!即便有足够的内存,程序也会挂起一段时间,直到最后生成结果
这个时候若是咱们使用itertools里面的迭代器就能够大大改善这种状况:
def better_grouper(lst, n):
iters = [iter(lst)] * n
return zip(*iters)
复制代码
这个方法中蕴含的信息量有点大,咱们如今拆开一个个看,表达式 [iters(lst)] * n 建立了对同一迭代器的n个引用的list:
nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
iters = [iter(nums)] * 2
list(id(itr) for itr in iters) # Id 没有变化,就是建立了n个索引
Out: [1623329389256, 1623329389256]
复制代码
接下来,zip(* iters)在 iters 中的每一个迭代器的对应元素对上返回一个迭代器。当第一个元素1取自“第一个”迭代器时,“第二个”迭代器如今从2开始,由于它只是对“第一个”迭代器的引用,所以向前走了一步。所以,zip()生成的第一个元组是(1,2)。
此时,iters中的所谓 “两个”迭代器从3开始,因此当zip()从“第一个”迭代器中拉出3时,它从“第二个”得到4以产生元组(3,4)。这个过程一直持续到zip()最终生成(9,10)而且iters中的“两个”迭代器都用完了:
注意: 这里的"第一个","第二个" ,"两个"都是指向一个迭代器,由于id没有任何变化!!
最后咱们发现结果是同样的:
nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
list(better_grouper(nums, 2))
Out: [(1, 2), (3, 4), (5, 6), (7, 8), (9, 10)]
复制代码
可是,这里我作了测试,发现两者的消耗内存是天壤之别,并且在使用iter+zip()的组合后,执行速度快了500倍以上,你们感兴趣能够本身测试,把 nums 改为 range(100000000) 便可
如今让咱们回顾一下刚刚写好的better_grouper(lst, n) 方法,不难发现,这个方法存在一个明显的缺陷:若是咱们传递的n不能被lst的长度整除,执行时就会出现明显的问题:
>>> nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> list(better_grouper(nums, 4))
[(1, 2, 3, 4), (5, 6, 7, 8)]
复制代码
在分组输出中缺乏元素9和10。发生这种状况是由于一旦传递给它的最短的迭代次数耗尽,zip()就会中止聚合元素。而咱们想要的是不丢失任何元素。所以解决办法是咱们可使用 itertools.zip_longest() 它能够接受任意数量的 iterables 和 fillvalue 这个关键字参数,默认为None。咱们先看一个简单实例
>>> import itertools as it
>>> x = [1, 2, 3, 4, 5]
>>> y = ['a', 'b', 'c']
>>> list(zip(x, y)) # zip老是执行完最短迭代次数中止
[(1, 'a'), (2, 'b'), (3, 'c')]
>>> list(it.zip_longest(x, y))
[(1, 'a'), (2, 'b'), (3, 'c'), (4, None), (5, None)]
复制代码
这个例子已经很是清晰的体现了zip()和 zip_longest()的区别,如今咱们能够优化 better_grouper 方法了:
import itertools as it
def grouper(lst, n, fillvalue=None):
iters = [iter(lst)] * n
return it.zip_longest(*iters, fillvalue=fillvalue) # 默认就是None
复制代码
咱们再来看优化后的测试:
>>> nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
>>> print(list(grouper(nums, 4)))
[(1, 2, 3, 4), (5, 6, 7, 8), (9, 10, None, None)]
复制代码
已经很是理想了,各位同窗们可能尚未意识到,咱们刚刚所作的一切就是建立itertools 里面grouper方法的全过程!
如今让咱们看看真正的 官方文档 里面所写的grouper方法:
和咱们写的基本同样,除了能够接受多个iterable 参数,用了*args
最后心满意足的测试一下:
from itertools import zip_longest
def grouper(iterable, n, fillvalue=None):
"Collect data into fixed-length chunks or blocks"
# grouper('ABCDEFG', 3, 'x') --> ABC DEF Gxx"
args = [iter(iterable)] * n
return zip_longest(*args, fillvalue=fillvalue)
test_list = list(grouper("ABCDEFG",3))
test_tuple = tuple(grouper(range(0,7),2,'Null'))
test_dict = dict(zip(test_list,test_tuple))
复制代码
输出结果:
print(test_list)
Out:[('A', 'B', 'C'), ('D', 'E', 'F'), ('G', None, None)]
复制代码
print(test_tuple)
Out:((0, 1), (2, 3), (4, 5), (6, 'Null'))
复制代码
print(test_dict)
Out: {('A', 'B', 'C'): (0, 1), ('D', 'E', 'F'): (2, 3), ('G', None, None): (4, 5)}
复制代码
首先基础概念扫盲,所谓暴力求解是算法中的一种,简单来讲就是 利用枚举全部的状况,或者其它大量运算又不用技巧的方式,来求解问题的方法。 我在看过暴力算法的广义概念后,首先想到的竟然是盗墓笔记中的王胖子
若是有看过盗墓笔记朋友,你会发现王胖子实际上是一个推崇暴力求解的人,在无数次遇到困境时祭出的”枚举法“,就是暴力求解,例如我印象最深的是云顶天宫中,一行人被困在全是珠宝的密室中没法逃脱,王胖子经过枚举排除全部可能性,直接获得”身边有鬼“ 的最终解。
PS: 此处致敬南派三叔,和那些他填不上的坑
扯远了,回到现实中来,咱们常常会碰到以下的经典题目:
你有三张20美圆的钞票,五张10美圆的钞票,两张5美圆的钞票和五张1美圆的钞票。能够经过多少种方式获得100美圆?
为了暴力破解这个问题,咱们只要把全部组合的可能性罗列出来,而后找出100美圆的组合便可,首先,让咱们建立一个list,包含咱们手上全部的美圆:
bills = [20, 20, 20, 10, 10, 10, 10, 10, 5, 5, 1, 1, 1, 1, 1]
复制代码
这里itertools会帮到咱们。 itertools.combinations() 接受两个参数
最终会在 input中 n 个元素的全部组合的元组上产生一个迭代器。
import itertools as it
bills = [20, 20, 20, 10, 10, 10, 10, 10, 5, 5, 1, 1, 1, 1, 1]
result =list(it.combinations(bills, 3))
print(len(result)) # 455种组合
print(result)
Out: 455
[(20, 20, 20), (20, 20, 10), (20, 20, 10), ... ]
复制代码
我仅剩的高中数学知识告诉我其实这个就是一个几率里面的 C 15(下标),3(上标)问题,好了,如今咱们拥有了各类组合,那么咱们只须要在各类组合里选取总数等于100的,问题就解决了:
makes_100 = []
for n in range(1, len(bills) + 1):
for combination in it.combinations(bills, n):
if sum(combination) == 100:
makes_100.append(combination)
复制代码
这样获得的结果是包含重复组合的,咱们能够在最后直接用一个set过滤掉重复值,最终获得答案:
import itertools as it
bills = [20, 20, 20, 10, 10, 10, 10, 10, 5, 5, 1, 1, 1, 1, 1]
makes_100 = []
for n in range(1, len(bills) + 1):
for combination in it.combinations(bills, n):
if sum(combination) == 100:
makes_100.append(combination)
print(set(makes_100))
Out:{(20, 20, 10, 10, 10, 10, 10, 5, 1, 1, 1, 1, 1),
(20, 20, 10, 10, 10, 10, 10, 5, 5),
(20, 20, 20, 10, 10, 10, 5, 1, 1, 1, 1, 1),
(20, 20, 20, 10, 10, 10, 5, 5),
(20, 20, 20, 10, 10, 10, 10)}
复制代码
因此最后咱们发现一共有5种方式。 如今让咱们把题目换一种问法,就彻底不同了:
如今要把100美圆的钞票换成零钱,你可使用任意数量的50美圆,20美圆,10美圆,5美圆和1美圆钞票,有多少种方法?
在这种状况下,咱们没有预先设定的钞票数量,所以咱们须要一种方法来使用任意数量的钞票生成全部可能的组合。为此,咱们须要用到**itertools.combinations_with_replacement()**函数。
它就像combination()同样,接受可迭代的输入input 和正整数n,并从输入返回有n个元组的迭代器。不一样之处在于combination_with_replacement()容许元素在它返回的元组中重复,看一个小栗子:
>>> list(it.combinations_with_replacement([1, 2], 2)) #本身和本身的组合也能够
[(1, 1), (1, 2), (2, 2)]
复制代码
对比 itertools.combinations():
>>> list(it.combinations([1, 2], 2)) #不容许本身和本身的组合
[(1, 2)]
复制代码
因此针对新问题,解法以下:
bills = [50, 20, 10, 5, 1]
make_100 = []
for n in range(1, 101):
for combination in it.combinations_with_replacement(bills, n):
if sum(combination) == 100:
makes_100.append(combination)
复制代码
最后的结果咱们不须要去重,由于这个方法不会产生重复组合:
>>> len(makes_100)
343
复制代码
若是你亲自运行一下,可能会注意到输出须要一段时间。那是由于它必须处理96,560,645种组合!这里咱们就在执行暴力求解
另外一个“暴力” 的itertools函数是permutations(),它接受单个iterable并产生其元素的全部可能的排列(从新排列):
>>> list(it.permutations(['a', 'b', 'c']))
[('a', 'b', 'c'), ('a', 'c', 'b'), ('b', 'a', 'c'),
('b', 'c', 'a'), ('c', 'a', 'b'), ('c', 'b', 'a')]
复制代码
任何三个元素的可迭代对象(好比list)将有六个排列,而且较长迭代的对象排列数量增加得很是快。实际上,长度为n的可迭代对象有n!排列:
只有少数输入产生大量结果的现象称为组合爆炸,在使用combination(),combinations_with_replacement()和permutations()时咱们须要牢记这一点。
说实话,一般最好避免暴力算法,但有时咱们可能必须使用(好比算法的正确性相当重要,或者必须考虑每一个可能的结果)
因为篇幅有限,我先分享到这里,这篇文章咱们主要深刻理解了如下函数的基本原理:
在下一篇文章我会先对最后三个进行总结,而后继续和你们分享itertools里面各类神奇的东西