python的迭代协议

引言

  • 迭代器是访问集合内部元素的一种方式,一般用来遍历数据
  • 迭代器和用下标索引访问的方式不一样,迭代器是不能直接返回值的。
  • 迭代器提供了一种惰性访问数据的方式,需要的时候才产生数据。
  • 可迭代类型都实现了迭代协议,实际上就是__iter__()这个魔法函数。

    可迭代类型和迭代器

    前面讲过,collections.abc模块中定义了很多内置的抽象基类,现在我们重点关注其中的两个:IterableIterator

    Iterable

    迭代器和生成器 - 图1
    里面定义了一个抽象方法,__iter__(),也就是说某个类只要实现了这个魔法函数,它就是可迭代的类型

    Iterator

    迭代器和生成器 - 图2
    首先,Iterator继承了Iterable,在它的基础上,又增加了一个抽象方法:__next__(),它是用来让迭代器获取下一个元素。

    小结

    可迭代类型迭代器并不一样,前者只需要实现__iter__()函数,而对后者而言,__next__()才是它的核心。

比如list类型,它是一个可迭代类型,但并不是一个迭代器。

  1. a = [1, 2, 3]
  2. print(isinstance(a, Iterable), isinstance(a, Iterator))
  3. # result:
  4. # True False

补充

魔法函数那一小节,我们讲过这样一个例子:

  1. class Language(object):
  2. def __init__(self, language_list):
  3. self.lans = language_list
  4. def __getitem__(self, item):
  5. return self.lans[item]
  6. language = Language(["Python", "C", "Lisp"])
  7. for lan in language:
  8. print(lan)
  9. # result:
  10. # Python
  11. # C
  12. # Lisp

Language这个类中,我们定义的是__getitem__这个魔法函数,然后对这个类产生的实例我们可以使用for来遍历元素了。也就是说它成为了一个可迭代类型,但它并没有实现刚才我们讨论的__iter__()函数。

这是因为,在Python内部,很多地方做了兼容处理,当我们是用for进行迭代遍历,解释器首先会寻找__iter__()函数,如果没有,它就会退一步去寻找__getitem__(),这个是序列类型中会实现的一个魔法函数,只要它接收从 0 开始的整数为参数,这个对象也是会被当做可迭代类型的。

实际上,仅仅满足了可迭代类型还不够,真正能进行迭代取值的是迭代器。通过iter()函数,我们可以返回一个可迭代对象的迭代器,有了它才能进行迭代取值。

  1. class Language(object):
  2. def __init__(self, language_list):
  3. self.lans = language_list
  4. def __getitem__(self, item):
  5. return self.lans[item]
  6. language = Language(["Python", "C", "Lisp"])
  7. my_iterator = iter(language)
  8. print(my_iterator)
  9. # result:
  10. # <iterator object at 0x000002043CEE4F28>

如果我们不实现__iter__()__getitem__(),获取迭代器的过中会出错

  1. class Language(object):
  2. def __init__(self, language_list):
  3. self.lans = language_list
  4. language = Language(["Python", "C", "Lisp"])
  5. my_iterator = iter(language)
  6. print(my_iterator)
  7. # result:
  8. # TypeError: 'Language' object is not iterable

有了迭代器,迭代取值需要另外一个函数next(),每调用一次,就会返回一个值,直到抛出一个迭代结束的异常。

  1. class Language(object):
  2. def __init__(self, language_list):
  3. self.lans = language_list
  4. def __getitem__(self, item):
  5. return self.lans[item]
  6. language = Language(["Python", "C", "Lisp"])
  7. my_iterator = iter(language)
  8. print(next(my_iterator))
  9. print(next(my_iterator))
  10. print(next(my_iterator))
  11. print(next(my_iterator))
  12. # result:
  13. # Python
  14. # C
  15. # Lisp
  16. # StopIteration

上面的结果已经很接近直接使用for进行迭代了,但是,依赖__getitem__()函数,底层还是隐藏了很多细节,如果我们想纯粹地通过__iter__()来实现迭代过程,要怎么做呢?

__iter__()用来返回一个迭代器,通过这个迭代器来迭代取值,对应显示调用iter()的逻辑。

__next__()用来让迭代器取下一个值,对应显示调用next()的逻辑。

使用for的时候,这两个魔法函数会被自动调用,完成迭代取值过程。

  1. from collections.abc import Iterator
  2. class MyIterator(Iterator):
  3. def __init__(self, data_list):
  4. self.iter_list = data_list
  5. self.index = 0
  6. def __next__(self):
  7. # 这里是通过记录索引,单次取值达到迭代目的
  8. # 更好的方式是通过生成器来进行迭代取值
  9. try:
  10. data = self.iter_list[self.index]
  11. except IndexError:
  12. raise StopIteration
  13. self.index += 1
  14. return data
  15. class Language(object):
  16. def __init__(self, language_list):
  17. self.lans = language_list
  18. def __iter__(self):
  19. return MyIterator(self.lans)
  20. language = Language(["Python", "C", "Lisp"])
  21. for lan in language:
  22. print(lan)
  23. # result:
  24. # Python
  25. # C
  26. # Lisp

生成器函数使用

