这几天在弄scrapy爬虫的时候,发现scrapy将爬取的网页用for循环去对每一小块进行处理,但是为了尽量少占用内存,在循环体内采用的是yield代替的return,从而通过生成器的方式实现了异步非阻塞的流水作业,边爬取边解析。这一篇就从原理来说一说python中必须要掌握但是又不太好区分的三个概念:可迭代对象,迭代器和生成器。
既然提到了生成器和yield,就先从它开始说起。不一次性生成全部结果,而是根据某种算法在需要的时候再推算出后续元素的方式,叫做生成器(generator)。
举了简单例子,某个函数调用一次可以返回列表[1,2,3]
,而另一个函数,调用第一次返回1
,第二次返回2
,第三次返回3
,每次调用之间可以执行别的任务。第二个函数就是一个生成器。
生成器的好处之一就是节约内存。想想一次计算要生成1万个元素的列表,如果等这1万个元素都生成再读进内存,不仅耗时而且占内存。而每生成一个元素就往后先执行,等下一个元素生成了再回来读取,就不用一次占用那么多的内存,同时也节约了时间。
但是前提条件就是后续操作是针对生成的单个元素的,假设后续操作是要对上面的一万个元素进行排序,那就必须得等所有元素生成完毕,这时生成器就不适用了。
补充:对于大文件无法一次性读进内存来进行排序操作感兴趣的朋友,可以参考另一篇博客《python3利用归并算法对超过内存限制的超大文件进行排序》
创建一个生成器的第一种方式是带yield的函数,以耳熟能详的斐波那契数列举例,下面是个普通函数
def fib_list(n):
"""返回列表"""
a = 0
b = 1
result = []
for i in range(n):
a, b = b, a + b
i += 1
result.append(a)
return result
fib=fib_list(8)
print(fib) # [1, 1, 2, 3, 5, 8, 13, 21]
如果去掉return,将每一次希望返回的值用yield关键字来返回,如下
def fib_generator(n):
"""返回生成器"""
a = 0
b = 1
for i in range(n):
a, b = b, a + b
i += 1
yield a
调用这个函数会返回一个生成器
fib = fib_generator(8)
print(fib) #
想要逐个获取生成器的元素,可以调用生成器的__next__()
方法,或者直接用内置next()
函数,例如
fib = fib_generator(8)
print(next(fib)) # 1
print(next(fib)) # 1
print(fib.__next__()) # 2
print(fib.__next__()) # 3
print(fib.__next__()) # 5
print(next(fib)) # 8
print(next(fib)) # 13
print(next(fib)) # 21
每次生成器都会在yield关键字处阻塞,下次调用的时候再继续执行。
如果想获取全部元素,直接用for循环
fib = fib_generator(8)
for i in fib:
print(i)
这里还要提一下,有的时候还会对生成器进行send(value)
操作,该操作会传递一个值到yield的位置,可以用于赋值给变量,但是要注意的是,一旦yield进行了赋值就只能返回send进去的内容,例如
def fib_generator(n):
"""返回生成器"""
a = 0
b = 1
for i in range(n):
a, b = b, a + b
i += 1
# yield a
tmp = yield a
print(tmp)
fib = fib_generator(8)
fib.__next__()
fib.__next__() # None
fib.send('test') # test
fib.__next__() # None
fib.__next__() # None
第一次调用__next__()
程序停在了yield
处,但没有任何操作,第二次调用程序首先返回了None给tmp
,然后打印tmp
又再次停在yield处。依次类推,每次都是打印传递进去的内容。
实际使用中主要还是从程序获取返回值,send
用的并不多。
创建一个生成器的第二种方式就是生成器表达式,非常简单,就是将列表生成器的中括号换成小括号。
例如
# result = [x ** 2 for x in range(5)]
result = (x ** 2 for x in range(5))
print(result) # at 0x00000148B6671DC8>
顺便温习一下列表生成器,基本上记住一下四种格式即可
result = [x ** 2 for x in range(5)] result1 = [x + y for x in range(3) for y in range(4)] result2 = [x for x in range(10) if x % 2 == 0] result3 = [x if x % 2 == 0 else x + 1 for x in range(10)] print(result) # [0, 1, 4, 9, 16] print(result1) # [0, 1, 2, 3, 1, 2, 3, 4, 2, 3, 4, 5] print(result2) # [0, 2, 4, 6, 8] print(result3) # [0, 2, 2, 4, 4, 6, 6, 8, 8, 10]
获取元素的方式也是调用__next__()
或者是for循环,就不再展开了。
生成器的本质是一种特殊的迭代器(iterator)。
在python中,任何实现了__next__()
和__iter__()
方法的都是迭代器,而这两个方法分别保证了前面提到的获取单个元素和for循环的实现。
from collections.abc import Iterator
class MyIterator:
def __init__(self):
pass
def __next__(self):
pass
def __iter__(self):
pass
myIterator = MyIterator('this is crazy')
print(isinstance(myIterator,Iterator)) # True
但是通常情况下很少会自己去创建迭代器类,而是要么直接使用生成器,要么用iter()
方法将一个容器,例如列表,字符串等,变成迭代器。
a = [1, 2, 3]
b = iter(a)
print(isinstance(a, Iterator)) # False
print(isinstance(b, Iterator)) # True
既然说到了iter()
方法,就可以引出可迭代对象(iterable)的概念了。
迭代,就是从迭代器中依次获取元素的意思。可迭代对象,就是能够变成迭代器的一个对象。在python中,凡是能够通过iter()
方法返回一个迭代器的都是可迭代对象。
那么iter()
方法究竟做了什么呢?首先会检查对象是否实现了__iter__()
方法,如果实现了就调用它,返回一个迭代器;如果没有实现__iter__()
但是实现了__getitem__()
,并且可以返回从下标0开始的元素,就会调用它,返回一个迭代器;如果前面两个方法都没有,就会抛出异常。
所以想要成为迭代器,必须能通过__iter__()
方法或者__getitem__()
方法返回一个迭代器。但是只有实现了__iter__()
方法的对象可以用isinstance(xx,Iteratable)
来判断。
下面直接用实例来演示下。
首先创建如下类,其实现了__getitem__()
方法,自带的一个参数就是下标,要确保可以从0开始进行元素获取
class MyIterable:
def __init__(self, n):
self.n = n
def __getitem__(self, item):
return range(self.n)[item]
myIterable = MyIterable(8)
myIterator = iter(myIterable)
print(isinstance(myIterator, Iterator)) # True
print(isinstance(myIterable, Iterable)) # False
for i in myIterator:
print(i, end=' ') # 0 1 2 3 4 5 6 7
可以发现,单独用__getitem__()
虽然可以达成Iterable的效果,但是并不能用isinstance()
来判断是一个Iterable。
再加一个__iter__()
方法,用yield返回一个生成器
class MyIterable:
def __init__(self, n):
self.n = n
def __getitem__(self, item):
return range(self.n)[item]
def __iter__(self):
a = 1
b = 1
for i in range(self.n):
yield a
a, b = b, a + b
i += 1
myIterable = MyIterable(8)
myIterator = iter(myIterable)
print(isinstance(myIterator, Iterator)) # True
print(isinstance(myIterable, Iterable)) # True
print(myIterator) # generator
for i in myIterator:
print(i, end=' ') # 1 1 2 3 5 8 13 21
可见iter()
方法会优先调用__iter__()
方法返回迭代器,而且可以通过isinstance()
来进行判断。
总结一下知识点
能够通过iter()
方法返回迭代器的对象叫做可迭代对象,python中的大多数容器例如列表,字符串都可以
可迭代对象是通过调用__iter__()
或者__getitem__()
来返回迭代器的
迭代器是实现了__next__()
和__iter__()
方法的对象,前者用来获取单个元素,后者用来实现迭代
生成器是一种特殊的迭代器,通常使用生成器的场景比较多
生成器有两种方式产生,循环中使用yield返回结果,或者用列表生成式来转换
我是T型人小付,一位坚持终身学习的互联网从业者。喜欢我的博客欢迎在csdn上关注我,如果有问题欢迎在底下的评论区交流,谢谢。