Advance Python 08 :迭代器与生成器

本文深入探讨Python的迭代协议,介绍迭代器、可迭代对象和生成器的概念及使用。阐述了生成器作为迭代器的特性,如惰性计算和内存效率,以及在UserList和大文件读取中的应用。同时,详细解析了生成器函数的工作原理,展示了如何通过字节码理解其内部机制。
摘要由CSDN通过智能技术生成

Introduce

  • python中的迭代协议

  • 什么是迭代器和可迭代对象

  • 生成器函数的使用

  • python是如何实现生成器的

  • 生成器在UserList中的应用

  • 生成器如何读取大文件


本章节为后续学习IO和协程奠定基础

前言

在了解Python的数据结构时,容器(container)、可迭代对象(iterable)、迭代器(iterator)、生成器(generator)、列表/集合/字典推导式(list,set,dict comprehension)众多概念参杂在一起,难免让初学者一头雾水,我将用一篇文章试图将这些概念以及它们之间的关系捋清楚

在这里插入图片描述

Python中的迭代是指按照元素的顺序逐个调用的过程,迭代概念包括:迭代协议、可迭代对象和迭代器三个概念。

补充概念:

容器:

容器是一种把多个元素组织在一起的数据结构,容器中的元素可以逐个地迭代获取,可以用 in , not in 关键字判断元素是否包含在容器中。通常这类数据结构把所有的元素存储在内存中(也有一些特列并不是所有的元素都放在内存)在Python中,常见的容器对象有:

  • list, deque, …
  • set, frozensets, …
  • dict, defaultdict, OrderedDict, Counter, …
  • tuple, namedtuple, …
  • str

容器比较容易理解,因为你就可以把它看作是一个盒子、一栋房子、一个柜子,里面可以塞任何东西。从技术角度来说,当它可以用来询问某个元素是否包含在其中时,那么这个对象就可以认为是一个容器,比如 list,set,tuples都是容器对象。

尽管绝大多数容器都提供了某种方式来获取其中的每一个元素,但这并不是容器本身提供的能力,而是 可迭代对象 赋予了容器这种能力,当然并不是所有的容器都是可迭代的。

一、python中的迭代协议

1.1、什么是迭代协议?

  • Iterable
  • Iterator

1.2、迭代器是什么?

迭代器是访问集合内元素的一种方式,一般用来遍历数据。

1.3、为何要有迭代器?

对于序列类型:字符串、列表、元组,我们可以使用索引的方式迭代取出其包含的元素。但对于字典、集合、文件等类型是没有索引的,若还想取出其内部包含的元素,则必须找出一种不依赖于索引的迭代方式,这就是迭代器

迭代器和以下标访问方式不一样

  • 迭代器是不能返回的,迭代器提供了一种惰性访问数据的方式
  • []下标的方式,原理是__getitem__ ,例如list等可迭代的类型都实现了迭代协议(__iter__这个方法)

示例代码:

# 什么是迭代协议
# 迭代器是什么? 迭代器是访问集合内元素的一种方式, 一般用来遍历数据
# 迭代器和以下标的访问方式不一样, 迭代器是不能返回的, 迭代器提供了一种产生惰性数据的方式
# []下标的方式,原理是__getitem__ ,例如list等可迭代的类型都实现了迭代协议(__iter__这个方法)

from collections.abc import Iterable, Iterator

a = [1, 2]
iter_rator = iter(a)
print(isinstance(a, Iterable))  # 是一个可迭代对象
print(isinstance(iter_rator, Iterator))  # 但不是一个迭代器

True
False

python中能完成for循环,是背后的迭代器在起作用。

生成器背后也是迭代器,访问数据的时候才去获取数据,和list不同。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4UxisFoh-1644503856146)(C:\Users\pc\AppData\Roaming\Typora\typora-user-images\image-20220210173417195.png)]

__iter__方法是Iterable的一个重要的魔法函数,只要实现了__iter__方法,那么就是一个可迭代类型。

我们的重点是Iterator(迭代器)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R8fC21qe-1644503856146)(C:\Users\pc\AppData\Roaming\Typora\typora-user-images\image-20220210173438398.png)]

Iterator继承了Iterable这个抽象基类,并且加了一个抽象方法__next__(获取下一个元素必须要实现的方法)。所以Iterator有两个必须要实现的魔法函数,一个是Iterable里的__iter__(实现迭代协议),和__next__(返回下一个数据),从迭代器访问数据用的是__next__。

