循环和可迭代对象高级用法

举报
brucexiaogui 发表于 2022/09/25 00:09:45 2022/09/25
【摘要】 循环和可迭代对象高级用法 1.概述 这篇文章通过循环的本质介绍什么是循环,以及它的相关高级用法。 2.迭代器与可迭代对象 在编写for循环时,不是所有对象都可以用作循环主体——只有那些可迭代的对象...

循环和可迭代对象高级用法

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

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。