前言
这是“Python 工匠”系列的第 7 篇文章。(点击原文链接,可查看系列其余文章)
循环是一种罕用的程序控制构造。咱们常说,机器相比人类的最大长处之一,就是机器能够不眠不休的反复做某件事情,但人却不行。而“ 循环 ”,则是实现让机器一直反复工作的要害概念。
在循环语法方面,Python 体现的即传统又不传统。它尽管摈弃了常见的 for(init;condition;incrment)
三段式构造,但还是抉择了 for 和 while
这两个经典的关键字来表白循环。绝大多数状况下,咱们的循环需要都能够用 for<item>in<iterable>
来满足,while<condition>
相比之下用的则更少些。
尽管循环的语法很简略,然而要写好它确并不容易。在这篇文章里,咱们将探讨什么是“纯粹”的循环代码,以及如何编写它们。
什么是“纯粹”的循环?
“纯粹”这个词,通常被用来形容某人做某件事情时,十分合乎当地传统,做的十分好。打个比方,你去加入一个敌人团聚,同桌的有一位广东人,对方一闭口,句句都是规范京腔、完满儿化音。那你能够对她说:“您的北京话说的真纯粹”。
既然“纯粹”这个词形容的常常是口音、做菜的口味这类实实在在的货色,那“纯粹”的循环代码又是什么意思呢?让我拿一个经典的例子来解释一下。
如果你去问一位刚学习 Python 一个月的人:“如何在遍历一个列表的同时获取以后下标?”。他可能会交出这样的代码:
下面的循环尽管没错,但它确一点都不“纯粹”。一个领有三年 Python 开发教训的人会说,代码应该这么写:
enumerate()
是 Python 的一个内置函数,它接管一个“可迭代”对象作为参数,而后返回一个一直生成 (以后下标, 以后元素)
的新可迭代对象。这个场景应用它最适宜不过。
所以,在下面的例子里,咱们会认为第二段循环代码比第一段更“纯粹”。因为它用更直观的代码,更聪慧的实现了工作。
enumerate() 所代表的编程思路
不过,判断某段循环代码是否纯粹,并不仅仅是以晓得或不晓得某个内置办法作为规范。咱们能够从下面的例子挖掘出更深层的货色。
如你所见,Python 的 for 循环只有 for<item>in<iterable>
这一种构造,而构造里的前半部分 – 赋值给 item-
没有太多花色可玩。所以后半局部的 可迭代对象 是咱们惟一可能大做文章的货色。而以 enumerate()
函数为代表的“润饰函数”,刚好提供了一种思路:通过润饰可迭代对象来优化循环自身。
这就引出了我的第一个倡议。
倡议 1:应用函数润饰被迭代对象来优化循环
应用润饰函数解决可迭代对象,能够在各种方面影响循环代码。而要找到适合的例子来演示这个办法,并不必去太远,内置模块 itertools 就是一个绝佳的例子。
简略来说,itertools 是一个蕴含很多面向可迭代对象的工具函数集。我在之前的系列文章《容器的门道》里提到过它。
如果要学习 itertools,那么 Python 官网文档 是你的首选,外面有十分具体的模块相干材料。但在这篇文章里,侧重点将和官网文档稍有不同。我会通过一些常见的代码场景,来具体解释它是如何改善循环代码的。
1. 应用 product 扁平化多层嵌套循环
尽管咱们都晓得“扁平的代码比嵌套的好”。但有时针对某类需要,仿佛肯定得写多层嵌套循环才行。比方上面这段:
对于这种须要嵌套遍历多个对象的多层循环代码,咱们能够应用 product() 函数来优化它。product()
能够接管多个可迭代对象,而后依据它们的笛卡尔积一直生成后果。
相比之前的代码,应用 product()
的函数只用了一层 for 循环就实现了工作,代码变得更精炼了。
2. 应用 islice 实现循环内隔行解决
有一份蕴含 Reddit 帖子题目的内部数据文件,外面的内容格局是这样的:
可能是为了好看,在这份文件里的每两个题目之间,都有一个 “—” 分隔符。当初,咱们须要获取文件里所有的题目列表,所以在遍历文件内容的过程中,必须跳过这些无意义的分隔符。
参考之前对 enumerate()
函数的理解,咱们能够通过在循环内加一段基于以后循环序号的 if 判断来做到这一点:
但对于这类在循环内进行隔行解决的需要来说,如果应用 itertools 里的 islice() 函数润饰被循环对象,能够让循环体代码变得更简略间接。
islice(seq,start,end,step) 函数和数组切片操作(list[start:stop:step])有着简直截然不同的参数。如果须要在循环外部进行隔行解决的话,只有设置第三个递提高长参数 step 值为 2 即可(默认为 1)。
3. 应用 takewhile 代替 break 语句
有时,咱们须要在每次循环开始时,判断循环是否须要提前结束。比方上面这样:
对于这类须要提前中断的循环,咱们能够应用 takewhile()
函数来简化它。takewhile(predicate,iterable)
会在迭代 iterable
的过程中一直应用以后对象作为参数调用 predicate
函数并测试返回后果,如果函数返回值为真,则生成以后对象,循环持续。否则立刻中断以后循环。
应用 takewhile
的代码样例:
itertools 外面还有一些其余有意思的工具函数,他们都能够用来和循环搭配应用,比方应用 chain 函数扁平化双层嵌套循环、应用 zip_longest 函数一次同时循环多个对象等等。
篇幅无限,我在这里不再一一介绍。如果有趣味,能够自行去官网文档具体理解。
4. 应用生成器编写本人的润饰函数
除了 itertools 提供的那些函数外,咱们还能够十分不便的应用生成器来定义本人的循环润饰函数。
让咱们拿一个简略的函数举例:
在下面的函数里,循环体内为了过滤掉所有奇数,引入了一条额定的 if 判断语句。如果要简化循环体内容,咱们能够定义一个生成器函数来专门进行偶数过滤:
将 numbers
变量应用 even_only
函数装璜后,sum_even_only_v2
函数外部便不必持续关注“偶数过滤”逻辑了,只须要简略实现求和即可。
Hint:当然,下面的这个函数其实并不实用。在事实世界里,这种简略需要最适宜间接用生成器 / 列表表达式搞定:sum(numfornuminnumbersifnum%2==0)
倡议 2:按职责拆解循环体内简单代码块
我始终感觉循环是一个比拟神奇的货色,每当你写下一个新的循环代码块,就如同开拓了一片黑魔法阵,阵内的所有内容都会开始无休止的反复执行。
但我同时发现,这片黑魔法阵除了能带来益处, 它还会诱惑你一直往阵内塞入越来越多的代码,包含过滤掉有效元素、预处理数据、打印日志等等。甚至一些本来不属于同一形象的内容,也会被塞入到同一片黑魔法阵内 。
你可能会感觉这所有天经地义,咱们就是迫切需要阵内的魔法成果。如果不把这一大堆逻辑塞满到循环体内,还能把它们放哪去呢?
让咱们来看看上面这个业务场景。在网站中,有一个每 30 天执行一次的周期脚本,它的工作是是查问过来 30 天内,在每周末特定时间段登录过的用户,而后为其发送处分积分。
代码如下:
下面这个函数次要由两层循环形成。外层循环的职责,次要是获取过来 30 天内符合要求的工夫,并将其转换为 UNIX 工夫戳。之后由内层循环应用这两个工夫戳进行积分发送。
如之前所说,外层循环所开拓的黑魔法阵内被塞的满满当当。但通过观察后,咱们能够发现 整个循环体其实是由两个齐全无关的工作形成的:“筛选日期与筹备工夫戳”以及“发送处分积分”。
简单循环体如何应答新需要
这样的代码有什么害处呢?让我来通知你。
某日,产品找过去说,有一些用户周末中午不睡觉,还在刷咱们的网站,咱们得给他们发告诉让他们当前早点睡觉。于是新需要呈现了:“给过来 30 天外在周末凌晨 3 点到 5 点登录过的用户发送一条告诉”。
新问题也随之而来。敏锐如你,必定一眼能够发现,这个新需要在用户筛选局部的要求,和之前的需要十分十分类似。然而,如果你再关上之前那团循环体看看,你会发现代码基本没法复用,因为在循环外部,不同的逻辑齐全被 耦合
在一起了。☹️
在计算机的世界里,咱们常常用 “耦合”
这个词来示意事物之间的关联关系。下面的例子中,“筛选工夫”和“发送积分”这两件事件身处同一个循环体内,建设了十分强的耦合关系。
为了更好的进行代码复用,咱们须要把函数里的“筛选工夫”局部从循环体中解耦进去。而咱们的老朋友,“生成器函数”
是进行这项工作的不二之选。
应用生成器函数解耦循环体
要把“筛选工夫”局部从循环内解耦进去,咱们须要定义新的生成器函数 gen_weekend_ts_ranges()
,专门用来生成须要的 UNIX 工夫戳:
有了这个生成器函数后,旧需要“发送处分积分”和新需要“发送告诉”,就都能够在循环体内复用它来实现工作了:
总结
在这篇文章里,咱们首先简略解释了“纯粹”循环代码的定义。而后提出了第一个倡议:应用润饰函数来改善循环。之后我虚构了一个业务场景,形容了按职责拆解循环内代码的重要性。
一些要点总结:
- 应用函数润饰被循环对象自身,能够改善循环体内的代码
- itertools 外面有很多工具函数都能够用来改善循环
- 应用生成器函数能够轻松定义本人的润饰函数
- 循环外部,是一个极易产生“代码收缩”的场地
- 请应用生成器函数将循环内不同职责的代码块解耦进去,取得更好的灵活性
看完文章的你,有没有什么想吐槽的?请留言或者在 我的项目 Github Issues 通知我吧。
附录
题图起源: Photo by Lai man nung on Unsplash
更多系列文章地址:https://github.com/piglei/one…