Python编程-深度剖析迭代器与生成器机制和CPython代码实现

Python编程-深度剖析迭代器与生成器机制和CPython代码实现

感谢前面大佬的学习成果,本文基于python3.11.6如果你的验证出错,那有可能是版本问题或者我debug后忘记修改代码

参考资料:

  • https://www.cnblogs.com/wj-1314/p/8490822.html
  • https://pythonhowto.readthedocs.io/zh-cn/latest/iterator.html#id6
  • https://www.runoob.com/python3/python3-iterator-generator.html
  • https://docs.python.org/zh-tw/3.9/c-api/iterator.html
  • https://docs.python.org/3.12/glossary.html
  • https://zhuanlan.zhihu.com/p/82787357
  • https://blog.csdn.net/gymaisyl/article/details/83180730
  • https://www.cnblogs.com/traditional/p/14040093.html
  • Python Cook Book 第四章
  • python源码地址:https://www.python.org/ftp/python/3.12.0/Python-3.12.0.tgz

容器,管道,迭代与可迭代对象

  • 容器对象

容器对象是用于存储和组织其他对象的数据结构,像列表(list)、集合(set)、序列(tuple)、字典(dict)都是容器,可以逐个迭代获取其中的元素。容器可以用in来判断容器中是否包含某个元素,可以用循环结构来遍历容器中的其他对象

  • 管道对象

管道(Pipeline)通常是指一系列相互连接的处理单元,其中一个处理单元的输出直接成为下一个处理单元的输入。这种方式可以用于将复杂的任务分解为一系列简单的步骤,每个步骤负责特定的处理或转换,python中的subprocess.PIPE 是一个特殊的常量,用于创建子进程的标准输入、输出或错误的管道。这种管道允许子进程和父进程之间进行通信,subprocess.PIPE就是一个管道对象

  • 迭代与可迭代对象

在 Python 中通过循环结构对对象进行遍历的操作被称为迭代(Iteration),可以进行迭代操作的对象被称为可迭代 (Iterable) 对象,我们可以使用isinstance(对象, 对象类型)来对一个对象进行判断:

from collections.abc import Iterable
print(isinstance('abc', Iterable))

经过广泛的资料查找,这个地方使用这样的方法检测可能会引起一定的争议,后文细讲

常见的可迭代对象有:列表(list)、集合(set)、序列(tuple)、字典(dict)、文件对象、管道对象、以及生成器对象

何为迭代器

迭代器是一个可以记住遍历的位置的对象。迭代器对象从集合的第一个元素开始访问,直到所有的元素被访问完结束。迭代器只能往前不会后退。迭代器有两个基本的方法:iter()next()

字符串,列表或元组对象都可用于创建迭代器,但是,可迭代对象不一定是迭代器对象,在迭代器的实现中必须同时实现 __iter__()__next__()方法,__next__() 方法包含了用户自定义的推导算法,这是迭代器对象的本质

迭代器与可迭代对象的关系

可迭代对象(iterable)和迭代器对象(iterator)是两个相关但不同的概念。

可迭代对象(Iterable):

  • 可迭代对象是一个对象,它实现了 __iter__() 方法,该方法返回一个迭代器对象。
  • 可迭代对象可以被用于 for...in 循环,因为循环内部会自动调用 iter() 方法获取迭代器。
my_list = [1, 2, 3]

for item in my_list:
    print(item)

在这个例子中,my_list 是一个可迭代对象,因为它实现了 __iter__() 方法。

迭代器对象(Iterator):

  • 迭代器对象是一个实现了 __iter__()__next__() 方法的对象。
  • 迭代器对象负责迭代序列中的元素,每次调用 __next__() 方法返回下一个元素。
my_list = [1, 2, 3]
my_iterator = iter(my_list)

print(next(my_iterator))  # 输出: 1
print(next(my_iterator))  # 输出: 2
print(next(my_iterator))  # 输出: 3

在这个例子中,my_iterator 是一个迭代器对象,因为它实现了 __iter__()__next__() 方法。

即所有迭代器对象都是可迭代的,但并非所有可迭代对象都是迭代器。可迭代对象只需要实现 __iter__() 方法,而迭代器对象需要实现 __iter__()__next__() 方法。

迭代器对象的使用

对于迭代器对象的使用通常不是独立的,这里我们演示单独使用迭代器的方法:

  • 对于容器对象,除了使用iter进行转换,我们还可以调用其迭代时自调用的魔术方法__iter__()
list = [1, 2, 3, 4, 5, 6]
list_iter = list.__iter__()

while True:
    next_element = next(list_iter, None) # next方法的第二个参数用于迭代完毕所有参数后继续迭代返回的默认参数
    if next_element is None:
        break
    print(next_element, end='  ')

'''
运行结果: 1  2  3  4  5  6
'''
  • 当迭代完毕后,未设置默认参数,系统将会抛出StopIteration异常,我们可以对其进行捕获
list = [1, 2, 3, 4, 5, 6]
list_iter = list.__iter__()