Iterator里已经重载了__iter__魔法函数,返回self。

list中只实现了__iter__,但没有__next__,所以它只是一个可迭代对象,而不是迭代器。

二、什么是迭代器和可迭代对象

2.1、概念

1、为何要有迭代器?

对于序列类型:字符串、列表、元组,我们可以使用索引的方式迭代取出其包含的元素。但对于字典、集合、文件等类型是没有索引的,若还想取出其内部包含的元素,则必须找出一种不依赖于索引的迭代方式,这就是迭代器

2、什么是可迭代对象?

可迭代对象指的是内置有__iter__方法的对象,即obj.iter,如下
‘hello’.iter
(1,2,3).iter
[1,2,3].iter
{‘a’:1}.iter
{‘a’,‘b’}.iter
open(‘a.txt’).iter

3、什么是迭代器对象?

可迭代对象执行obj.iter()得到的结果就是迭代器对象
而迭代器对象指的是即内置有__iter__又内置有__next__方法的对象

文件类型是迭代器对象
open(‘a.txt’).iter()
open(‘a.txt’).next()

4、注意

迭代器对象一定是可迭代对象,而可迭代对象不一定是迭代器对象

5、迭代器对象的使用

示例代码:

dic = {'a': 1, 'b': 2, 'c': 3}
iter_dic = dic.__iter__()  # 得到迭代器对象,迭代器对象即有__iter__又有__next__,但是:迭代器.__iter__()得到的仍然是迭代器本身
iter_dic.__iter__() is iter_dic  # True

print(iter_dic.__next__())  # 等同于next(iter_dic)
print(iter_dic.__next__())  # 等同于next(iter_dic)
print(iter_dic.__next__())  # 等同于next(iter_dic)
# print(iter_dic.__next__()) #抛出异常StopIteration,或者说结束标志

# 有了迭代器,我们就可以不依赖索引迭代取值了
iter_dic = dic.__iter__()
while 1:
    try:
        k = next(iter_dic)
        print(dic[k])
    except StopIteration:
        break

# 这么写太丑陋了,需要我们自己捕捉异常,控制next,python这么牛逼,能不能帮我解决呢?能,请看for循环

运行结果:

a
b
c
1
2
3

进程已结束,退出代码为 0

6、for循环

for循环的工作原理

1:执行in后对象的dic.iter()方法,得到一个迭代器对象iter_dic
2: 执行next(iter_dic),将得到的值赋值给k,然后执行循环体代码
3: 重复过程2,直到捕捉到异常StopIteration,结束循环

示例代码:

# 基于for循环,我们可以完全不再依赖索引去取值了
dic = {'a': 1, 'b': 2, 'c': 3}
for k in dic:
    print(dic[k])
    
1
2
3

进程已结束,退出代码为 0

7、迭代器的优缺点

优点:

  • 提供一种统一的、不依赖于索引的迭代方式
  • 惰性计算,节省内存

缺点:

  • 无法获取长度(只有在next完毕才知道到底有几个值)
  • 一次性的,只能往后走,不能往前退

2.2、深入迭代器和可迭代对象

示例代码:

from collections.abc import Iterable, Iterator
a = [1,2]
iter_rator = iter(a)
print (isinstance(a, Iterable))
print (isinstance(iter_rator, Iterator))
True
True

如果a实现了__iter__方法,再调用iter函数的话,就是返回一个迭代器。

之前我们讲过,实现了__geitem__方法,就可以进行for循环。

class Company(object):
    def __init__(self, employee_list):
        self.employee = employee_list

    def __getitem__(self, item):
        return self.employee[item]


if __name__ == "__main__":
    company = Company(["tom", "bob", "jane"])
    for item in company:
        print(item)
        
tom
bob
jane

在调用for循环的时候,会去尝试调用iter(company),虽然company里没有实现__iter__魔法函数,但是iter这个内置方法首先会去寻找是否有__iter__方法,如果没有则默认创建一个迭代器,这个迭代器会利用__geitem__方法进行遍历(从0开始遍历)–相当于利用__geitem__退化为迭代器。

我们不用iter()利用__geitem__方法这种方式定义迭代器。

我们在类里面自定义一个迭代器

实现 iter 时,必须返回 Iterator 对象

我们在类里面自定义一个迭代器

from collections.abc import Iterator


