前言

大家好,今天想和大家分享一下我的itertools学习体验及心得,itertools是一个Python的自带库,内含多种非常实用的方法,我简单学习了一下,发现可以大大提升工作效率,在sf社区内没有发现十分详细的介绍,因此希望想自己做一个学习总结。也和朋友们一起分享一下心得

首先,有关itertools的详细介绍,我参考的是Python 3.7官方文档:itertools — Functions creating iterators for efficient looping,大家感兴趣可以去看看,目前还没有中文版本,十分遗憾,这里不得不吐槽一句,为啥有日语,韩语,中文的版本没有跟上呢?

书规正传,itertools 我个人评价是Python3里最酷的东西! 如果你还没有听说过它,那么你就错过了Python 3标准库的一个最大隐藏宝藏,是的,我很快就抛弃了刚刚分享的collections模块:Python 进阶之路 (七) 隐藏的神奇宝藏:探秘Collections,毕竟男人都是大猪蹄子

网上有很多优秀的资源可用于学习itertools模块中的功能。但对我而言,官方文档本身总是一个很好的起点。学会做甜点之前,总是要会最基础的面包。这篇文章便是基本基于文档归纳整理而来。

我在学习后的整体感受是,关于itertools只知道它包含的函数是远远不够的。真正的强大之处在于组合这些功能以创建快速,占用内存效率极少,漂亮优雅的代码。

在这篇很长的文章里,我会全面复盘我的学习历程,争取全面复制每一个细节,在开始之前,如果朋友们还不太知道迭代器和生成器是什么,可以参考以下科普扫盲:

  • 菜鸟教程 迭代器生成器
  • 廖雪峰的迭代器讲解
  • 廖雪峰的生成器讲解

神奇的itertools

好啦,坐好扶稳,我们准备上车了,根据官方文档的定义:

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的每个元素:

  • 模板: map(func,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 里面所谓的 “迭代器代数” 带来的好处有两个:

  1. 提高内存效率 (lazy evaluation)
  2. 提速

可能有朋友对这两个好处有所疑问,不要着急,我们可以分析一个具体的场景:

现在我们有一个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) // nreturn [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)] * nreturn 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()从“first”迭代器中拉出3时,它从“second”获得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 改成 xrange(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 itdef grouper(lst, n, fillvalue=None):iters = [iter(lst)] * nreturn 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

最后心满意足的直接调用一下:

输出结果如下:

暴力求解(brute force)

首先基础概念扫盲,所谓暴力求解是算法中的一种,简单来说就是 利用枚举所有的情况,或者其它大量运算又不用技巧的方式,来求解问题的方法。
我在看过暴力算法的广义概念后,首先想到的居然是盗墓笔记中的王胖子

如果有看过盗墓笔记朋友,你会发现王胖子其实是一个推崇暴力求解的人,在无数次遇到困境时祭出的”枚举法“,就是暴力求解,例如我印象最深的是云顶天宫中,一行人被困在全是珠宝的密室中无法逃脱,王胖子通过枚举排除所有可能性,直接得到”身边有鬼“ 的最终解。

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

最终会在 input中 n 个元素的所有组合的元组上产生一个迭代器。

