Python学习教程(Python学习路线):Python编写循环的两个建议

Python学习教程(Python学习路线):给你们总结了两个Python编写循环的建议python

循环是一种经常使用的程序控制结构。咱们常说,机器相比人类的最大优势之一,就是机器能够不眠不休的重复作某件事情,但人却不行。而“循环”,则是实现让机器不断重复工做的关键概念。编程

在循环语法方面,Python 表现的即传统又不传统。它虽然抛弃了常见的 for(init;condition;incrment) 三段式结构,但仍是选择了 for 和 while 这两个经典的关键字来表达循环。绝大多数状况下,咱们的循环需求均可以用 for<item>in<iterable> 来知足, while<condition> 相比之下用的则更少些。数组

虽然循环的语法很简单,可是要写好它确并不容易。这里,咱们将探讨什么是“地道”的循环代码,以及如何编写它们。bash

Python学习教程(Python学习路线):Python编写循环的两个建议

什么是“地道”的循环?ide

“地道”这个词,一般被用来形容某人作某件事情时,很是符合当地传统,作的很是好。打个比方,你去参加一个朋友聚会,同桌的有一位广东人,对方一开口,句句都是标准京腔、完美儿化音。那你能够对她说:“您的北京话说的真地道”。函数

既然“地道”这个词形容的常常是口音、作菜的口味这类实实在在的东西,那“地道”的循环代码又是什么意思呢?让我拿一个经典的例子来解释一下。工具

若是你去问一位刚学习 Python 一个月的人:“如何在遍历一个列表的同时获取当前下标?”。他可能会交出这样的代码:学习

index = 0
for name in names:
 print(index, name)
 index += 1
复制代码

上面的循环虽然没错,但它确一点都不“地道”。一个拥有三年 Python 开发经验的人会说,代码应该这么写:测试

for i, name in enumerate(names):
 print(i, name)
复制代码

enumerate() 是 Python 的一个内置函数,它接收一个“可迭代”对象做为参数,而后返回一个不断生成 (当前下标,当前元素) 的新可迭代对象。这个场景使用它最适合不过。优化

因此,在上面的例子里,咱们会认为第二段循环代码比第一段更“地道”。

由于它用更直观的代码,更聪明的完成了工做。

enumerate() 所表明的编程思路

不过,判断某段循环代码是否地道,并不只仅是以知道或不知道某个内置方法做为标准。咱们能够从上面的例子挖掘出更深层的东西。

如你所见,Python 的 for 循环只有 for<item>in<iterable> 这一种结构,而结构里的前半部分 - 赋值给 item- 没有太多花样可玩。因此后半部分的可迭代对象是咱们惟一可以大作文章的东西。而以 enumerate() 函数为表明的“修饰函数”,恰好提供了一种思路:经过修饰可迭代对象来优化循环自己。

这里就想给你们说一下个人第一个建议了:

建议一:使用函数修饰被迭代对象来优化循环

使用修饰函数处理可迭代对象,能够在各类方面影响循环代码。而要找到合适的例子来演示这个方法,并不用去太远,内置模块 itertools 就是一个绝佳的例子。

简单来讲,itertools 是一个包含不少面向可迭代对象的工具函数集。我在以前的系列文章《容器的门道》里提到过它。

若是要学习 itertools,那么 Python 官方文档 是你的首选,里面有很是详细的模块相关资料。但在这篇文章里,侧重点将和官方文档稍有不一样。我会经过一些常见的代码场景,来详细解释它是如何改善循环代码的。

1. 使用 product 扁平化多层嵌套循环

虽然咱们都知道“扁平的代码比嵌套的好”。但有时针对某类需求,彷佛必定得写多层嵌套循环才行。好比下面这段:

def find_twelve(num_list1, num_list2, num_list3):
 """从 3 个数字列表中,寻找是否存在和为 12 的 3 个数 """
 for num1 in num_list1:
 for num2 in num_list2:
 for num3 in num_list3:
 if num1 + num2 + num3 == 12:
 return num1, num2, num3
复制代码


对于这种须要嵌套遍历多个对象的多层循环代码,咱们可使用 product() 函数来优化它。product() 能够接收多个可迭代对象,而后根据它们的笛卡尔积不断生成结果。

from itertools import product
def find_twelve_v2(num_list1, num_list2, num_list3):
 for num1, num2, num3 in product(num_list1, num_list2, num_list3):
 if num1 + num2 + num3 == 12:
 return num1, num2, num3
复制代码


相比以前的代码,使用 product() 的函数只用了一层 for 循环就完成了任务,代码变得更精炼了。

2. 使用 islice 实现循环内隔行处理

有一份包含 Reddit 帖子标题的外部数据文件,里面的内容格式是这样的:

python-guide: Python best practices guidebook, written for humans.
---
Python 2 Death Clock
---
Run any Python Script with an Alexa Voice Command
---
<... ...>
复制代码