class MyIterator(Iterator):
    """
    实现了一个迭代器
    """
    def __init__(self, employee):
        self.employee = employee
        self.index = 0

    def __next__(self):
        # 真正返回迭代值的逻辑
        # 迭代器不支持切片,不会接收索引值,只能一步一步走
        # 遍历大文件
        try:
            word = self.employee[self.index]
        except IndexError:
            raise StopIteration
        self.index += 1
        return word


class Company:
    def __init__(self, employee):
        self.employee = employee

    # def __iter__(self):
    #     return 1        # TypeError: iter() returned non-iterator of type 'int'

    # def __iter__(self):
    #     return self     # TypeError: iter() returned non-iterator of type 'Company'

    # 使用内置方法 iter
    # def __iter__(self):
    #     return iter(self.employee)  # <iterator object at 0x000001F512B907C8>

    # 使用自定义 MyIterator ******
    def __iter__(self):
        return MyIterator(self.employee)    # <__main__.MyIterator object at 0x0000013462EF0848>

    def __getitem__(self, index):
        return self.employee[index]


if __name__ == '__main__':
    company = Company(['linda', 'alex', 'catherine'])
    my_iterator = iter(company)
    print(my_iterator)
    # for 循环首先查找 __iter__;如果没有自动生成一个__iter__,里面遍历__getitem__
    # for item in company:
    #     print(item)

    while True:
        try:
            print(next(my_iterator))
        except StopIteration:
            break

    """
    迭代器设计模式,不要在Company中实现 __next__ 方法,而要单独实现MyIterator实现,Company中__iter__调用MyIterator就行
    """

运行结果:

<__main__.MyIterator object at 0x0000015F45AEA460>
linda
alex
catherine

进程已结束,退出代码为 0
  • Comanpy实例化的对象为可迭代对象,可用for循环遍历数据,内部实现了__iter__方法,该方法返回迭代器
  • MyIterator实现化对象为迭代器,可用next()获取数值,内部实现了__iter__和__next__方法

大佬博客分享:
迭代器、生成器、面向过程编程 - linhaifeng - 博客园 (cnblogs.com)

三、生成器函数的使用

3.1、什么是生成器

1、概念

只要函数内部包含有yield关键字,那么函数名()的到的结果就是生成器,并且不会执行函数内部代码

2、生成器就是迭代器

示例代码:

# 只要函数内部包含有yield关键字,那么函数名()的到的结果就是生成器,并且不会执行函数内部代码

def func():
    print('====>first')
    yield 1
    print('====>second')
    yield 2
    print('====>third')
    yield 3
    print('====>end')


g = func()
print(g)  # <generator object func at 0x0000000002184360>

g.__iter__
g.__next__
# 所以生成器就是迭代器,因此可以这么取值
res = next(g)
print(res)
res = next(g)
print(res)
res = next(g)
print(res)

运行结果:

<generator object func at 0x000002608DE7F350>
====>first
1
====>second
2
====>third
3

进程已结束,退出代码为 0

3、yield总结

# 1、把函数做成迭代器
# 2、对比return,可以返回多次值,可以挂起/保存函数的运行状态

深的来说:

yield是python语法中非常精妙的设计

  • 1.yield关键词(生成器)为实现协程提供可能
  • 2.也为惰性求值(延迟求值)提供了可能

3.2、生成器函数的使用

先看一段示例代码:

def gen_func():
    yield 1  # 为什么有了yield就会变成生成器呢?因为python在运行前,会将代码变成字节码
    yield 2
    yield 3


# 惰性求值,延迟求值提供了可能性
# 斐波拉契函数 0 1 1 2 3 5 8 ...
def fib(index):
    if index <= 2:
        return 1
    else:
        return fib(index - 1) + fib(index - 2)


def func():
    return 1


if __name__ == '__main__':
    # 返回为生成器对象,python编译字节码的时候产生
    gen = gen_func()

    # 生成器对象也是实现了迭代协议的,可以for循环
    for value in gen:
        print(value)

    ret = func()

1
2
3

ret 就是一个返回的值,而gen是一个生成器对象,不再是普通的值。

3.3、小练习

  • 执行生成器函数得到生成器对象,可for循环取值
  • 生成器函数可以多次返回值,流程的变化
# 获取对应位置的值
def fib(index):
    if index <= 2:
        return 1
    else:
        return fib(index-1) + fib(index-2)


# 获取整个过程
def fib2(index):
    ret_list = []
    n, a, b = 0, 0, 1
    while n < index:
        ret_list.append(b)
        a, b = b, a + b
        n += 1
    return ret_list


# yield
def gen_fib(index):
    n, a, b = 0, 0, 1
    while n < index:
        yield b
        a, b = b, a + b
        n += 1


