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不同。
__iter__方法是Iterable的一个重要的魔法函数,只要实现了__iter__方法,那么就是一个可迭代类型。
我们的重点是Iterator(迭代器)
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的栈帧可以在它的调用之外存活。
现在这项技术被用到了 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的时候,产生一个栈帧,调用子程序的时候,又会创建一个栈帧
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
在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)