try:
    while True:
        print(next(list_iter), end=' ')
except StopIteration as iter_error:
    print('\n', type(iter_error))

'''
运行结果:
    1 2 3 4 5 6
    <class 'StopIteration'>
'''

迭代器是一种Lasy Load的模式,只有在调用时才生成值,没有调用的时候就等待下一次调用。它是一种惰性计算

惰性计算又称为惰性求值(Lazy Evaluation),是一个计算机编程中的概念,它的目的是要最小化计算机要做的工作,尽可能延迟表达式求值。延迟求值特别用于函数式编程语言中。在使用延迟求值的时候,表达式不在它被绑定到变量之后就立即求值,而是在该值被取用的时候求值。惰性计算的最重要的好处是它可以构造一个无限的数据类型。

具有惰性计算特点的序列称为惰性序列,Python 中的迭代器就是一个惰性序列,调用 iter() 返回一个 iterator 并赋值给一个变量后不会立即进行求值,而是当你用到其中某些元素的时候才去求某元素的值。

惰性计算还可以在大规模数据处理中平滑处理时间,提高内存使用率。当处理大规模数据时,一次性进行处理往往是不方便的。

很多语言对惰性计算提供了支持,比如 Java,Scala,当然 Python 也不例外,它是函数式编程语言的一大特点。

迭代器转换方法的哨兵迭代

所谓哨兵实质上是自定义了第二个参数,用来标志迭代结束位

value = 0

def iter_ex():
    global value
    value += 1
    return value

my_iter = iter(iter_ex, 5)
for x in my_iter:
    print(x, end=' ')
'''
运行结果: 1 2 3 4
'''

何为生成器

要了解生成器,我们要从内存的使用方面去解释,我们使用getsizeof来获取不同的容器对象的内存:

import sys

list_one = [x for x in range(1, 5)]
print(sys.getsizeof(list_one))

list_two = [x for x in range(1, 6)]
print(sys.getsizeof(list_two))
'''
运行结果:
    88
    120
'''

从上述代码中可见,当容器中的对象数量变多时,内存占用也会逐渐增加,如果存在一个极其巨大的容器,超出了内存容量,我们又该如何处理?,这时候就需要我们的生成器了:

如果我们要处理更多元素,那么所占内存就呈线性增大,所以受到内存限制,列表容量是有限的。通常我们并不会一次处理所有元素,而只是集中在其中的某些相邻的元素上。所以如果列表元素可以用某种算法用已知量推导出来,就不必一次创建所有的元素。这种边循环边计算的机制,称为生成器(generator),生成器是用时间换空间的典型实例。

生成器通常由两种方式生成,用小括号()表示的生成器表达式(generator expression)和生成器函数(generator function)。

生成器定义与迭代器的关系

在 Python 中,使用了 yield 的函数被称为生成器(generator)。yield 是一个关键字,用于定义生成器函数,生成器函数是一种特殊的函数,可以在迭代过程中逐步产生值,而不是一次性返回所有结果。

跟普通函数不同的是,生成器是一个返回迭代器的函数,只能用于迭代操作,更简单点理解生成器就是一个迭代器。当在生成器函数中使用 yield 语句时,函数的执行将会暂停,并将 yield 后面的表达式作为当前迭代的值返回。

然后,每次调用生成器的 next() 方法或使用 for 循环进行迭代时,函数会从上次暂停的地方继续执行,直到再次遇到 yield 语句。这样,生成器函数可以逐步产生值,而不需要一次性计算并返回所有结果。

调用一个生成器函数,返回的是一个生成器对象,而生成器对象自动实现了迭代器协议,所以我们才能够使用访问迭代器的方法来访问生成器,你也可以说生成器返回的是一个特殊的迭代器

生成器表达式(元组推导式)

tuple_one = ( x for x in range(1,13) )

print(tuple(tuple_one), "\n", type(tuple_one))

'''
运行结果:
    (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12) 
    <class 'generator'>
'''

对于一个生成器,它仅仅保留了产生对象的算法,如果我们需要获取生成中的值,我们一是像上面一样进行强制转换为其他对象,再者就是通过next方法来进行生成下一个值, 即使用生成器,函数不用一次性生成所有的元素,只需在每次调用next的时候生成元素,这样更节省内存和CPU,如以下示例:

tuple_one = (x for x in range(1, 13))

while True:
    element = next(tuple_one, 'Done')
    if element == 'Done':
        break
    print(element, end=' ')
'''
运行结果:   1 2 3 4 5 6 7 8 9 10 11 12
'''

自定义惰性计算生成器

类似于返回值的生成器,在这个地方相当于一个return,只不过它可以再次被唤起罢了

生成器函数在执行完毕之后会返回一个值然后退出,然后生成器函数会自动挂起,当再次调用时将会从挂起位置启动(yield位置),他会利用yield关键字关起函数,给调用者返回一个值,同时保留了当前的足够多的状态,可以使函数继续执行,生成器和迭代协议是密切相关的,**迭代器都有一个__next__()成员方法,**这个方法要么返回迭代的下一项,要么抛出StopIteration异常

