循环和可迭代对象高级用法
循环和可迭代对象高级用法
1.概述
这篇文章通过循环的本质介绍什么是循环,以及它的相关高级用法。
2.迭代器与可迭代对象
在编写for循环时,不是所有对象都可以用作循环主体——只有那些可迭代的对象才行。说到可迭代对象,你最先想到的肯定是哪些内置类型,比如字符串、生成器以及所有容器类型,等等。
除了这些内置类型外,可以轻松定义其他可迭代类型。但在此之前,我们要先搞清楚python里的迭代究竟是怎么一回事。这就需要引入两个重要的内置函数:iter()和next()
2.1.迭代器
1.迭代器就是for循环本质
迭代器就是一种帮助你迭代其他对象的对象,迭代器最鲜明的特征是:不断对它执行next()函数会返回下一次迭代结果。当迭代器没有更多值可以返回时,变回抛出StopIterration异常。
迭代器例子
l = [1, 2, 3]
# 通过iter函数拿到列表l的迭代器对象
iter_l = iter(l)
# 然后对迭代器调用next()不断获取列表下一个值
print(next(iter_l))
- 1
- 2
- 3
- 4
- 5
除了使用next()拿到迭代结果以外,迭代器还有一个重要的特点,那就是当你对迭代器执行iter()函数,尝试获取迭代器的迭代器对象时,返回的结果一定是迭代器本身。
print(iter(iter_l))
<list_iterator object at 0x10c139c00>
- 1
- 2
了解完上述概念后,你就已经了解了for循环的工作原理。当你使用for循环遍历某个可迭代对象时,其实是县调用了iter()拿到它的可迭代器,然后不断调用next()从迭代器中回去值。
下面这段for循环代码,可以翻译成迭代器。
names= ['foo', 'bar', 'foobar']
for name in names:
print(name)
# 将上面的代码改成下面的样子
names= ['foo', 'bar', 'foobar']
# 使用iter()函数得到names的迭代器
iterator = iter(names)
# 通过next()不断获取迭代器的结果
while True:
try:
name = next(iterator)
print(name)
except StopIteration:
break
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
2.自定义迭代器
要自定义一个迭代器类型,关键在于实现下面两个魔法方法。
- __iter__调用iter()时触发,迭代器对象总是返回自身
- _next__调用next()时触发,通过return来返回结果,没有更多内容就抛出StopIteration异常。
创建自定义迭代器示例
假如我想编写一个和range()类型的迭代器对象,他可以返回某个范围内被7整除或包含7的整数。
class Range7:
'''生成某个范围内可被7整除或包含7的整数
:param start: 开始数字
:param end: 结束数字
'''
def __init__(self, start, end):
self.start = start
self.end = end
# 使用current保存当前所处位置
self.current = start
# 返回迭代器对象本身
def __iter__(self):
return self
def __next__(self):
while True:
if self.current > self.end:
raise StopIteration
if self.num_is_valid(self.current):
ret = self.current
self.current += 1
return ret
self.current += 1
def num_is_valid(self, num):
if num == 0:
return False
return num % 7 == 0 or '7' in str(num)
# 通过for循环验证迭代器执行结果
r = Range7(0,20)
for n in r:
print(n)
# 输出结果
7
14
17
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
这个迭代器虽然可以满足需求,但是每个Range7对象只能遍历一遍,假如做第二次遍历就会拿不到结果
r = Range7(0,20)
# 第一次遍历迭代器
print(tuple(r))
#输出结果
(7, 14, 17)
#第二次遍历迭代器
print(tuple(r))
#输出结果
()
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
3.迭代器和可迭代对象区别
迭代器和可迭代对象的含义大不相同
迭代器
迭代器是可迭代对象的一种,它的使用场景就是迭代其他对象时,作为一种介质或工具对象存在。每个迭代器对应一次完整的迭代过程,因此它自身必须保存与迭代相关的状态——迭代位置(就像Range7里面的current属性)
一个合法的迭代器,必须同时实现__iter__和__next__两个魔法方法
可迭代对象
判断一个对象是否可迭代的唯一标准就是调用iter(obj),然后看结果是不是一个迭代器。因此可迭代对象只需要实现__iter__方法,不一定得实现__next__方法。
如果想让Range7对象在每次迭代时都返回完整结果,我们可以将其拆分成两部分,可迭代对象Range7P和迭代器Range7PIterator
# 创建可迭代对象,返回一个迭代器对象
class Range7P:
def __init__(self, start, end):
self.start = start
self.end = end
# 返回一个新的迭代器对象
def __iter__(self):
# 可迭代对象传入迭代器
return Range7PIterator(self)
# 创建一个迭代器
class Range7PIterator:
def __init__(self, range_obj):
self.range_obj = range_obj
self.current = range_obj.start
def __iter__(self):
return self
def __next__(self):
while True:
if self.current >= self.range_obj.end:
raise StopIteration
if self.num_is_valid(self.current):
ret = self.current
self.current += 1
return ret
self.current += 1
def num_is_valid(self, num):
if num == 0:
return False
return num % 7 == 0 or '7' in str(num)
r = Range7P(0, 20)
print(tuple(r))
# 输出结果
(7, 14, 17)
print(tuple(r))
# 输出结果
(7, 14, 17)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
总结迭代器和可迭代对象区别
- 可迭代对象不一定是迭代器,但迭代器一定是可迭代对象
- 对可迭代对象使用iter()会返回迭代器,迭代器则会返回自身。
- 每个迭代器被迭代的过程是一次性的,可迭代对象则不一定
- 可迭代对象只需实现__iter__方法,而迭代器要额外实现__next__方法
4.生成器是迭代器
生成器是一种简化的迭代器实现,使用它可以大大降低实现传统迭代器的编码成本。因此在平时,我们基本不需要通过__iter__和__next__来实现迭代器,只要写上几个yield即可。
生成器利用简单的语法,大大降低了迭代器的使用门槛,是优化循环代码的最得力助手。
利用生成器改写上面的Range7PIterator迭代器,只有6行的代码。
def range7_gen(start, end):
num = start
while num < end:
if num != 0 and (num % 7 == 0 or '7' in str(num)):
yield num
num += 1
# 使用iter()和next()函数验证生成器就是迭代器
nums = range7_gen(0, 20)
# 验证生成器返回值类型是一个迭代器对象
print(iter(nums))
<generator object range7_gen at 0x10c481af0>
# 使用next不断获取下一个值
print(next(nums))
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
5.修饰可迭代对象优化循环
什么是修饰可迭代对象,就是创建一个函数对可迭代对象的值做修改输出符合业务逻辑值。
通过生成器可修改迭代对象,下面通过一个示例介绍它的用法。
def sum_even_only(numbers):
result = 0
for num in numbers:
if num % 2 == 0:
return num
result += num
return result
- 1
- 2
- 3
- 4
- 5
- 6
- 7
这段代码的循环体内使用if语句剔除所有奇数,我们可以把“奇数剔除逻辑”提炼成一个生成器函数,从而简化循环内部代码。
# 把“奇数剔除逻辑”提炼成一个生成器函数,修饰可迭代对象
def even_only(numbers):
for num in numbers:
if num % 2 == 0:
yield num
# 只需要做求和操作,去掉了剔除奇数逻辑。
def sum_even_only2(numbers):
result = 0
for num in even_only(numbers):
result += num
return result
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
修饰可迭代对象是指用生成器(或普通的迭代器) 在循环外部包装原本的循环主体(sum_even_only函数中循环主体),完成一些原本必须在循环内部执行的工作——比如过滤特定成员,提供额外结果等,以此简化循环代码。
3.使用itertools模块优化循环
itertools是一个和迭代器有关的标准库模块,其中包含许多用来处理可迭代对象的工具函数。下面来介绍几个函数优化循环的例子。
3.1.使用product函数扁平化嵌套循环
虽然我们知道”扁平优于嵌套“但是为实现某些需求不得不使用嵌套循环才行,例如下面的例子。
def find_twelve(num_list1, num_list2, num_list3):
'''从3个列表里寻找和等于12的三个数字'''
for num1 in num_list1:
for num2 in num_list2:
for num3 in num_list3:
if num1 + num2 + num3 == 12:
return num1, num2, num3
- 1
- 2
- 3
- 4
- 5
- 6
- 7
对于这种嵌套循环我们可以使用product函数来优化它。product接收多个可迭代对象作为参数,然后根据他们笛卡尔积不断生成结果。
from itertools import product
ls = list(product([1, 2], [3, 4]))
print('输出组合结果:', ls)
# 输出结果
输出组合结果: [(1, 3), (1, 4), (2, 3), (2, 4)]
- 1
- 2
- 3
- 4
- 5
- 6
使用product函数优化上面的循环,新函数只用了一层循环完成了任务。
from itertools import product
def find_product(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
- 1
- 2
- 3
- 4
- 5
- 6
3.2.islice函数实现循环内隔行处理
假如遇到下面的一份数据,可能为了美观每两行之间就有一个”—“ 分隔符,但是我们在读取每行数据时这个分隔符是没有意义的,给我们的遍历带来了一点小麻烦,利用enumerate函数,可以直接在循环内加上一个基于当前行号做判断实现,如下示例。
def parse_titles(filename):
'''从隔行数据中读取'''
with open(filename, 'r') as fp:
for i, line in enumerate(fp):
# 跳过---行
if i % 2 == 0:
yield line.strip()
- 1
- 2
- 3
- 4
- 5
- 6
- 7
如果使用islice函数修饰被循环对象,循环代码可以变得更简单,更直接。
islice(seq, start, end, step) 函数和数组切片操作一样,如果需要在循环内实现隔行处理,只需要在第四个参数step设置步长为2即可。
from itertools import islice
def parse_islice(filename):
with open(filename, 'r') as fp:
for line in islice(fp, 0, None, 2):
yield line.strip()
- 1
- 2
- 3
- 4
- 5
3.3.使用takewhile函数替代break语句
有时,我们在循环过程中对当前值做判断决定是否中断循环,例如下面的代码
for user in users:
# 当出现不合格的用户后,不再继续循环
if not is_qualified(user):
break
- 1
- 2
- 3
- 4
如果使用takewhile(predicate, iterable)函数会在迭代第二个参数iterable过程中,不断使用当前值作为参数调用predicate()函数,并对返回结果做真值测试。如果返回True则继续,否则中断本次迭代。
for user in takewhile(is_qualified, users):
- 1
- 2
3.4.循环语句的else关键字
else在python中是一个特殊的关键字 ,在for中表示循环正常结束则执行else中的语句。
def process_tasks(tasks)
'''如果遇到状态不为pending的任务,则终止本次处理'''
non_pending = False
for task in tasks:
if not task.is_pending():
non_pending = True
break
process(task)
if non_pending:
notify_admin('Found non-pending task ,processing aborted')
else:
notify_admin('All tasks was processed')
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
函数在循环执行结束时通知管理员,为了在不同情况下发送不同的通知,函数在循环开始前定义了一个标记变量non_pending
如果利用else关键字,上面的代码可以更简单。
def process_tasks(tasks)
'''如果遇到状态不为pending的任务,则终止本次处理'''
for task in tasks:
if not task.is_pending():
notify_admin('Found non-pending task ,processing aborted')
break
process(task)
else:
notify_admin('All tasks was processed')
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
4.使用循环分块读取大文件
当用for循环遍历一个文件对象,并可逐行读取它的内容。但这种方式在碰到大文件时会出现内存溢出等问题。通过下面的例子介绍如果读取大文件。
1.读取文件标准做法
def count_digits(fname):
'''计算文件里包含多少个数字字符'''
count = 0
with open(fname) as file:
for line in file:
for s in line:
if s.isdigit():
count += 1
return count
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
上面的写法是python读取文件的标准做法,使用with open打开一个上下文管理器获得一个文件对象,然后用for循环迭代他,逐行获取文件内存。这种方式有一个缺点,被读取的文件如果没有换行符,将会一次性读取整个文件,生成一个巨大的字符串对象,耗费大量时间和内存。
2.使用while循环加read方法分块读取
处理直接遍历文件对象来逐行读取文件内容外,还可以调用更底层的file.read()方法。每次调用file.read(chunk_size),都会从当前游标位置往后chunk_size大小的文件内容,不必等待换行符出现。
def count_digits_v2(fname):
count = 0
block_size = 1024 * 8
with open(fname) as file:
while True:
# 当文件没有更多内容的时候,将返回空字符串
chunk = file.read(block_size)
if not chunk:
break
for s in chunk:
if s.isdigit():
count += 1
return count
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
3.iter()另一个用法
iter()是一个用来获取迭代器对象的函数,它还有另外一个功能。当我们以iter(callable, sentinel)调用会拿到一个特殊的迭代器对象,用循环遍历这个迭代器,会不断返回调用callable()的结果,假如结果等于sentinel,迭代过程终止。下面是使用例子。
from functools import partial
def count_digits_v3(fname):
count = 0
block_size = 1024 * 8
with open(fname) as fp:
# 使用functolls.partial 构造一个无需参数的函数
# callable = _read sentinel= ''
_read = partial(fp.read, block_size)
for chunk in iter(_read, ''):
for s in chunk:
if s.isdigit():
count += 1
return count
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
5.编程建议
5.1.中断嵌套循环的正确方式
在python里使用break用来停止循环体,如果在一个多层嵌套的循环里中断时,一个break就显得不够用了,下面是一个break中断的例子。
在文件中找到第一个特定前缀开头的单词,为了让程序在找到第一个单词时中断查找,写了两个break内层循环一个,外层循环一个,这其实是不得已而为之,因为python不支持带标签break语句。
def print_first_word(fp, prefix):
first_word = None
for line in fp:
for word in line.split():
if word.startswith(prefix):
first_word = word
# 这里的break只能停止最内侧循环
break
# 一定要在外层加一个额外的break语句判断是否结束循环
if first_word:
break
if first_word:
print(f'found the first word startswith "{prefix}": {first_word}')
else:
print(f'word starts with {prefix} was not found')
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
这样写其实并不好,许许多多的break会让代码变得难易理解,也会出现bug,如果想从循环中快速跳出来,那就是把循环代码拆分为一个新的函数。
def find_first_word(fp, prefix):
for line in fp:
for word in line.split():
if word.startswith(prefix):
return word
return None
def print_first_word(fp, prefix):
first_word = find_first_word(fp, prefix)
if first_word:
print(f'found the first word startswith "{prefix}": {first_word}')
else:
print(f'word starts with {prefix} was not found')
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
这样修改后,嵌套循环里的中断逻辑就变得更容易理解了。
5.2.巧用next()函数
内置函数next()是否成迭代器的关键函数,next函数很有趣,配合恰当的迭代器经常可以用很少的代码完成意想不到的功能。
举个例子,假如有个一字典,你要怎么拿到它第一个key那。
直接调用d.keys()[0] 是不对的,因为字典不是普通的容器,不支持切片操作。
为了获取第一个key,需要将字典转为普通该列表才行,这么做有一个很大的缺点,那就是字典内容很多list需要在内存中构建一个大列表,占用内存,执行效率也低。
list(d.keys())[0]
- 1
假如用next()函数将非常简单,只要先用iter获取d.keys的迭代器,在对它调用next就能马上拿到第一个元素。这样做不需要变量字典的所有key,比转换列表更高效
next(iter(d.keys()))
- 1
在可迭代对象上执行next还可以完成元素查找类的工作
例如在列表中查找第一个可以被7整除的数字
numbers = [1, 2, 3, 4, 5, 6, 7]
print(next(i for i in numbers if i % 7 == 0))
- 1
- 2
5.3.当心已被耗尽的迭代器
使用生成器的好处很多,例如比列表更省内存,可以用来解耦循环体代码等等,但是它也有一个陷阱就是迭代器会被耗尽。
下面举个迭代器耗尽的例子
number = [1, 2, 3, 4]
# 使用生成器表达式创建一个新的生成器对象
number = (i * 2 for i in number)
# 连着两次对number成员做判断,返回了不同的结果。
print(4 in number)
print(4 in number)
# 输出结果
True
False
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
这种由生成器的耗尽特性所导致的bug是非常隐蔽的,当他出现在一些复杂项目中就很难定位。
因此你要将生成器(迭代器)的”可被一次性耗尽“ 特点铭记于心,避免写出有他导致的bug。假如要重复使用一个生成器,可以调用list()函数将它转为列表后在使用。
除了生成器函数、生成器表达式以外,还尝尝忽略内置的map()、filter()函数也会返回一个一次性迭代器对象。
文章来源: brucelong.blog.csdn.net,作者:Bruce小鬼,版权归原作者所有,如需转载,请联系作者。
原文链接:brucelong.blog.csdn.net/article/details/126771316
- 点赞
- 收藏
- 关注作者
评论(0)