多是为了美观,在这份文件里的每两个标题之间,都有一个 "---" 分隔符。如今,咱们须要获取文件里全部的标题列表,因此在遍历文件内容的过程当中,必须跳过这些无心义的分隔符。

参考以前对 enumerate() 函数的了解,咱们能够经过在循环内加一段基于当前循环序号的 if 判断来作到这一点:

def parse_titles(filename):
 """从隔行数据文件中读取 reddit 主题名称 """
 with open(filename, 'r') as fp:
 for i, line in enumerate(fp):
 # 跳过无心义的 '---' 分隔符
 if i % 2 == 0:
 yield line.strip()
复制代码


但对于这类在循环内进行隔行处理的需求来讲,若是使用 itertools 里的 islice() 函数修饰被循环对象,可让循环体代码变得更简单直接。

islice(seq,start,end,step) 函数和数组切片操做( list[start:stop:step] )有着几乎如出一辙的参数。若是须要在循环内部进行隔行处理的话,只要设置第三个递进步长参数 step 值为 2 便可(默认为 1)。

from itertools import islice
def parse_titles_v2(filename):
 with open(filename, 'r') as fp:
 # 设置 step=2,跳过无心义的 '---' 分隔符
 for line in islice(fp, 0, None, 2):
 yield line.strip()
复制代码

3. 使用 takewhile 替代 break 语句

有时,咱们须要在每次循环开始时,判断循环是否须要提早结束。好比下面这样:

for user in users:
 # 当第一个不合格的用户出现后,再也不进行后面的处理
 if not is_qualified(user):
 break
 # 进行处理 ... ...
复制代码


对于这类须要提早中断的循环,咱们可使用 takewhile() 函数来简化它。takewhile(predicate,iterable)会在迭代 iterable 的过程当中不断使用当前对象做为参数调用 predicate 函数并测试返回结果,若是函数返回值为真,则生成当前对象,循环继续。不然当即中断当前循环。

使用 takewhile 的代码样例:

from itertools import takewhile
for user in takewhile(is_qualified, users):
 # 进行处理 ... ...
复制代码


itertools 里面还有一些其余有意思的工具函数,他们均可以用来和循环搭配使用,好比使用 chain 函数扁平化双层嵌套循环、使用 zip_longest 函数一次同时循环多个对象等等。

篇幅有限,我在这里再也不一一介绍。若是有兴趣,能够自行去官方文档详细了解。

4. 使用生成器编写本身的修饰函数

除了 itertools 提供的那些函数外,咱们还能够很是方便的使用生成器来定义本身的循环修饰函数。

让咱们拿一个简单的函数举例:

def sum_even_only(numbers):
 """对 numbers 里面全部的偶数求和"""
 result = 0
 for num in numbers:
 if num % 2 == 0:
 result += num
 return result
复制代码


在上面的函数里,循环体内为了过滤掉全部奇数,引入了一条额外的 if 判断语句。若是要简化循环体内容,咱们能够定义一个生成器函数来专门进行偶数过滤:

def even_only(numbers):
 for num in numbers:
 if num % 2 == 0:
 yield num
def sum_even_only_v2(numbers):
 """对 numbers 里面全部的偶数求和"""
 result = 0
 for num in even_only(numbers):
 result += num
 return result
复制代码


将 numbers 变量使用 even_only 函数装饰后, sum_even_only_v2 函数内部便不用继续关注“偶数过滤”逻辑了,只须要简单完成求和便可。

Hint:固然,上面的这个函数其实并不实用。在现实世界里,这种简单需求最适合直接用生成器/列表表达式搞定:sum(numfornuminnumbersifnum%2==0)

建议二:按职责拆解循环体内复杂代码块

我一直以为循环是一个比较神奇的东西,每当你写下一个新的循环代码块,就好像开辟了一片黑魔法阵,阵内的全部内容都会开始无休止的重复执行。

但我同时发现,这片黑魔法阵除了能带来好处,它还会引诱你不断往阵内塞入愈来愈多的代码,包括过滤掉无效元素、预处理数据、打印日志等等。甚至一些本来不属于同一抽象的内容,也会被塞入到同一片黑魔法阵内。

你可能会以为这一切理所固然,咱们就是迫切须要阵内的魔法效果。若是不把这一大堆逻辑塞满到循环体内,还能把它们放哪去呢?

让咱们来看看下面这个业务场景。在网站中,有一个每 30 天执行一次的周期脚本,它的任务是是查询过去 30 天内,在每周末特定时间段登陆过的用户,而后为其发送奖励积分。

代码以下:

