第九章 迭代器和生成器

9.1 python的迭代协议

#什么是迭代协议
#迭代器是什么? 迭代器是访问集合内元素的一种方式, 一般用来遍历数据
#迭代器和以下标的访问方式不一样, 迭代器是不能返回的, 迭代器提供了一种产生惰性数据的方式
#[]下标的方式,原理是__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不同。

image-20200921162229962

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

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

image-20200921191842631

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

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

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

9.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__方法这种方式定义迭代器。

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

from collections.abc import Iterator
class MyIterator(Iterator):
    """
    实现了一个迭代器
    """
    def __init__(self, employee_list):
        self.iter_list = employee_list
        self.index = 0
    def __next__(self):
        # 真正返回迭代值的逻辑,不支持切片
        # 迭代器不会产生索引值,且是一个一个产生数据的,所以我们需要内部维护一个索引变量
        try:
            word =  self.iter_list[self.index] # 这里可能会抛出异常,随着index增加,会超出已有数据索引范围

        except IndexError: # list中的异常是索引异常
            raise StopIteration # 迭代器中的异常是停止迭代器

        self.index += 1
        return word

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

    def __iter__(self):
        return MyIterator(self.employee) # 返回自定义迭代器

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

迭代器是经典设计模式中的一种,要遍历一个数据对象的时候,必须要新建一个迭代器,在对象里返回迭代器,用迭代器去维护迭代所需的一些变量,不要去迭代对象内部维护。比如上面的self.index,或者不要在class Company内定义__next__。

9.3 生成器函数使用

生成器和普通函数定义是一样的

#生成器函数,函数里只要有yield关键字
def gen_func():
    yield 1 # 为什么有了yield就会变成生成器呢?因为python在运行前,会将代码变成字节码,
    yield 2	# 发现有yield就生成一个生成器对象
    yield 3 # yield是返回值,可以多次yield

def func():
    return 1
	# return 2 普通函数只能写一个return

if __name__ == "__main__":
    #生成器对象, 在python编译字节码的时候就产生了
    gen = gen_func() # 生成器对象也实现了迭代器协议,所以可以使用for循环
    re = func()
    pass # 在此处打断点debug

image-20200921203519637

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

#生成器函数,函数里只要有yield关键字
def gen_func():
    yield 1
    yield 2
    yield 3
    
if __name__ == "__main__":
    gen = gen_func()
    for value in gen:
        print (value)
1
2
3

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

1.yield关键词(生成器)为实现协程提供可能

2.也为惰性求值(延迟求值)提供了可能

9.4 生成器的原理

什么是字节码?

def foo():
    bar()

def bar():
    pass

import dis
print(dis.dis(foo))
  2           0 LOAD_GLOBAL              0 (bar)
              2 CALL_FUNCTION            0
              4 POP_TOP                                 # 从栈顶端把元素打印出来
              6 LOAD_CONST               0 (None)
              8 RETURN_VALUE
None

什么场景下考虑使用生成器?自己如何写生成器?如何和函数区别开来?

python.exe运行python脚本,python.exe是用c语言写的。

#1.python中函数的工作原理
"""

"""
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的时候,产生一个栈帧,调用子程序的时候,又会创建一个栈帧

image-20200921211452366

cpython(python解释器)

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

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

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

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

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

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

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

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

image-20200921220131967

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

PyFrameObject中的

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

生成器对象也是保存在堆内存中的,所以可以独立于调用者存在,只要有这个栈帧对象,就可以自由控制。可以在任何地方,任何函数或模块中,只要拿到了生成器对象,就可以恢复、暂停、继续这个生成器对象。正了有了这个特性,才有后面协程的概念,这是协程能够执行的理论基础。

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

9.5 通过UserList来看生成器的应用

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

为什么__getitem__可以进行遍历,来看一下一个数据结构UserList(list使用c语言写的,UserList是用python写的),UserList也可以用自己继承,不要去继承list(因为是c语言写的,里面有一些很关键的方法)。

image-20200922094231079

image-20200922094321324

UserList继承了MutableSequence,MutableSequence继承了Sequence

image-20200922094350974

Sequence里的__iter__内部就是用yield实现的

  • while True在函数里不停的循环取数据(循环同一个方法__getitem__)
  • i是一个局部变量且保存在生成器栈帧中,记录了元素位置
  • 内部的self[i]调用了__getitem__方法,相当于__getitem__退化为了一个迭代器

所以通过生成器的方式,实现了对list的遍历

9.6 生成器如何读取大文件

读取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)
posted @ 2020-09-22 10:12  yueqiudian  阅读(240)  评论(0编辑  收藏  举报