引言

  • 函数里面只要存在yield关键字,它就是生成器函数
  • 生成器是惰性计算的一个关键

    使用案例

    ```python def gen_func(): yield “MetaTian”

def func(): return “MetaTian”

gen, res = gen_func(), func() print(gen) print(res)

result:

MetaTian

for val in gen: print(val)

result:

MetaTian

  1. 第一个函数返回的是一个**生成器对象**,它是一个可迭代类型,因此,可以通过`for`进行访问。
  2. ```python
  3. def gen_func():
  4. yield 1
  5. yield 2
  6. yield 3
  7. gen = gen_func()
  8. for val in gen:
  9. print(val)
  10. # result:
  11. # 1
  12. # 2
  13. # 3

生成器的原理

Python中函数工作原理

对于编译型语言,函数的调用会维持一个函数调用栈,某个函数执行完成后,它就会被出栈处理,也就是说,函数执行后,它的生命周期就结束了。

对于Python这样的解释型语言,函数的调用也要依赖栈结构,但是,函数对象是存放在堆内存中的,也就意味着,一个函数被调用执行了,它还在那儿。

什么是堆内存和栈内存?

解释器用一个叫做PyEval_EvalFrameEx的C函数来执行Python程序。对于一个Python中的函数,解释器接受一个栈帧(stack frame)对象,并在这个栈帧的上下文中执行Python字节码,完成函数调用。在字节码执行中,如果遇到了要调用其他函数的指令,解释器会创建一个新的栈帧用来执行新调用的函数。

生成器+函数

  1. def gen_func():
  2. yield 1
  3. name = "MetaTian"
  4. yield 2
  5. return "done"

Python将函数编译为字节码时,如果遇到yield关键字,它就知道这是一个生成器函数,内部会做一个标记。当我们调用这个函数的时候,解释器看到这个标记后就会创建一个生成器,而不是去运行它,后续函数的执行交给生成器控制。

这个生成器内部有两个东西,一是对栈帧的引用,二是函数字节码的引用。栈帧中有一个指针,指向最近执行的那条指令,因为执行到和yield有关的字节码后,函数会停止执行,相当于打了个断点,同时将yield后面的值返回。通过next(),可以让函数继续执行(因为生成器也是迭代器),直到遇到下一个yield

如果直到函数结束都还没有遇到,说明迭代结束了,随后会抛出一个 StopIteration 的异常,这样的话,上面函数中的返回值是接收不到的,我们要手动进行处理才行,这个在后面生成器进阶部分介绍。

生成器对象也是分配在堆内存中的,也就是说,只要我们在程序运行的任何地方拿到了这个对象,都可以用它来控制函数的执行。这也是后面携程的一个理论基础。

重构自己的可迭代类型

引言

  • 前面将Language这个类,构建成为了我们自定义的一个可迭代类型
  • 生成器也是迭代器,通过使用生成器,可以更简洁地达成目的。 ```python from collections.abc import Iterator def gen_func(): yield “MetaTian”

gen = gen_func() print(isinstance(gen, Iterator))

result:

True

  1. <a name="568d18bd-1"></a>
  2. ## 使用案例
  3. ```python
  4. class Language(object):
  5. def __init__(self, language_list):
  6. self.lans = language_list
  7. def __iter__(self):
  8. i = 0
  9. try:
  10. while True:
  11. val = self.lans[i]
  12. yield val
  13. i += 1
  14. except IndexError:
  15. return
  16. language = Language(["Python", "C", "Lisp"])
  17. for lan in language:
  18. print(lan)
  19. # result:
  20. # Python
  21. # C
  22. # Lisp

总结

这里再来回顾一下前面讲过的内容,使用for遍历的时候,首先会看作用对象是否为一个可迭代类型,如果是,那么会隐式调用__iter__(),得到一个迭代器对象,有了它,再隐式调用__next__()来不断获取下一个元素,直到
遇到一个停止迭代的异常。我们也可以通过内置的两个函数iter()next()来人工干预迭代的过程。

Language类中,__iter__()内部加入了一个生成器的逻辑,结合前面的生成器函数,可以知道,遇到yield语句后,会产生一个生成器对象,由它来控制这个函数的后续执行。

因为生成器也是迭代器,所以__next__()的逻辑对它同样适用,每次 next 都会在__iter__()函数中的while循环中不断取值,直到抛出一个IndexError,迭代结束,__iter__()函数结束,for逻辑完成。

生成器读取大文件

引言

  • 有一个数据文件,大小为 10GB
  • 数据只有一行,行中有特殊的分隔符,现在需要剔除分隔符,获得每一个被分隔的元素。

    1. 比如,数据文件长这样:
    2. dj134o0kgfdkjfkdjfk'<end>6823sdkfsl<end>kfsldkfj'sdkfslfjyeroj<end>skfj...
    3. <end> 是其中的分隔符

    实现过程

    ```python def extract(f, sep): buff = “”

    while True:

    1. block = f.read(1024*4)
    2. if not block: # 没读到内容,说明读到尾部了
    3. yield buff # 上次留下来的内容
    4. break
    5. buff += block
    6. while sep in buff:
    7. cut = buff.index(sep) # 定位
    8. yield buff[:cut] # 分隔符前的一个元素
    9. buff = buff[cut + len(sep)] # 跳过分隔符和前面的元素

with open(“data.txt”) as f: for line in extract(f, ““): print(line) ```