import time
import datetime
def award_active_users_in_last_30days():
 """获取全部在过去 30 天周末晚上 8 点到 10 点登陆过的用户,为其发送奖励积分 """
 days = 30
 for days_delta in range(days):
 dt = datetime.date.today() - datetime.timedelta(days=days_delta)
 # 5: Saturday, 6: Sunday
 if dt.weekday() not in (5, 6):
 continue
 time_start = datetime.datetime(dt.year, dt.month, dt.day, 20, 0)
 time_end = datetime.datetime(dt.year, dt.month, dt.day, 23, 0)
 # 转换为 unix 时间戳,以后的 ORM 查询须要
 ts_start = time.mktime(time_start.timetuple())
 ts_end = time.mktime(time_end.timetuple())
 # 查询用户并挨个发送 1000 奖励积分
 for record in LoginRecord.filter_by_range(ts_start, ts_end):
 # 这里能够添加复杂逻辑
 send_awarding_points(record.user_id, 1000) 
复制代码


上面这个函数主要由两层循环构成。外层循环的职责,主要是获取过去 30 天内符合要求的时间,并将其转换为 UNIX 时间戳。以后由内层循环使用这两个时间戳进行积分发送。

如以前所说,外层循环所开辟的黑魔法阵内被塞的满满当当。但经过观察后,咱们能够发现整个循环体实际上是由两个彻底无关的任务构成的:“挑选日期与准备时间戳” 以及 “发送奖励积分”。

复杂循环体如何应对新需求

这样的代码有什么坏处呢?让我来告诉你。

某日,产品找过来讲,有一些用户周末半夜不睡觉,还在刷咱们的网站,咱们得给他们发通知让他们之后早点睡觉。因而新需求出现了:“给过去 30 天内在周末凌晨 3 点到 5 点登陆过的用户发送一条通知”

新问题也随之而来。敏锐如你,确定一眼能够发现,这个新需求在用户筛选部分的要求,和以前的需求很是很是类似。可是,若是你再打开以前那团循环体看看,你会发现代码根本无法复用,由于在循环内部,不一样的逻辑彻底被耦合在一块儿了。☹️

在计算机的世界里,咱们常常用“耦合”这个词来表示事物之间的关联关系。上面的例子中,“挑选时间”和“发送积分”这两件事情身处同一个循环体内,创建了很是强的耦合关系。

为了更好的进行代码复用,咱们须要把函数里的“挑选时间”部分从循环体中解耦出来。而咱们的老朋友,“生成器函数”是进行这项工做的不二之选。

使用生成器函数解耦循环体

要把 “挑选时间” 部分从循环内解耦出来,咱们须要定义新的生成器函数 gen_weekend_ts_ranges(),专门用来生成须要的 UNIX 时间戳:

def gen_weekend_ts_ranges(days_ago, hour_start, hour_end):
 """生成过去一段时间内周六日特定时间段范围,并以 UNIX 时间戳返回 """
 for days_delta in range(days_ago):
 dt = datetime.date.today() - datetime.timedelta(days=days_delta)
 # 5: Saturday, 6: Sunday
 if dt.weekday() not in (5, 6):
 continue
 time_start = datetime.datetime(dt.year, dt.month, dt.day, hour_start, 0)
 time_end = datetime.datetime(dt.year, dt.month, dt.day, hour_end, 0)
 # 转换为 unix 时间戳,以后的 ORM 查询须要
 ts_start = time.mktime(time_start.timetuple())
 ts_end = time.mktime(time_end.timetuple())
 yield ts_start, ts_end
复制代码


有了这个生成器函数后,旧需求“发送奖励积分”和新需求“发送通知”,就均可以在循环体内复用它来完成任务了:

def award_active_users_in_last_30days_v2():
 """发送奖励积分"""
 for ts_start, ts_end in gen_weekend_ts_ranges(30, hour_start=20, hour_end=23):
 for record in LoginRecord.filter_by_range(ts_start, ts_end):
 send_awarding_points(record.user_id, 1000)
def notify_nonsleep_users_in_last_30days():
 """发送通知"""
 for ts_start, ts_end in gen_weekend_ts_range(30, hour_start=3, hour_end=6):
 for record in LoginRecord.filter_by_range(ts_start, ts_end):
 notify_user(record.user_id, 'You should sleep more')
复制代码


总结

在这篇Python学习教程文章里,咱们首先简单解释了“地道”循环代码的定义。而后提出了第一个建议:使用修饰函数来改善循环。以后我虚拟了一个业务场景,描述了按职责拆解循环内代码的重要性。

一些要点总结:

  • 使用函数修饰被循环对象自己,能够改善循环体内的代码
  • itertools 里面有不少工具函数均可以用来改善循环
  • 使用生成器函数能够轻松定义本身的修饰函数
  • 循环内部,是一个极易发生“代码膨胀”的场地
  • 请使用生成器函数将循环内不一样职责的代码块解耦出来,得到更好的灵活性

更多的Python学习教程Python学习路线会继续跟你们更新哦!或者你们有想学的相关Python学习教程也能够留言哈!

相关文章
相关标签/搜索