def fib_generator(max_size):
    n, a, b = 0, 0, 1
    while n < max_size:
        yield b
        a, b = b, a + b
        n = n + 1

fib_sequence = fib_generator(10)

while fib_sequence is not None:
    try:
        print(next(fib_sequence), end=' ')
    except StopIteration:
        break

这里还有一种方式,即检测它是否为可迭代对象来允许其迭代:

from collections.abc import Iterable

def fib_generator(max_size):
    n, a, b = 0, 0, 1
    while n < max_size:
        yield b
        a, b = b, a + b
        n = n + 1

fib_sequence = fib_generator(10)

while isinstance(fib_sequence, Iterable):
    try:
        print(next(fib_sequence), end=' ')
    except StopIteration:
        break

注意:还存在有一种存在返回值的特殊情况:

def generator_with_return():
    yield 1
    yield 2
    yield 3
    return "finished"

if __name__ == "__main__":
    gen = generator_with_return()
    print(next(gen))  # 输出: 1
    print(next(gen))  # 输出: 2
    print(next(gen))  # 输出: 3

    try:
        print(next(gen))
    except StopIteration as e:
        print("Generator returned:", e.value)

这样的话,在迭代结束后,将会把最后的值"finished"字符串封装到StopIteration异常中,可以通过value属性进行获取

注意,请不要使用迭代器循环,因为迭代器循环会将生成器中的值耗尽,后续导致出现StopIteration异常时,value属性无法获取到值

自定义协程交互生成器

在早期Python中,send() 方法通常用于在生成器(Generator)和协程(Coroutine)之间进行通信。这个方法允许在生成器或协程中暂停的同时发送数据,然后在生成器或协程中接收这些数据并继续执行。

在生成器中,send() 方法的工作原理如下:

  1. 第一次调用生成器的 send() 方法时,必须使用 None 作为参数。这是因为在开始执行生成器之前,没有可供生成器接收的数据。

  2. 之后,通过 send() 方法发送的任何数据都将成为生成器中相应 yield 表达式的值。生成器将在这里暂停,等待下一次调用 send()next()

下面是一个简单的示例,演示了 send() 方法在生成器中的使用:

def simple_generator():
    print("Generator started")
    x = yield  # 第一次调用 send() 时暂停在这里
    print("Received:", x)

# 创建生成器对象
gen = simple_generator()

# 启动生成器,执行到第一个 yield 表达式
next(gen)  # 输出 "Generator started"

# 向生成器发送数据,并继续执行生成器
gen.send(10)  # 输出 "Received: 10"

# 关闭生成器
gen.close()

在协程中,send() 方法的使用类似于生成器,但有一些差异。协程通常用于异步编程,而生成器则用于同步代码中。在协程中,send() 除了发送数据之外,还可以用于启动协程的执行,下面是一个简单的协程示例:

async def simple_coroutine():
    print("Coroutine started")
    x = await asyncio.sleep(1)  # 模拟异步操作
    print("Coroutine resumed, slept for 1 second")

# 创建协程对象
coro = simple_coroutine()

# 启动协程,执行到第一个 await 表达式
await coro  # 输出 "Coroutine started" 和 "Coroutine resumed, slept for 1 second"

在协程中,await 关键字用于暂停协程的执行,等待异步操作完成。在这里,await asyncio.sleep(1) 模拟了一个异步操作。协程在这里暂停,等待异步操作完成,然后继续执行。

需要注意的是,send() 方法和 await 关键字通常用于不同的上下文中。在生成器中使用 send(),而在协程中使用 await

send()和next()的区别就在于send可传递参数给yield表达式,这时候传递的参数就会作为yield表达式的值,而yield的参数是返回给调用者的值,也就是说send可以强行修改上一个yield表达式值。

send()和next()都有返回值,他们的返回值是当前迭代遇到的yield的时候,yield后面表达式的值,其实就是当前迭代yield后面的参数。

第一次调用时候必须先next()或send(),否则会报错,send后之所以为None是因为这时候没有上一个yield,所以也可以认为next()等同于send(None)

from:https://www.cnblogs.com/wj-1314/p/8490822.html

协程概念

协程(Coroutines)是一种并发编程的概念,通常用于异步编程。协程是一种轻量级的线程,它允许在代码中暂停执行,等待某些事件发生,然后恢复执行。在Python中,协程是通过 asyncawait 关键字实现的。

协程具有以下几个特点:

  1. 异步性质: 协程通常用于异步编程,允许在等待某些操作完成时继续执行其他任务,而不会阻塞整个程序。

  2. 轻量级: 协程是轻量级的,相比于线程和进程,创建和切换协程的开销较小。

  3. 非抢占式: 协程是协作式的,不像线程那样是抢占式的。一个协程需要主动释放控制权,让其他协程执行。

  4. 使用asyncawait 在Python中,通过使用 async def 定义协程函数,而在协程内使用 await 关键字来等待异步操作完成。

