likes
comments
collection
share

重新探索循环与可迭代对象:解密迭代的奥秘

作者站长头像
站长
· 阅读数 3

前言

对于一个编程人员来说,循环迭代应该是开发工作中使用最多的,对于python来说,使用循环也就很简单,for循环、while循环语法都比较简单,但想要真正掌握合理运用,那就需要我们进一步探索了。

迭代器

iter()与next()内置函数

iter()函数会返回一个迭代器对象,像这样

iter([1,2,3]) # <list_iterator object at 0x10a9ce7b0>
iter('123') # <str_ascii_iterator object at 0x10a9cd450>
iter(1) # TypeError: 'int' object is not iterable

从输出结果可以看到,

  • 列表类型的迭代器对象-list_iterator
  • 字符串类型的迭代器对象-str_ascii_iterator
  • 整数类型是不可迭代对象

迭代器到底是啥?

说到底就是帮助你迭代其他对象的对象。特征:不断对它执行next()函数会返回下一次迭代结果。

l = [1, 2]
iter_l = iter(l)
next(iter_l) # 1
next(iter_l) # 2
next(iter_l) # StopIteration

通过iter()返回迭代器对象,然后通过next()函数返回下一次迭代结果,没有值返回时,抛出StopIteration异常。

重要特点:对迭代器执行iter(),获取迭代器的迭代器对象,返回的结果一定是迭代器本身

iter_l # <list_iterator object at 0x10a9cc690>
iter(iter_l) # <list_iterator object at 0x10a9cc690>
iter(iter_l) is iter_l # True

for循环工作原理

先来看看for循环字节码

for i in [1, 2, 3]:
    pass
  
0 LOAD_CONST               0 ((1, 2, 3))
2 GET_ITER
>>    4 FOR_ITER                 4 (to 10)
6 STORE_NAME               0 (i)

LOAD_CONST 0 ((1, 2, 3)):将常量 (1, 2, 3) 加载到栈顶

GET_ITER:从栈顶取出可迭代对象,并获取其迭代器

FOR_ITER 4 (to 10):对于迭代器返回的下一个元素,执行迭代循环

从字节码可以看出,其实就是先调用iter()获取到迭代器,然后不断用next()从迭代器中获取值。

自定义迭代器

关键:实现两个魔法方法__iter____next__

案例:定义一个迭代器来生成斐波那契数列

class FibonacciIterator:
    def __init__(self, limit):
        self.limit = limit
        self.current = 0
        self.previous = 0
        self.next = 1

    def __iter__(self):
        return self

    def __next__(self):
        if self.current >= self.limit:
            raise StopIteration

        value = self.current
        self.current = self.next
        self.previous, self.next = self.next, self.previous + self.next

        return value
      
fib_iter = FibonacciIterator(10)
for num in fib_iter:
    print(num)
    
0
1
1
2
3
5
8

定义了一个名为 FibonacciIterator 的迭代器类。它接受一个参数 limit,用于指定要生成斐波那契数列的长度限制。

__init__ 方法中,我们初始化了迭代器的状态变量。current 存储当前生成的斐波那契数值,previous 存储前一个斐波那契数值,next 存储下一个斐波那契数值。

__iter__ 方法返回迭代器本身,以便可以将迭代器用于 for 循环等迭代操作。

__next__ 方法根据斐波那契数列的生成规则,计算下一个斐波那契数值,并返回当前的斐波那契数值。同时更新状态变量,准备生成下一个斐波那契数值。

这样就完成一个自定义迭代器,看似不错吧,但其实存在一个问题,如果对fib_iter进行第二次遍历,拿不到任何结果,当然这是所有迭代器都存在的问题

仔细看FibonacciIterator代码,会发现当程序第一次遍历完迭代器后,current会不断增长到limit值,第二次遍历,除非收到设置current的值否则没有结果。

如何解决FibonacciIterator对象不可重复使用的问题呢?先看看迭代器和可迭代对象的区别。