print(fib(10))
print(fib2(10))
for value in gen_fib(10):
    print(value)

斐波拉契 1 1 2 3 5 8 …

  • 根据位置获取对应值
  • 根据位置获取所有值

四、python是如何实现生成器的

  • 什么场景下运用生成器

  • 生成器内部实现原理

  • 生成器函数与普通函数区别

4.1、python中函数工作原理

标准的 Python 解释器是用 C 写的。

解释器用一个叫做 PyEval_EvalFrameEx 的 C 函数来执行 Python 函数。它接受一个 Python 的堆栈帧(stack frame)对象,并在这个堆栈帧的上下文中执行 Python 字节码。

1、什么是字节码?

import dis


def foo():
    bar()


def bar():
    pass


print(dis.dis(foo))

我们用dis模块可以查看python程序的字节码,下面是函数foo()的字节码:

  5           0 LOAD_GLOBAL              0 (bar)
              2 CALL_FUNCTION            0
              4 POP_TOP
              6 LOAD_CONST               0 (None)
              8 RETURN_VALUE
None

进程已结束,退出代码为 0
  • foo函数将bar加载到栈中并调用它,然后从栈中弹出返回值,最后加载并返回None;
  • 当PyEval_EvalFrameEx遇到CALL_FUNCTION字节码的时候,它会创建一个新的python栈帧,然后用这个新的帧作为参数递归调用PyEval_EvalFrameEx来执行bar。

Python 的堆栈帧是分配在堆内存中的,不过有一点要注意的是,python解释器是个普通的C程序,所以它的堆栈帧就是普通的堆栈。但是它操作的python堆栈帧是分配在堆上的,所以python的栈帧可以在它的调用之外存活。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-unqwI4me-1644503856147)(C:\Users\pc\AppData\Roaming\Typora\typora-user-images\image-20220210210415980.png)]

现在这项技术被用到了 Python 生成器(generator)上——使用代码对象和堆栈帧这些相同的组件来产生奇妙的效果。

2、python中函数的工作原理

python.exe会用一个叫做PyEval_EvalFrameEx(c函数)去执行foo函数 首先会创建一个栈帧(stack_frame)

import inspect

frame = None


def foo():
    bar()


def bar():
    global frame
    frame = inspect.currentframe()


# python.exe会用一个叫做 PyEval_EvalFramEx(c函数)去执行foo函数,
# 1.首先会创建一个栈帧(stack frame),在栈帧上下文运行字节码(字节码-即函数,全局唯一)
# 2.当foo调用子函数 bar, 又会创建一个栈帧,然后把函数控制权交给这个栈帧对象,运行bar的上下文字节码
# 3.所有的栈帧都是分配在堆内存上(不是栈的内存上),堆的特性是不释放就会一直在内存中,这就决定了栈帧可以
# 独立于调用者存在,就算调用者foo函数删了,bar栈帧还是在内存中,只要有对应的指针,就可以控制bar
"""
python一切皆对象,栈帧也是对象
"""

改写下上面的函数,看下栈帧的特性

import inspect

frame = None


def foo():
    bar()


def bar():
    global frame
    frame = inspect.currentframe()  # 把当前bar的栈帧赋值给全局变量


foo()  # 运行foo函数,完成后即退出该函数
print(frame.f_code.co_name)  # frame的栈帧
caller_frame = frame.f_back  # 调用者的栈帧
print(caller_frame.f_code.co_name)

bar
foo

所以调用foo的时候,产生一个栈帧,调用子程序的时候,又会创建一个栈帧

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JyGgFxkN-1644503856147)(C:\Users\pc\AppData\Roaming\Typora\typora-user-images\image-20220209005730110.png)]

cpython(python解释器)

1.通过PyEval_EvalFramEx函数创建一个栈帧对象

  • f_code指向PyCodeObject(foo的字节码)

2.foo里面调用了bar,又创建一个栈帧对象

  • f_back指向调用者的栈帧对象
  • f_code指向PyCodeObject(boo的字节码)

生成器对象利用了python栈帧对象是分配在堆内存中的这一特性。

生成器对象对python frame做了一个封装

(python中一切皆对象,栈帧对象中运行foo函数字节码对象 当foo调用子函数bar,又会创建一个栈帧对象,在此栈帧对象中运行bar函数字节码对象

所有的栈帧都是分配再堆内存上(不会自动释放),这就对定了栈帧可以独立于调用者存在;不用于静态语言的调用,静态语言是栈的形式,调用完就自动释放)