下面是一个简单的协程示例:

import asyncio

async def my_coroutine():
    print("Start Coroutine")
    await asyncio.sleep(2)
    print("Coroutine resumed after 2 seconds")

# 创建协程对象
coro = my_coroutine()

# 创建事件循环并运行协程
loop = asyncio.get_event_loop()
loop.run_until_complete(coro)

在这个示例中,my_coroutine 是一个简单的协程函数,它使用 await asyncio.sleep(2) 模拟一个异步操作。通过调用 loop.run_until_complete(coro),事件循环会运行协程,协程会在 await asyncio.sleep(2) 处暂停,然后在2秒后继续执行。

协程在异步编程中广泛用于处理大量的并发任务,例如网络请求、IO操作等,而不需要使用多线程或多进程的复杂性。asyncio 模块是Python中用于支持异步编程的标准库,它提供了协程和事件循环的实现。

Python的协议特性-接口实现

由于提到了接口实现,所以这里插播一条知识点

在Python中,协议是一种约定或接口,不同于传统的面向对象编程中的继承机制。协议是一种轻量级的、基于约定而非强制的方式,用于描述对象应该具有的方法、属性或行为。通过遵循协议,对象可以与其他对象进行交互,而无需继承相同的基类。

不过我们也可以定义抽象基类以实现类似接口的功能,以下是在 Python 中定义接口的常见方法:

抽象基类

Python 提供了 abc 模块,用于创建抽象基类,从而定义接口。具体的类可以继承这些抽象基类,并实现相应的方法。

from abc import ABC, abstractmethod

class MyInterface(ABC):
    @abstractmethod
    def method1(self):
        pass

    @abstractmethod
    def method2(self):
        pass

class MyClass(MyInterface):
    def method1(self):
        print("Implementation of method1")

    def method2(self):
        print("Implementation of method2")

obj = MyClass()
obj.method1()
obj.method2()

鸭子类型

在 Python 中,重要的是对象的行为,而不是其类型。如果一个对象像鸭子一样走路、游泳、嘎嘎叫,那么它就是鸭子。这意味着你可以通过对象的行为来判断它是否符合某个接口。

class Duck:
    def quack(self):
        print("Quack")

    def walk(self):
        print("Walk")

class Dog:
    def bark(self):
        print("Bark")

    def walk(self):
        print("Walk")

def animal_walk(animal):
    animal.walk()

duck = Duck()
dog = Dog()

animal_walk(duck)  # 输出: Walk
animal_walk(dog)   # 输出: Walk

注释和文档

可以通过文档字符串(docstring)或注释来明确一个类或对象的接口,让其他开发者能够了解如何使用它。

class MyInterface:
    def method1(self):
        """Documentation for method1."""
        pass

    def method2(self):
        """Documentation for method2."""
        pass

这些方法并非强制性的,而是 Python 社区中常用的一些方式,具体的选择取决于你的项目需求和团队的约定。在实际应用中,鸭子类型和文档通常能够满足大多数情况。如果需要更强的约束,抽象基类是一个更强大的工具。

常见的协议

这些协议提供了一种灵活的方式,使得对象可以按照特定的方式与其他对象进行交互,而无需显式地继承相同的基类。这符合Python的鸭子类型思想,即“如果它走起路来像鸭子,叫起来像鸭子,那么它就是鸭子”。

  1. 可迭代协议(Iterable Protocol)

    • 对象可以通过实现 __iter__ 方法来支持迭代。
    • 可迭代对象可以被 for 循环迭代,例如列表、元组、集合等。
    class MyIterable:
        def __iter__(self):
            # 实现迭代逻辑
            pass
    
  2. 迭代器协议(Iterator Protocol)

    • 实现了 __iter____next__ 方法的对象是迭代器。
    • 迭代器用于逐个返回序列中的元素。
    class MyIterator:
        def __iter__(self):
            return self
    
        def __next__(self):
            # 返回下一个元素
            pass
    
  3. 上下文管理器协议(Context Manager Protocol)

    • 通过实现 __enter____exit__ 方法,对象可以用于管理资源的获取和释放。
    class MyContextManager:
        def __enter__(self):
            # 获取资源
            pass
    
        def __exit__(self, exc_type, exc_value, traceback):
            # 释放资源
            pass
    
  4. 可哈希性协议(Hashable Protocol)

    • 对象需要实现 __hash____eq__ 方法,以便可以用作字典的键或集合的元素。
    class MyHashable:
        def __hash__(self):
            # 返回哈希值
    
        def __eq__(self, other):
            # 比较对象是否相等
    
  5. 序列协议(Sequence Protocol)

    • 通过实现一组特定的方法(如 __len____getitem__),对象可以表现得像序列一样,支持索引和切片。
    class MySequence:
        def __len__(self):
            # 返回序列长度
    
        def __getitem__(self, index):
            # 返回指定索引的元素
    
  6. 动态属性协议(Dynamic Attribute Protocol)

    • 通过实现 __getattr____setattr__ 方法,对象可以处理对不存在属性的访问或属性的设置。
    class MyDynamicAttributes:
        def __getattr__(self, name):
            # 处理对不存在属性的访问
    
        def __setattr__(self, name, value):
            # 处理属性的设置
    