可迭代对象

一个合法的迭代器,必须同时实现__iter____next__

可迭代对象,只需要实现__iter__,不一定实现__next__

要想解决FibonacciIterator对象不可重复使用的问题,必须拆分为两部分,可迭代类型Fibonacci和迭代器类型FibonacciIterator。

class Fibonacci:
    def __init__(self, limit):
        self.limit = limit

    def __iter__(self):
        return FibonacciIterator(self)


class FibonacciIterator:
    def __init__(self, obj):
        self.limit = obj.limit
        self.current = 0
        self.previous = 0
        self.next = 1

    def __iter__(self):
        return self

    def __next__(self):
        if self.current >= self.limit:
            raise StopIteration

        value = self.current
        self.current = self.next
        self.previous, self.next = self.next, self.previous + self.next
        return value


fib_iter = Fibonacci(10)
for num in fib_iter:
    print(num)

for num in fib_iter:
    print(num)

我们看到,在这段代码中,每次遍历Fibonacci对象时,会创建一个全新的FibonacciIterator迭代器对象,这样就可以重复使用了。

总结:

  • 可迭代对象不一定是迭代器,但迭代器一定是可迭代对象
  • 可迭代对象使用iter()会返回迭代器,迭代器则会返回自身
  • 每个迭代器的被迭代过程是一次性的,可迭代对象不一定
  • 可迭代对象只需要实现__iter__方法,迭代器需要额外实现__next__方法

可迭代对象与__getitem__

如果一个类型没有定义__iter__,但定义了__getitem__,python也认为是可迭代的。在遍历时,解释器不断使用数字索引(0,1,2...)来调用__getitem__方法获得返回值,直到抛出IndexError为止。这里做个了解,主要是对旧版本的兼容行为,不是主流的迭代器协议。

生成器是迭代器

生成器是一种懒惰的可迭代对象。利用生成器实现FibonacciIterator

def fibonacci_gen(limit):
    current = 0
    previous = 0
    next = 1
    while current <= limit:
        value = current
        current = next
        previous, next = next, previous + next
        yield value
 
fib = fibonacci_gen(10)
next(fib)

使用next()函数不断获取下一个值。

生成器利用简单的语法,降低了迭代器使用门槛,是优化循环代码的得力帮手。

修饰可迭代对象优化循环

典型代表:enumerate()。接收一个可迭代对象作为参数,返回一个不断生成当前(当前下标, 当前元素)的新可迭代对象。

使用生成器函数修饰可迭代对象

案例:计算偶数之和,很好实现,我们很快可以完成代码

def sum_evens(numbers):
    total = 0
    for num in numbers:
        if num % 2 == 0:
            total += num
    return total

借助enumerate()思路,我们可以将找偶数这部分逻辑变成一个生成器函数

def even_only(numbers):
    for num in numbers:
        if num % 2 == 0:
            yield num

def sum_evens_v2(numbers):
    total = 0
    for num in even_only(numbers):
        total += num
    return total

这样在sum_evens_v2函数内部,判断偶数的逻辑就可以去掉了。

修饰可迭代对象是指使用生成器或普通的迭代器在循环外部包装完成原本需要在循环内部执行的工作。

当然,也可以使用标准库模块itertools。

使用itertools模块优化循环

itertools是一个和迭代器相关的标准库。

product()扁平化多层嵌套循环

比如,我们想要得到[[1, 2, 3],[4, 5, 6],[7, 8, 9]]这个列表的所有元素组合,循环实现的话需要好几层循环,利用product()可以轻松实现

from itertools import product

matrix = [[1, 2, 3],[4, 5, 6],[7, 8, 9]]

for combination in product(*matrix):
    print(combination)

使用 product() 函数来生成所有元素的组合。*matrix 表示将 matrix 列表解包作为参数传递给 product() 函数。

product() 函数会返回一个迭代器,该迭代器会依次生成所有可能的组合。每个组合都表示为一个元组。

