前言这是“PythonTinkerers”系列的第七篇文章。(点击原文链接可查看该系列其他文章)循环是一种常用的程序控制结构。我们常说,机器相对于人类最大的优势之一就是机器可以不眠不休地重复做一件事,而人类做不到。而“循环”是实现机器重复工作的关键概念。在循环语法方面,Python既传统又标新立异。虽然它摒弃了常见的for(init;condition;incrment)三阶段结构,但仍然选择了for和while这两个经典的关键字来表达循环。在大多数情况下,我们的循环需求可以通过for- in来满足,而相比之下用得更少。循环的语法虽然简单,但要写好却并不容易。在本文中,我们将探讨什么是“惯用的”循环代码,以及如何编写它们。什么是“真实”循环?“地道”一词通常用来形容某人做某事时,非常符合当地传统,做得非常好。比如你去参加一个朋友的聚会,同桌有一个广东人。对方说话的时候,每一句话都是标准的北京口音,口音完美。然后你就可以对她说:“你的北京话真地道。”由于“正宗”一词经常描述口音和烹饪口味等内容,因此“正宗”循环代码是什么意思?让我用一个经典的例子来解释。如果你问一个刚学Python一个月的人:“如何在遍历列表时获取当前下标?”。他可能会交出这样的代码:上面的循环很好,但它一点也不“地道”。有三年Python开发经验的人会说代码应该这样写:enumerate()是Python中的一个内置函数,它接受一个“可迭代”对象作为参数,然后返回一个不断生成的(当前下标),当前元素)的新迭代对象。最适合这个场景。所以,在上面的例子中,我们会认为第二个循环代码比第一个更“惯用”。因为它可以通过更直观的代码更智能地完成工作。以enumerate()为代表的编程思想但是,判断某个循环代码是否正宗,并不能仅仅以知道或不知道某个内置方法为标准。我们可以从上面的例子中深入挖掘。可以看到,Python的for循环只有for
- in这样的结构,而结构的前半部分——对item的赋值——并没有多少花样可玩。所以后半部分的可迭代对象是我们唯一可以大做文章的东西。enumerate()函数所代表的“修饰函数”只是提供了一种思路:通过修改可迭代对象来优化循环本身。这引出了我的第一个建议。建议一:使用函数修饰可迭代对象来优化循环使用修饰函数来处理可迭代对象可以从多方面影响循环代码。而且您不必费力寻找合适的示例来演示这种方法,内置模块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])几乎完全相同的参数。如果需要在循环内部进行隔行处理,只需要将第三个progressivestep参数step值设置为2(默认为1)即可。3、使用takewhile代替break语句有时候,我们需要在每次循环开始时判断循环是否需要提前结束。例如如下:对于这种需要提前中断的循环,我们可以使用takewhile()函数来简化。takewhile(predicate,iterable)在迭代过程中会不断以当前对象为参数调用predicate函数,并测试返回结果。如果函数返回值为真,则生成当前对象并继续循环。否则,当前循环立即中断。使用takewhile的代码示例:itertools中还有一些其他有趣的工具函数,它们可以与循环结合使用,例如使用chain函数压平双层嵌套循环,使用zip_longest函数一次循环多个对象等。等待。篇幅有限,这里就不一一介绍了。如果你有兴趣,可以去官方文档了解更多。4、使用generators编写自己的装饰函数除了itertools提供的那些函数,我们还可以使用generators来非常方便的定义自己的循环装饰函数。我们以一个简单的函数为例:在上面的函数中,为了过滤掉循环体中的所有奇数,额外引入了一个if语句。如果我们想简化循环体的内容,我们可以定义一个偶数过滤的生成器函数:用even_only函数修饰numbers变量后,sum_even_only_v2函数就不需要继续关注“even--numberfiltering”逻辑在函数内部,只需要简单的完成求和即可。能。提示:当然,以上功能并不实用。在现实世界中,这种简单的需求最适合直接用生成器/列表表达式来解决:sum(numfornuminnumbersifnum%2==0)建议2:将复杂的代码块在循环体中按照职责拆解。我一直认为循环是一种魔法每当你写一个新的循环代码块时,就像打开了一个黑色的魔法圈,圈中的所有内容都会开始无休止地重复。但我也发现,这个黑魔法数组除了好处之外,还会引诱你不断往数组里塞越来越多的代码,包括过滤无效元素、预处理数据、打印日志等等。甚至一些原本不属于同一个抽象的内容,也会被塞进同一个黑魔法阵中。你可能认为这一切都是理所当然的,我们只是迫切需要阵法中的魔法效果。如果你不把所有这些逻辑都塞进循环体,你还能把它放在哪里?我们来看看下面的业务场景。在该网站中,有一个每30天执行一次的周期性脚本。它的任务是查询过去30天内每个周末在特定时间段登陆过的用户,然后给他们发奖励积分。代码如下:上面的函数主要由两层循环组成。外层循环的职责主要是获取最近30天符合要求的时间,转换成UNIX时间戳。之后,内层循环使用这两个时间戳进行积分发送。如前所述,外循环形成的黑色魔法阵已经满了。但是经过观察我们可以发现,整个循环体实际上是由两个完全不相关的任务组成的:“选择日期并准备时间戳”和“发送奖励积分”。代码有什么问题,比如复杂的循环体如何响应新的要求?让我告诉你。一天,产品来找我说,有些用户周末半夜不睡觉,还在浏览我们的网站,我们要给他们发一个通知,让他们以后早点睡觉。于是出现了一个新的需求:“向过去30天内周末凌晨3点到5点登录的用户发送通知”。新的问题也随之而来。敏锐的你肯定一眼就能发现,这个新需求在用户筛选部分的要求和之前的需求非常非常相似。但是,如果你看之前的循环体,你会发现代码根本无法复用,因为在循环内部,不同的逻辑是完全耦合在一起的。??在计算机世界中,我们经常用“耦合”这个词来表示事物之间的关系。在上面的例子中,“选择时间”和“发送点”这两个东西在同一个循环体中,建立了很强的耦合关系。为了更好的代码复用,我们需要将函数的“采摘时间”部分与循环体解耦。而我们的老朋友,“生成器函数”,是这项工作的不二之选。使用生成器函数解耦循环体为了将“选择时间”部分从循环中解耦,我们需要定义一个新的生成器函数gen_weekend_ts_ranges(),它专门用于生成所需的UNIX时间戳:有了这个生成器函数,最后,旧需求“发送奖励积分”和新需求“发送通知”可以在循环体中重复使用来完成任务:总结在本文中,我们先简单解释一下“正宗”循环代码的定义。然后是第一个建议:使用装饰器函数来改进循环。然后我创建了一个虚拟的业务场景,按职责描述了在循环中拆解代码的重要性。一些关键点的总结:使用一个函数来装饰循环对象本身可以改进循环体中的代码。itertools中有许多实用函数可用于改进循环。使用生成器函数可以很方便的在循环内部定义自己的装饰函数,这种情况很容易发生”在“代码扩展”的情况下,请使用生成器函数将循环中不同职责的代码块解耦,以获得更好的灵活性.看完文章,有什么不满意的吗,欢迎在项目GithubIssues中留言或者告诉我附录题图来源:PhotobyLaimannungonUnsplash更多系列文章地址:https://github.com/piglei/一个...