实现一个迭代器协议

class MyIterable:
    def __init__(self, data):
        self.data = data

    def __iter__(self):
        self.index = 0
        return self

    def __next__(self):
        if self.index < len(self.data):
            result = self.data[self.index]
            self.index += 1
            return result
        else:
            raise StopIteration

# 测试可迭代协议
my_iterable_instance = MyIterable([1, 2, 3, 4, 5])

if isinstance(my_iterable_instance, MyIterable):
    for item in my_iterable_instance:
        print(item)

实现一个生成器类

同样的,我们也可以依据迭代器,将上面的斐波那契数列数列生成器更改为一个简单的生成器类:

class FibGenerator:
    def __init__(self):
        self.fib_size = 10

    def __iter__(self):
        n, a, b, value = 0, 0, 1, 0
        while value < self.fib_size:
            yield b
            a, b = b, a+b
            value += 1


fib_object = FibGenerator()

for fib_element in fib_object:
    print(fib_element, end="  ")

检测可迭代对象的争议

isinstance方法检测类型使用

isinstance() 是一个内建函数,用于检查一个对象是否是指定类(或元组中的其中一个类)的实例。其基本语法如下:

isinstance(object, classinfo)
  • object:要检查的对象。
  • classinfo:类名或由类对象组成的元组(如果对象的类型是元组中的任意一个类,则返回 True)。

以下是一些示例:

  1. 检查对象是否是特定类的实例:

    x = 5
    result = isinstance(x, int)
    print(result)  # 输出: True
    
  2. 检查对象是否是多个类中的一个:

    x = 5
    result = isinstance(x, (int, float, str))
    print(result)  # 输出: True
    
  3. 用于类继承关系:

    class Animal:
        pass
    
    class Dog(Animal):
        pass
    
    my_dog = Dog()
    result = isinstance(my_dog, Animal)
    print(result)  # 输出: True
    
  4. type() 的比较:

    x = 5
    result_type = type(x) is int
    result_instanceof = isinstance(x, int)
    print(result_type)       # 输出: True
    print(result_instanceof) # 输出: True
    

    注意:虽然 type()isinstance() 在某些情况下可以互换使用,但它们的目的略有不同。type() 主要用于获取对象的类型,而 isinstance() 主要用于检查对象的类型。

isinstance检测可迭代对象的机制

使用 isinstance(obj, Iterable) 是一种通用的方法来检测对象是否是可迭代的,它本质上是检测当前对象是否实现了collections.abc.Iterable接口。虽然大多数可迭代对象都会被正确地检测,但有时某些对象可能实现了迭代协议,但并没有显式地继承自 collections.abc.Iterable。因此,这种方法并不是绝对准确的。

一个更准确的方法是使用 iter(obj) 函数并捕获 TypeError 异常。如果对象是可迭代的,iter(obj) 将成功;否则,将抛出 TypeError 异常。这种方法更为严格,因为它确保对象实际上支持迭代协议。

isinstance检测可迭代对象的争议

检查一个对象是否是可迭代的(即能够在迭代中产生值),应该检查它是否实现了 __iter__() 方法,而不是直接使用 collections.abc 模块中的 Iterator 类。

我们给出示例,实现了迭代器,但是检测出错:

from collections.abc import Iterable

class MyIterable:
    def __init__(self, data):
        self.data = data

    def __getitem__(self, index):
        if 0 <= index < len(self.data):
            return self.data[index]
        else:
            raise IndexError("Index out of range")

# 创建一个实例
custom_iter = MyIterable([1, 2, 3, 4, 5])

for x  in custom_iter:
    print(x, end=' ')

try:
    iter(custom_iter)
    print("\nit is iterable")
except TypeError:
    print("\nit is not iterable")

# 使用 isinstance 检测
print(isinstance(custom_iter, Iterable))

给出Python3.12运行结果:

1 2 3 4 5 
it is iterable
False
PS D:\CodeProjects\VScodeProjects\py_test> 

检测出错原因:

▍序列可迭代的原因:iter函数
我们都知道序列是可迭代的。当解释器需要迭代对象x时,会自动调用iter(x)
内置的iter函数有以下作用:

  • 检查对象是否实现了__iter__方法,如果实现了就调用它,获得一个迭代器。
  • 如果没有实现__iter__方法,但是实现了__getitem__方法,python会创建一个迭代器,尝试按顺序(从索引0开始)获取元素。
  • 如果尝试失败,python会抛出TypeError异常,通常会提示"C object is not iterable",其中C是目标对象所属的类。

截止到Python3.6,基本上所有的Python序列也都实现了__getitem__方法,这是保证任何序列都可迭代的原因。当然标准的序列也都实现了__iter__方法,之所以对__getitem__也可以创建迭代器是为了向后兼容,未来可能不在这么做。