islice()

itertools.islice() 函数用于对可迭代对象进行切片操作,类似于列表的切片操作 list[start:end:step]

from itertools import islice

numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# 切片操作
sliced_numbers = islice(numbers, 2, 7, 2)

# 打印切片结果
for number in sliced_numbers:
    print(number)

takewhile()替代break语句

itertools.takewhile() 函数用于从可迭代对象中获取满足指定条件的元素,直到条件不再满足为止。一旦条件不满足,takewhile() 将停止迭代并返回结果。

from itertools import takewhile

numbers = [1, 3, 5, 2, 4, 6]

# 获取小于等于 3 的元素
filtered_numbers = takewhile(lambda x: x <= 3, numbers)

# 打印满足条件的结果
for number in filtered_numbers:
    print(number)

通过 takewhile() 函数,我们从 numbers 中获取满足条件(小于等于 3)的元素。参数 lambda x: x <= 3 是一个匿名函数,用于指定条件。

我们将满足条件的结果赋值给变量 filtered_numbers

然后,我们通过遍历 filtered_numbers 打印出满足条件的结果。

chain()

itertools.chain() 函数用于将多个可迭代对象合并成一个迭代器,返回一个包含所有元素的新迭代器。这个函数可以接受任意数量的可迭代对象作为参数。

from itertools import chain

list1 = [1, 2, 3]
list2 = ['a', 'b', 'c']
list3 = [True, False]

# 合并多个列表
merged_list = chain(list1, list2, list3)

# 打印合并后的结果
for item in merged_list:
    print(item)

定义了三个列表 list1list2list3,分别包含一些整数、字符串和布尔值。

通过 chain() 函数,我们将这三个列表合并成一个迭代器。我们将多个可迭代对象作为参数传递给 chain() 函数。

我们将合并后的结果赋值给变量 merged_list

然后,我们通过遍历 merged_list 打印出合并后的结果。

运行上述代码,输出结果为:

1
2
3
a
b
c
True
False

这样,我们成功地使用 chain() 函数将多个可迭代对象合并成一个迭代器,并打印出了结果。

zip_longest()

itertools.zip_longest() 函数用于将多个可迭代对象按最长的长度进行配对,并返回一个新的迭代器。如果某个可迭代对象的长度不足,可以指定一个默认值来填充。

from itertools import zip_longest

list1 = [1, 2, 3]
list2 = ['a', 'b']
list3 = [True, False, None]

# 配对多个列表,使用 None 填充不足的部分
paired_lists = zip_longest(list1, list2, list3, fillvalue=None)

# 打印配对后的结果
for item in paired_lists:
    print(item)

定义了三个列表 list1list2list3,分别包含一些整数、字符串和布尔值。

通过 zip_longest() 函数,我们将这三个列表按照最长的长度进行配对。如果某个列表的长度不足,我们使用 fillvalue 参数指定的值(这里是 None)来填充。

我们将配对后的结果赋值给变量 paired_lists

然后,我们通过遍历 paired_lists 打印出配对后的结果。

运行上述代码,输出结果为:

(1, 'a', True)
(2, 'b', False)
(3, None, None)

这样,我们成功地使用 zip_longest() 函数将多个可迭代对象按最长的长度进行配对,并打印出了结果。你可以根据需求配对不同数量和类型的可迭代对象,并指定不同的填充值。

隐患

迭代器是可以被耗尽的。

numbers = [1,2,3]

numbers = (i * 2 for i in numbers)

4 in numbers # True
4 in numbers # False

numbers使用生成器表达式创建一个新的生成器对象,第一次进行成员判断时,返回True,第二次进行成员判断时,返回False,因为第二次进行判断时,生成器已被部分遍历过,无法在找到4.

谨记:生成器(迭代器)可被一次性耗尽,如果要服用生成器,可以转为列表后使用

最后

掌握这些知识后,实际编程中有意去优化使用,相信我们都可以更好的使用迭代。