def gen_func():
    """
    python解释器会编译函数的字节码,遇到yield关键词,知道这是个生成器函数,会对这个函数做一个标记
    """
    yield 1
    name = "bobby"
    yield 2
    age = 30
    return "imooc"  # 生成器可以return一个值


gen = gen_func()  # 返回生成器对象,实际上对pyframe做了封装

4.2、python中生成器函数工作原理

def gen_func():
    address = 'China'
    yield 1
    name = 'linda'
    yield 2
    age = 20
    return 'done'


gen = gen_func()

import dis
print(dis.dis(gen))

print(gen.gi_frame.f_lasti)
print(gen.gi_frame.f_locals)

print('\nfirst value: %s' % next(gen))
print(gen.gi_frame.f_lasti)
print(gen.gi_frame.f_locals)

print('\nsecond value: %s' % next(gen))
print(gen.gi_frame.f_lasti)
print(gen.gi_frame.f_locals)

  2           0 LOAD_CONST               1 ('China')
              2 STORE_FAST               0 (address)

  3           4 LOAD_CONST               2 (1)
              6 YIELD_VALUE
              8 POP_TOP

  4          10 LOAD_CONST               3 ('linda')
             12 STORE_FAST               1 (name)

  5          14 LOAD_CONST               4 (2)
             16 YIELD_VALUE
             18 POP_TOP

  6          20 LOAD_CONST               5 (20)
             22 STORE_FAST               2 (age)

  7          24 LOAD_CONST               6 ('done')
             26 RETURN_VALUE
None
-1
{}

first value: 1
6
{'address': 'China'}

second value: 2
16
{'address': 'China', 'name': 'linda'}

进程已结束,退出代码为 0

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zYG1H9tP-1644503856148)(C:\Users\pc\AppData\Roaming\Typora\typora-user-images\image-20220209005749493.png)]

在PyFrameObject和PyCodeObject上面又加了一层PyGenObject(python中的生成器对象)。

PyFrameObject中的

  • f_lasti:指向最近执行的代码(在字节码中的什么位置)
  • f_locals:运行到下一个yield之前保存的局部变量
  • 为PyGenObject的暂停和继续提供了理论基础

生成器对象也是保存在堆内存中的,所以可以独立于调用者存在,只要有这个栈帧对象,就可以自由控制。

可以在任何地方,任何函数或模块中,只要拿到了生成器对象,就可以恢复、暂停、继续这个生成器对象。

正了有了这个特性,才有后面协程的概念,这是协程能够执行的理论基础。

有个f_lasti,f_locals,就可以在生成器里面不断地循环同一个函数。

  • 控制整个生成器函数暂定和继续前进 gen.gi_frame.f_lasti
  • 整个生成器函数作用域逐渐变化 gen.gi_frame.f_locals

五、生成器在UserList中的应用

通过对list的遍历来看生成器的具体应用,list可以用for循环进行遍历(list实现了__getitem__方法)。

为什么__getitem__可以进行遍历,来看一下一个数据结构UserList(list使用c语言写的,UserList是用python写的),UserList也可以用自己继承,不要去继承list(因为是c语言写的,里面的很多关键方法是不能被重写的。但是python提供了UserList,即python实现的list。)。

六、生成器如何读取大文件

场景:500G 文件 特殊只有一行,特殊分割符号 {|}

读取500G的文件,一行一行读取出来,写入到数据库中。

可以通过open方式,然后一行一行读,但这对文件本身有一定要求(是一行一行保存的)。

如果文件只有一行,中间用特殊分隔符分割,那就没法使用open方式。

#500G, 特殊 一行
def myreadlines(f, newline):
  buf = "" # 声明一个buf,缓存
  while True:
    while newline in buf: # 缓存中是否包含newline分隔符
      pos = buf.index(newline) # 如果存在则把分隔符位置找到
      yield buf[:pos]
      buf = buf[pos + len(newline):] # buf有可能会有多行,所以在取出一行后,要更新
    chunk = f.read(4096)# 刚开始buf为空字符串,直接读取f
    # f.read(4096) 只会读取4096个字符
    # f.read(4096) f会接着上一个偏移量进行数据读取,f内部会自己维护这个偏移量

    if not chunk:
      #说明已经读到了文件结尾
      yield buf
      break
    buf += chunk

with open("input.txt") as f:
    for line in myreadlines(f, "{|}"):
        print (line)
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

DLNovice

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值