但是,从Python3.4开始,检查x能否迭代,最准确的方法是调用iter(x)函数,如果不可迭代,再处理TypeError异常。

from:https://zhuanlan.zhihu.com/p/82787357

引发isinstance检测出错机制的类型

可迭代的不一定就是可迭代对象(Iterable)。能判断Iterable的唯一条件是看内置函数或者自定义类型中使用了__iter__方法。
如果我自定义类型中有getitemlen方法(注意,len方法并不是必须的),那么就可以进行迭代。但用isinstance函数判断是否为Iterable时,会显示False

为何可迭代对象不是迭代器

你可能会问,为什么listdictstr等数据类型不是Iterator

这是因为Python的Iterator对象表示的是一个数据流,Iterator对象可以被next()函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration错误。可以把这个数据流看做是一个有序序列,但我们却不能提前知道序列的长度,只能不断通过next()函数实现按需计算下一个数据,所以Iterator的计算是惰性的,只有在需要返回下一个数据时它才会计算。

Iterator甚至可以表示一个无限大的数据流,例如全体自然数。而使用list是永远不可能存储全体自然数的。

我们思考一下上面的说法是否正确:

从C的流和Python的迭代器行为来看,Python的迭代器是一个数据流,它们满足了流的特征:

在 Python 中,迭代器(iterator)的设计和使用模式与数据流(data stream)有一些相似之处:

  1. 逐个提供元素: 迭代器的主要目的是逐个提供序列中的元素。在很多情况下,这个序列可以是一个数据流,其中元素是按顺序产生或获取的。

  2. 实时获取元素: 迭代器通常支持实时获取元素的操作,而不需要提前加载整个数据集。这种逐步获取元素的方式类似于数据流的特性,它允许在数据产生的同时进行处理。

  3. 无限序列: 迭代器可以表示无限序列,例如 itertools.count() 返回一个无限递增的整数序列。这种情况下,迭代器就像是从一个无限的数据流中获取元素。

  4. 惰性计算: 迭代器通常是惰性计算的,只有在需要时才会计算或获取下一个元素。这种惰性计算的方式与数据流处理一致,只在需要时进行操作,而不是一次性处理所有数据。

  5. 文件迭代: 当迭代器用于遍历文件中的行时,它实际上是按行逐步提供数据,这也符合数据流的概念。

底层源码审计-讨论迭代器与可迭代对象的区别

我们来看next源码:

typedef struct {
    PyObject_HEAD
    Py_ssize_t it_index;
    PyObject *it_seq; /* Set to NULL when iterator is exhausted */
} seqiterobject;
static PyObject *
iter_iternext(PyObject *iterator)
{
    seqiterobject *it;
    PyObject *seq;
    PyObject *result;

    assert(PySeqIter_Check(iterator));
    it = (seqiterobject *)iterator;
    seq = it->it_seq;
    if (seq == NULL)
        return NULL;
    if (it->it_index == PY_SSIZE_T_MAX) {
        PyErr_SetString(PyExc_OverflowError,
                        "iter index too large");
        return NULL;
    }

    result = PySequence_GetItem(seq, it->it_index);
    if (result != NULL) {
        it->it_index++;
        return result;
    }
    if (PyErr_ExceptionMatches(PyExc_IndexError) ||
        PyErr_ExceptionMatches(PyExc_StopIteration))
    {
        PyErr_Clear();
        it->it_seq = NULL;
        Py_DECREF(seq);
    }
    return NULL;
}

可见底层是C语言返回了一个PyObject *指针result,作为本次位置索引,然后使得当前迭代器对象的index指向了下一个位置,无论NULL与否(因为写有判断),那么我们应该重点关注 PySequence_GetItem对于本次迭代的操作:

PyObject *
PySequence_GetItem(PyObject *s, Py_ssize_t i)
{
    if (s == NULL) {
        return null_error();
    }

    PySequenceMethods *m = Py_TYPE(s)->tp_as_sequence;
    if (m && m->sq_item) {
        if (i < 0) {
            if (m->sq_length) {
                Py_ssize_t l = (*m->sq_length)(s);
                assert(_Py_CheckSlotResult(s, "__len__", l >= 0));
                if (l < 0) {
                    return NULL;
                }
                i += l;
            }
        }
        PyObject *res = m->sq_item(s, i);
        assert(_Py_CheckSlotResult(s, "__getitem__", res != NULL));
        return res;
    }

    if (Py_TYPE(s)->tp_as_mapping && Py_TYPE(s)->tp_as_mapping->mp_subscript) {
        return type_error("%.200s is not a sequence", s);
    }
    return type_error("'%.200s' object does not support indexing", s);
}

可见在经过越界检测后,res指针实质上是查找表的索引指针,内含有当前迭代状态迭代器的地址,至此迭代过程完毕

  • 解释器的退化功能