import  itertools as itbills = [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 itbills = [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()时我们需要牢记这一点。

说实话,通常最好避免暴力算法,但有时我们可能必须使用(比如算法的正确性至关重要,或者必须考虑每个可能的结果)

小结

由于篇幅有限,我先分享到这里,这篇文章我们主要深入理解了以下函数的基本原理:

  • map()
  • zip()
  • itertools.combinations
  • itertools.combinations_with_replacement
  • itertools.permutations

在下一篇文章我会先对最后三个进行总结,然后继续和大家分享itertools里面各种神奇的东西

Python 进阶之路 (九) 再立Flag, 社区最全的itertools深度解析(上)相关推荐

  1. Python 进阶之路 (十) 再立Flag, 社区最全的itertools深度解析(中)

    前情回顾 大家好,我又回来了.今天我会继续和大家分享itertools这个神奇的自带库,首先,让我们回顾一下上一期结尾的时候我们讲到的3个方法: combinations() combinations ...

  2. python flagin flagout_Python 进阶之路 (十) 再立Flag, 社区最全的itertools深度解析(中)...

    前情回顾 大家好,我又回来了.今天我会继续和大家分享itertools这个神奇的自带库,首先,让我们回顾一下上一期结尾的时候我们讲到的3个方法: combinations() combinations ...

  3. python flag用法_Python 进阶之路 (四) 先立Flag, 社区最全的Set用法集锦

    Set是什么 大家好,恰逢初五迎财神,先预祝大家新年财源滚滚!! 在上一期详解tuple元组的用法后,今天我们来看Python里面最后一种常见的数据类型:集合(Set) 与dict类似,set也是一组 ...

  4. Python 进阶之路 (十二) 尾声即是开始

    Python进阶之路总结 大家好,我的<< Python进阶之路>>到这一期就到此为止了,和 <<Python 基础起步>>不同,在掌握了一些基础知识后 ...

  5. Python 进阶之路 (八) 最用心的推导式详解 (附简单实战及源码)

    什么是推导式 大家好,今天为大家带来问我最喜欢的Python推导式使用指南,让我们先来看看定义~ 推导式(comprehensions)是Python的一种独有特性,推导式是可以从一个数据序列构建另一 ...

  6. 毛毛Python进阶之路6——MySQL 数据库(二)

    毛毛Python进阶之路6--MySQL 数据库(二) 一.对于自增 show create table 表名; # 查看表是怎样创建的. show create table 表名\G; #将某个表旋 ...

  7. Python进阶之路第一话之python引力

    Python的魅力:我来引你中毒 我不想说一些Python是世界最好的编程语言之类的鬼话,在我看来,每一种编程语言都有他们各自擅长的领域和不擅长的领域.举个例子,我用一根针,来削铅笔,很费劲不能实现, ...

  8. Python进阶之路:namedtuple

    Python中的tuple大家应该都非常熟悉了.它可以存储一个Python对象序列.与list不同的是,你不能改变tuple中元素的值.tuple的元素是通过索引进行访问的: Tuple还有一个兄弟, ...

  9. 《正规军的Python进阶之路|Python技能树测评》

    通过<Python技能树测评>判断自己在哪个级别: Python技能树测评[https://bbs.csdn.net/skill/python] 完整的[Python]学习体系,给你正规军 ...

最新文章

  1. 什么是以太坊,它是区块链2.0的代表,它又什么特点?
  2. 有源汇上下界最小费用可行流 ---- P4553 80人环游世界(拆点 + 有源汇上下界最小费用可行流)
  3. 一个女生写的如何追mm.看完后嫩头青变高手.zz(转贴)
  4. stand up meeting 12/24/2015 end sprint1
  5. iPhone屏幕知识点
  6. java util logging_Java 日志系列篇一 原生 Java.util.logging
  7. 超详细!一文教你如何备考HCIE!
  8. NFC与RFID的原理及应用区别
  9. JZOJ 5574. 【NOI2018模拟3.10】占领
  10. vSphere 计算vMotion的迁移原理
  11. XHTML学习笔记 Part2:核心元素
  12. Laravel核心代码学习--用户认证系统(基础介绍)
  13. Turbo C 2.0、Borland C++库函数及用例
  14. 网络工程师考试模拟器
  15. sublime3103 破解及Package Control离线安装
  16. 您的博文被删除了(1)
  17. 关于jquery 1.9以上多次点击checkbox无法选择的
  18. TP-link路由器设置界面展示
  19. 概要、详细设计文档内容简述
  20. 凹凸贴图(Bump Map)实现原理以及与法线贴图(Normal Map)的区别

热门文章

  1. Delphi XE5 for Android (十)
  2. Window7系统 中常见的进程命令分析?
  3. Object Pascal 运算符,常量,变量
  4. date新的使用方法
  5. 查看系统信息命令:uname
  6. 近7000字长文详细讲解Java包装类,面试稳了
  7. 坐标1-based和0-based
  8. Java虚拟机2:Java 运行时数据区
  9. 支付宝打造公共账号业务网关, RSA密钥对生成
  10. tableview插入刷新_iOS中tableview的几种刷新