在上述的PySequence_GetItem源码中我们注意到:如果类型对象内部没有定义 iter,那么解释器会退而求其次检测内部是否定义了 getitemassert(_Py_CheckSlotResult(s, "__getitem__", res != NULL));)这便是解释器的退化功能,其isinstance的检测出错也是来源于此

我们来观察list这类定义实现:

typedef struct {
    PyObject_VAR_HEAD
    /* Vector of pointers to list elements.  list[0] is ob_item[0], etc. */
    PyObject **ob_item;

    /* ob_item contains space for 'allocated' elements.  The number
     * currently in use is ob_size.
     * Invariants:
     *     0 <= ob_size <= allocated
     *     len(list) == ob_size
     *     ob_item == NULL implies ob_size == allocated == 0
     * list.sort() temporarily sets allocated to -1 to detect mutations.
     *
     * Items must normally not be NULL, except during construction when
     * the list is not yet visible outside the function that builds it.
     */
    Py_ssize_t allocated;
} PyListObject;

列表在创建时总是会申请一块内存,allocated 记录了这个列表申请的内存大小, ob_size 则是记录了当前列表已经使用内存的大小

我们以列表插入来观察其在内存中的状态:

static PyObject *
list_insert(PyListObject *self, PyObject *const *args, Py_ssize_t nargs)
{
    PyObject *return_value = NULL;
    Py_ssize_t index;
    PyObject *object;

    if (!_PyArg_CheckPositional("insert", nargs, 2, 2)) {
        goto exit;
    }
    {
        Py_ssize_t ival = -1;
        PyObject *iobj = _PyNumber_Index(args[0]);
        if (iobj != NULL) {
            ival = PyLong_AsSsize_t(iobj);
            Py_DECREF(iobj);
        }
        if (ival == -1 && PyErr_Occurred()) {
            goto exit;
        }
        index = ival;
    }
    object = args[1];
    return_value = list_insert_impl(self, index, object);

exit:
    return return_value;
}

注意到调用链list_insert_impl

static PyObject *
list_insert_impl(PyListObject *self, Py_ssize_t index, PyObject *object)
/*[clinic end generated code: output=7f35e32f60c8cb78 input=858514cf894c7eab]*/
{
    if (ins1(self, index, object) == 0)
        Py_RETURN_NONE;
    return NULL;
}

继续寻找ins1

static int
ins1(PyListObject *self, Py_ssize_t where, PyObject *v)
{
    Py_ssize_t i, n = Py_SIZE(self);
    PyObject **items;
    if (v == NULL) {
        PyErr_BadInternalCall();
        return -1;
    }

    assert((size_t)n + 1 < PY_SSIZE_T_MAX);
    if (list_resize(self, n+1) < 0)
        return -1;

    if (where < 0) {
        where += n;
        if (where < 0)
            where = 0;
    }
    if (where > n)
        where = n;
    items = self->ob_item;
    for (i = n; --i >= where; ) // 将插入位置以后的元素逐一往后移动一个位置。即这里的for循环是从后往前迭代
        items[i+1] = items[i];
    items[where] = Py_NewRef(v);
    return 0;
}

ins1() 首先对变长数组的当前空间进行越界检查,根据需要 resize 变长数组的大小。然后计算实际的插入位置 之后,ins1()obj_itemwhere 后边的元素逐个后移,将新元素 v (是一个指向待插入对象的指针)保存到 where 位置。同时将 v 的引用计数加 1,自始自终python操作的都是整个列表对象,自此列表插入实现结束

itertools库拓展

itertools 是 Python 标准库中的一个模块,提供了一组用于构建迭代器的高效工具。这个模块包含的函数可以帮助你处理迭代器、循环和组合,使得处理大型数据集或生成复杂序列变得更加方便。itertools库中的函数主要分为三类,分别为无限迭代器,有限迭代器,组合迭代器。

官方地址:https://docs.python.org/3.10/library/itertools.html

无限迭代器

  • itertools.count(start=0, step=1)

创建一个从 start 开始,以 step 为步长的无限计数器。例如,count(5) 会生成 5, 6, 7, 8, … 的无限序列。

from itertools import count

for i in count(1, 2):
    print(i)
  • itertools.cycle(iterable)

创建一个无限循环的迭代器,不断重复提供的可迭代对象。

from itertools import cycle

for item in cycle(['a', 'b', 'c']):
    print(item)
  • itertools.repeat(element, times=None)

重复生成一个元素 element,如果指定了 times 参数,则重复指定的次数。

from itertools import repeat

for i in repeat('Hello', 3):
    print(i)

有限迭代器

  • itertools.chain(iterable1, iterable2, ...)

将多个可迭代对象连接成一个迭代器。

from itertools import chain

for i in chain([1, 2, 3], ['a', 'b', 'c']):
    print(i)
  • itertools.groupby(iterable, key=None)

用于根据指定的 key 函数对可迭代对象中的元素进行分组(如果key函数为None,则只有相同的元素才能放在一组。)。返回的结果是一个迭代器,每个元素都是由 key 分组的元素列表。在使用 groupby 之前,通常需要先对可迭代对象进行排序,以确保相同的元素在一起。

from itertools import groupby

data = [('a', 1), ('b', 2), ('b', 3), ('a', 4), ('c', 5)]

# 需要先对数据进行排序,以确保相同的元素在一起
sorted_data = sorted(data, key=lambda x: x[0])

# 使用 groupby 对数据进行分组
grouped_data = groupby(sorted_data, key=lambda x: x[0])

# 打印分组的结果
for key, group in grouped_data:
    print(f"Key: {key}, Group: {list(group)}")
  • itertools.accumulate(iterable, func=None)

函数用于对可迭代对象进行累积计算。它返回一个迭代器,生成由给定函数 func(如果提供)计算的累积值:

from itertools import accumulate

data = [1, 2, 3, 4, 5]

# 使用 accumulate 对列表元素进行累积加法
result = accumulate(data)

# 打印累积结果
for value in result:
    print(value)

定义外部的func:

from itertools import accumulate

# 定义自定义函数
def custom_accumulate(x, y):
    return x * y

data = [1, 2, 3, 4, 5]

# 使用外部定义的函数进行累积计算
result = accumulate(data, func=custom_accumulate)

# 打印累积结果
for value in result:
    print(value)

还有一种比较骚的用法,可以将函数作为 func 参数传递给 accumulate (lambda表达式)

from itertools import accumulate

data = [1, 2, 3, 4, 5]

# 使用 accumulate 对列表元素进行累积乘法
result = accumulate(data, func=lambda x, y: x * y)

# 打印累积结果
for value in result:
    print(value)

组合迭代器

  • itertools.combinations(iterable, r)

返回可迭代对象中所有长度为 r 的组合。

from itertools import combinations

for combo in combinations('ABCD', 2):
    print(combo)
  • itertools.permutations(iterable, r=None)

返回可迭代对象中所有长度为 r(默认为可迭代对象的长度)的排列。

from itertools import permutations

for perm in permutations('ABCD', 2):
    print(perm)
  • itertools.product(iterable1, iterable2, ...)

返回可迭代对象的笛卡尔积。

from itertools import product

for prod in product('AB', repeat=2):
    print(prod)
  • itertools.islice(iterable, start, stop, step=1)

从可迭代对象中截取指定范围的元素。

from itertools import islice

data = range(10)
sliced_data = islice(data, 2, 8, 2)
print(list(sliced_data))

附表

附表均来自官方文档

Infinite iterators:

IteratorArgumentsResultsExample
count()start, [step]start, start+step, start+2*step, …count(10) --> 10 11 12 13 14 ...
cycle()pp0, p1, … plast, p0, p1, …cycle('ABCD') --> A B C D A B C D ...
repeat()elem [,n]elem, elem, elem, … endlessly or up to n timesrepeat(10, 3) --> 10 10 10

Iterators terminating on the shortest input sequence:

IteratorArgumentsResultsExample
accumulate()p [,func]p0, p0+p1, p0+p1+p2, …accumulate([1,2,3,4,5]) --> 1 3 6 10 15
chain()p, q, …p0, p1, … plast, q0, q1, …chain('ABC', 'DEF') --> A B C D E F
chain.from_iterable()iterablep0, p1, … plast, q0, q1, …chain.from_iterable(['ABC', 'DEF']) --> A B C D E F
compress()data, selectors(d[0] if s[0]), (d[1] if s[1]), …compress('ABCDEF', [1,0,1,0,1,1]) --> A C E F
dropwhile()pred, seqseq[n], seq[n+1], starting when pred failsdropwhile(lambda x: x<5, [1,4,6,4,1]) --> 6 4 1
filterfalse()pred, seqelements of seq where pred(elem) is falsefilterfalse(lambda x: x%2, range(10)) --> 0 2 4 6 8
groupby()iterable[, key]sub-iterators grouped by value of key(v)
islice()seq, [start,] stop [, step]elements from seq[start:stop:step]islice('ABCDEFG', 2, None) --> C D E F G
pairwise()iterable(p[0], p[1]), (p[1], p[2])pairwise('ABCDEFG') --> AB BC CD DE EF FG
starmap()func, seqfunc(*seq[0]), func(*seq[1]), …starmap(pow, [(2,5), (3,2), (10,3)]) --> 32 9 1000
takewhile()pred, seqseq[0], seq[1], until pred failstakewhile(lambda x: x<5, [1,4,6,4,1]) --> 1 4
tee()it, nit1, it2, … itn splits one iterator into n
zip_longest()p, q, …(p[0], q[0]), (p[1], q[1]), …zip_longest('ABCD', 'xy', fillvalue='-') --> Ax By C- D-

Combinatoric iterators:

IteratorArgumentsResults
product()p, q, … [repeat=1]cartesian product, equivalent to a nested for-loop
permutations()p[, r]r-length tuples, all possible orderings, no repeated elements
combinations()p, rr-length tuples, in sorted order, no repeated elements
combinations_with_replacement()p, rr-length tuples, in sorted order, with repeated elements
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值