在爬虫中我们会经常去使用生成器来得到数据进行保存,同时会使用for循环来迭代一个对象,来对我们需要的数据进行提取,我们先看一下以前所编写的爬虫的部分代码
def get_data(self, html):
"""
提取网页上所需要的数据
:param html: 网页源码
:return: 评价数据
"""
doc = pq(html)
items = doc('div.comment-item').items()
for item in items:
yield pd.DataFrame({
'user_nick': [item('span.comment-info > a').text()], # 昵称
'score': [self.get_point(item('span.rating').attr('title'))], # 评分
'content': [item('span.short').text()], # 评价
'userful_num': [item('span.votes').text()] # 多少人觉得有用
})
这是之前抓取《阿丽塔》的影评的主要数据提取代码,我们都知道for
循环是用来迭代的,因此上述代码中我们可以将得到的items
称为是一个可迭代对象,然后在for
循环中,由于使用了yield
语句,因此调用get_data()
将会得到一个生成器。因此,当我们在进行大量的数据的抓取时,迭代是必不可少的,而生成器也能够实现代码的优化。说到这儿,可能大部分同学觉得有点偏题了,不是要讲迭代器和生成器嘛,怎么讲爬虫了,上述例子只是为说明迭代器和生成器的使用,毕竟要学以致用,能够用到实际当中才是最好的。
那到底什么是可迭代对象,迭代器,生成器呢,他们之间又有什么区别呢?由于本人也只是小白一枚,可能会不够全面,还望大家海涵。
由于本文需要对一个对象进行判断其是什么,因此会使用到isinstance()
函数,其具体的判断方法如下:
from collections.abc import Iterator, Iterable
from types import GeneratorType
isinstance(obj, Iterable) # 判断一个对象obj是否为可迭代对象
isinstance(obj, Iterator) # 判断一个对象obj是否为迭代器
isinstance(obj, GeneratorType) # 判断一个对象obj是否为生成器
以上代码将会在下文中多次出现。
可迭代对象
使用iter内置函数可以获取迭代器的对象。如果对象实现了能返回迭代器的__iter__方法,那么对象就是可迭代的。序列都可以迭代;实现了__getitem__方法,而且其参数是从零开始的索引,这种对象也可以迭代。
可能大多数人都不能明白上面的意思,没关系,我们用通俗易懂的方式来在解释一遍。首先,学过Python基础的,应该都知道列表、元组、字典、字符串都是可迭代的,因为我们都会对这些类型的数据使用for
语句,因此根据我们之前的编程经验我们可以知道:
- 能用for语句的一般都是可迭代对象
或许,你可能忘记了列表、元组、字典、字符串都是可迭代的,无所谓,我们通过代码来加深我们的影响:
from collections.abc import Iterator, Iterable
isinstance([], Iterable) # True
isinstance((), Iterable) # True
isinstance({}, Iterable) # True
isinstance('', Iterable) # True
- 如果对象实现了能返回迭代器的__iter__方法,那么对象就是可迭代的
在此,我们可以先忽略迭代器,上述话的意思就是,我们只要实现了__iter__
方法,便可以得到了一个可迭代对象。上文中我们知道了列表是可迭代对象,因此,我们可以先查看list
中是否有__iter__
方法,可以直接调用dir()
来进行查看:
dir(list)
['__add__', '__class__', '__contains__', '__delattr__',
'__delitem__', '__dir__', '__doc__', '__eq__', '__format__',
'__ge__', '__getattribute__', '__getitem__', '__gt__',
'__hash__', '__iadd__', '__imul__', '__init__',
'__init_subclass__', '__iter__', '__le__', '__len__',
'__lt__', ...] # 由于结果较多,只截取部分放在这里
当然,我们也可以自己在一个类中使用__iter__
方法来定义一个可迭代对象,如下
from collections.abc import Iterator, Iterable
class my_iter:
def __iter__(self):
pass
it = my_iter()
isinstance(it, Iterable) # True
# 这里类就是可迭代对象,我们可以不创建实例来进行判断
issubclass(my_iter, Iterable) # True
因此,当对象中实现了__iter__方法,那么对象就是可迭代的。
- 实现了__getitem__方法,而且其参数是从零开始的索引,这种对象也可以迭代
从上面的话中我们可以知道,在类中仅实现__getitem__
方法,也能够创建一个可迭代对象,当然,并不是单纯的实现__getitem__
方法即可,我们需要指定其参数是从零开始的索引,也就是说要在__getitem__
方法中定义一个索引参数, 如下:
from collections.abc import Iterator, Iterable
class my_iter:
def __getitem__(self, item):
return range(0, 5)[item]
it = my_iter()
isinstance(it, Iterable) # False
从上述结果,我们发现我们在类中构造了__getitem__
方法,但是我们在判断其是否为可迭代对象时,却输出False
,难道之前说的定义__getitem__
方法能够实现可迭代对象的说法是错误的?不,当然不是,这是因为abc.Iterable
并不会去考虑__getitem__
,其只要发现无__iter__
方法便输出False
。从Python 3.4 开始,检查对象obj能否迭代,最准确的方法是调用iter(obj)
函数(对于iter
函数的说明将在后面提到,在此先不阐述),因为iter(obj)
函数会考虑到遗留的__getitem__
方法。我们使用iter(obj)
函数,然后再对其进行判断
it = iter(my_iter())
isinstance(it, Iterable) # True
这样一来我们便创建了一个可迭代的对象,__getitem__
可以让对象实现迭代功能,这样就可以使用for...in...
来迭代该对象了
class Company:
def __init__(self, employ_list):
self.empoyee = employ_list
def __getitem__(self, item):
return self.empoyee[item]
company = Company(['mike', 'jack', 'lucy'])
for item in company:
print(item)
mike
jack
lucy
虽然在上例中没有实现__iter__
方法,但是Company实例是可迭代对象,因为发现有__getitem__
方法时,Python会调用它,传入从0开始的整数索引,尝试迭代对象。
- 序列都可以迭代
Python有6中种内建的序列:列表、元组、字符串、Unicode字符串、buffer对象、xrange对象,我们这里不对序列进行介绍,而主要对序列为什么可以迭代进行说明。
任何Python序列都可以迭代的原因是,它们都实现了__getitem__
方法,并且标准的序列中也都实现了__iter__
方法(从上面dir(list)
的结果可以看出)。当我们需要去迭代对象obj时,会自动调用iter(obj)
,内置的iter
函数执行以下操作(在这里我们可以先剧透一下,iter
将会创建一个迭代器):
- 检查对象是否实现了
__iter__
方法,如果实现了就调用它,获取一个迭代器 - 如果没有实现
__iter__
,但是实现了__getitem__
方法,Python会创建一个迭代器,尝试按顺序(从索引0开始)获取元素 - 如果尝试失败,Python抛出TypeError异常
上面我们对可迭代对象进行了介绍,并说明了如何自己去创建去创建一个可迭代对象,并且序列都是可迭代,但是其中涉及到了迭代器,因此,我们接下来将对迭代器进行介绍。
迭代器
实现了无参数的__next__方法,返回序列中的下一个元素,如果没有元素了,那么抛出StopIteration异常, 同时实现了__iter__方法的对象便为迭代器
我们首先对迭代器器中的方法进行说明
- __next__方法
返回下一个可用的元素,如果没有元素了,抛出StopIteration异常 - __iter__方法
返回self,以便在应该使用可迭代对象的地方使用迭代器,例如在for循环中。
接着,我们需指出可迭代对象和迭代器之间的关系:Python从可迭代对象中获取迭代器。我们可以看如下一个例子(看不懂没关系,我们后面会进行讲解,这边只是为了说明什么是迭代器),在这个例子中我们不去使用for...in...
,因为该语句的迭代器藏在背后,我们无法看到,
s = 'ABC'
it = iter(s) # 构建迭代器 it
# it # <str_iterator at 0x18ffe98bc50>
while True:
try:
print(next(it)) # 不断在迭代器上调用next函数,获取下一个字符
except StopIteration: # 当迭代器中没有字符了,抛出StopIteration异常,并退出循环
del it
break
# A
# B
# C
你可能会好奇上文中不是用iter
来判断可迭代对象的嘛,怎么这里使用iter
来创建一个迭代器呢,首先需要指出的是,iter
在构建一个可迭代对象时,同时iter
也会创建一个迭代器,我们将之前的代码补充如下:
it = iter(my_iter())
isinstance(it, Iterable) # True
isinstance(it, Iterator) # True
其次,可以回过头去看序列都是可迭代的中的iter
操作顺序,其明确指出会创建一个迭代器。
在这里,我们可以对for..in..
进行一个简单的说明,毕竟在使用迭代时,99%会使用到它。在用for..in..
迭代对象时,如果对象没有实现__iter__、__next__
迭代器协议的话,Python的解释器就会去寻找__getitem__
来迭代对象,如果连__getitem__
都没有定义,这解释器就会报对象不是迭代器的错误,同时for..in..
会隐式的调用next()
上述是使用内置函数来创建了一个迭代器,那我们能否自己创建一个迭代器呢?当然可以,我们只需要根据迭代器的概念,在对象中实现__iter__、__next__
迭代器协议即可。我们通过定义一个迭代器来实现斐波那契数列,并且只让其输出前5个(为了演示StopIteration)
class Fabs:
def __init__(self, count):
self.num1 = 0
self.num2 = 1
self.n = 0
self.count = count
def __next__(self):
if self.n < self.count:
self.num1, self.num2 = self.num2, self.num1 + self.num2
self.n += 1
return self.num1
raise StopIteration # 抛出异常
def __iter__(self): # 返回迭代器自身
return self
my_iterator = Fabs(5)
# 判断一下其是否是可迭代对象
isinstance(my_iterator, Iterable) # True
# 判断一下其是否是迭代器
isinstance(my_iterator, Iterator) # True
# 不断迭代迭代器,直至没有元素了抛出异常
next(my_iterator) # 1
next(my_iterator) # 1
next(my_iterator) # 2
next(my_iterator) # 3
next(my_iterator) # 5
next(my_iterator) # StopIteration
# 上述代码等价于:
my_iterator = Fabs(5)
for item in my_iterator:
print(item)
# 不过上述代码不会输出报错,这是因为for机制会捕获异常,在循环终止时不会报错,
# 感兴趣的小伙伴可以试一下
至此,我们便自己实现了一个迭代器,同时也构建了一个可迭代对象,因此,迭代器可以迭代,但可迭代对象不一定是迭代器。__next__
方法容易理解,就是取下一个值,我们可以构建一个列表的迭代器来更好的理解__next__
方法
my_list = [1, 3, 5]
my_lister = iter(my_list) # 获取迭代器
my_lister.__next__() # 1
my_lister.__next__() # 3
my_lister.__next__() # 5
my_lister.__next__() # StopIteration
你可能会好奇我们在迭代器中使用__iter__
方法返回自身不是多此一举嘛?其实不然,当我们调用next()
方法并捕获到异常后,我们将没有办法还原迭代器,如果想再次迭代,那就要调用iter()
,传入之前构建迭代器的可迭代对象。这里需要注意的是,传入迭代器本身没用,因为传入的迭代器是已经耗尽的迭代器,并不能还原,因此,我们需要在迭代器中使用__iter__
方法来返回实例自身。
现在,你应该更加困惑了,怎么一会儿可迭代对象,一会儿又是迭代器的,什么玩意儿。别急,我们接下来就将两者进行区别开来。在区别之前,我们先看一段代码,我们分别构建了一个可迭代对象和迭代器
import re
import reprlib
class Sentence: # 定义一个可迭代对象
def __init__(self, text):
self.text = text
self.words = re.findall('\w+', text)
def __iter__(self): # 实例并返回一个迭代器
return SentenceIterator(self.words)
class SentenceIterator: # 定义一个迭代器
def __init__(self, words):
self.words = words
self.index = 0
def __next__(self):
try:
word = self.words[self.index] # 根据索引获取单词
except IndexError:
raise StopIteration
self.index += 1
return word
def __iter__(self): # 返回实例自身
return self
我们对上述代码进行测试
from collections.abc import Iterator, Iterable
s = Sentence('How are you? I am fine')
isinstance(s, Iterable) # True
isinstance(s, Iterator) # False
for i in s:
print(i, end = ' ') # How are you I am fine
# 我们构造了一个可迭代对象s,并且并不是一个迭代器,但是在for循环中,调用了
# Sentence.__iter__,返回了一个迭代器,从而对迭代器进行迭代,在for循环中
# 隐式的调用了next()方法,对结果进行输出,因此若直接对s使用next,将会报错
next(s) # TypeError: 'Sentence' object is not an iterator
# 我们将next()方法显示的使用,即不使用for循环,因此我们需要将使用iter()
# 将s变成一个迭代器
s_it = iter(s) # 获取迭代器
isinstance(s_it, Iterable) # True
isinstance(s_it, Iterator) # True
next(s_it) # How
next(s_it) # are
...
next(s_it) # StopIteration # 索引不到单词,抛出异常
# 上述代码iter()即执行了Sentence.__iter__方法,返回了实例化的SentenceIterator迭代器,
# 通过调用next()方法,来逐一输出结果
通过上述代码我们可以总结出可迭代对象与迭代器之间的区别:
- 迭代器可以迭代,但是可迭代的对象不是迭代器
- 可迭代的对象有个
__iter__
方法,每次都实例化一个新的迭代器。可迭代对象一定不能是自身的迭代器,其必须实现__iter__
方法,但不能实现__next__
方法 - 迭代器要实现
__next__
方法,返回单个元素,同时,迭代器应该一直可以迭代,实现__iter__
方法,返回迭代器自身
生成器
在生成器中,生成器函数是绕不过去的,因为只有调用生成器函数,才会返回一个生成器对象。
- 生成器函数
只要Python函数的定义体中有yield
关键字,该函数就是生成器函数
根据上面的定义,我们不难得到只要函数中有yield关键字,该函数就是生成器函数,举个例子
def gen():
yield 1
yield 2
gen
# <function __main__.gen()> # 函数对象
gen()
# <generator object gen at 0x0000017C991B9228> # 返回一个生成器对象
我们为什么要使用生成器呢,因为当我们计算的数据量较大时,将会消耗大量的内存,一般在处理数据的函数都是一次性返回所有的数据(若在得到一个数据后就对该函数使用return语句,该函数将停止执行),但是生成器却不一样,其一次只产生一个值,打个比方,现在有一把糖,每次只能拿一个(或根据要求的数量),因此,对于生成器而言,我们想把所有的数据都取出时,需进行迭代(for循环),所以,我们可将生成器看作是迭代器,见如下代码
def gen():
yield 1
yield 2
for item in gen():
print(item)
# 1
# 2
显然,使用for...in
我们无法看出生成器的执行过程。由于生成器是迭代器,因此我们将使用next()
来进行展示
from collections.abc import Iterator, Iterable
def gen():
yield 1
yield 2
g = gen()
isinstance(g, Iterable) # True # 判断其生成器是可迭代对象
isinstance(g, Iterator) # True # 判断其生成器是迭代器
next(g) # 1
next(g) # 2
next(g) # StopIteration
生成器函数在每次调用next()
的时候遇到yield
便返回暂停,等到下一次next()
再继续执行,当生成器函数的定义体执行完毕后,生成器会抛出StopIteration
异常。也就是说用多少取多少,但是再某些情况下,直接使用。但是在某些情况下,使用生成器函数与不使用生成器函数所占用的内存差不多,比如在本文一开始的时候的爬虫中,我们便得到了一个满足条件的列表items
,并且我们只是逐一的从该列表中产出我们所需要的数据,这里一开始便计算出列表中的所有元素无疑将消耗大量的内存,因此我们需要考虑有没有一种方法能够减少内存的使用,并得到同样的结果。在这里,就需要考虑惰性实现。
所谓惰性实现是指尽可能延后生成值。还以取糖为例,我们首先假设一次只能拿一颗。惰性实现就是我们取出一颗糖并对其处理完后,再去工厂取糖,但是这里工厂每次只有在我们需要的时候才去生产,而不像以往的一下的全部生产出来,如下
def gen(): # 按需生产
print('first need')
yield 1
print('second need')
yield 2
def gen2():
for item in gen():
yield item
for item in gen2():
print(item)
显然用for...in...
无法说明情况,同样使用next()
方法,
g = gen2()
next(g)
# first need
# 1
next(g)
# second need
# 1
换句话说,就是对糖工厂使用了yield
语句,让他一个一个的生产,我们再一个一个的取出,也就是只有在我们需要的时候才生产一个糖。在爬虫中,我们可以将匹配的列表同样构造一个生成器函数(返回的不是一个列表,而是一个生成器)来节省内存。
我们在学习Python基础的时候,都知道使用列表推导来快速的构建列表,同样我们也可以使用类似于列表推导的生成器表达式来创建其他任何类型的序列,其与列表推导的区别在于使用的是小括号,而不是中括号。
- 生成器表达式
生成器表达式可以理解为列表推导的惰性版本(即一个一个的按需产出):不会迫切的构建列表,而是返回一个生成器,按需惰性生成元素。也就是说,如果列表推导是制造列表的工厂,那么生成器表达式就是制造生成器的工厂。
我们来看一个生成器表达式的例子,使用生成器表达式来计算一个数的阶乘
def factorial(n):
return 1 if n < 2 else n * factorial(n - 1)
my_generator = (factorial(i) for i in range (0, 3))
next(my_generator) # 1 # 计算0的阶乘
next(my_generator) # 1 # 计算1的阶乘
next(my_generator) # 2 # 计算2的阶乘
next(my_generator) # StopIteration # 生成器表达式的定义体执行完毕,抛出异常
由于for循环就是通过不断调用next()函数实现的,故上述代码也可用for...in...
来实现,不过其异常不会被抛出。如果不使用生成器表达式,直接使用yield
语句,将会增加代码量,如下
def my_generator():
for i in range(0, 3):
yield factorial(i)
显然生成器表达式能够在一定程度上减少代码的编写。当然如果你编写的生成器表达式需要分行实现,那还是定义生成器函数较好。
其实,在之前的学习过程和自己编程的时候就已经在潜移默化的在使用一些生成器函数,有的可能还使用的相当的熟练,如map、enumerate
等。故在此对一些常用的生成器函数进行简单说明。
- filter(predicate, it)
把it中的各个元素传给predicate,如果predicate(item)返回真值,那么产出对应的元素;如果predicate是None,那么只产出真值元素。
filter(lambda x: x % 2 == 0, [1, 2, 3, 4])
# <filter at 0x17c9919a940>
list(filter(lambda x: x % 2 == 0, [1, 2, 3, 4]))
# [2, 4]
这里需注意的是,直接使用filter
是无法产出值的,因为它是一个生成器函数,需要去迭代或使用next
才可以产出,故在这里我们选择了list
方法来进行迭代产出。
- enumerate(iterable, start = 0)
产出由两个元素组成的元组,结构是(index, item),其中index从start开始计数,item则是从itearable中获取
list(enumerate([1, 2, 3, 4], 0))
# [(0, 1), (1, 2), (2, 3), (3, 4)]
list(enumerate([1, 2, 3, 4], 10))
# [(10, 1), (11, 2), (12, 3), (13, 4)]
- map(func, it1, [it2, …, itN])
把it中的各个参数传给func,产出结过,如果传入N个可迭代对象,那么func必须能接受N个参数,而且要并行处理各个可迭代的对象
list(map(lambda x: x ** 2, [1, 2, 3, 4]))
# [1, 4, 9, 16]
import operator
list(map(operator.mul, [1, 2, 3, 4], [1, 2, 3, 4]))
# [1, 4, 9, 16]
- zip(it1, it2, …, itN)
并行从输入的各个可迭代对象中获取元素,产出由N个元素组成的元组,只要有一个可迭代对象到头了,就默默的停止了
list(zip('ABC',[1, 2, 3]))
# [('A', 1), ('B', 2), ('C', 3)]
list(zip('ABC',[1, 2, 3, 4]))
# [('A', 1), ('B', 2), ('C', 3)]
- reversed(seq)
从后向前,倒序产出seq中的元素,seq必须是序列,或者是实现了__reversed__特殊方法的对象
names = ['Tom', 'Andy', 'Jack']
list(reversed(names))
# ['Jack', 'Andy', 'Tom']
- yield from语句
在生成器中,我们还需要了解一下yield from
语句,这是在Python 3.3 中新出现的语法,其可以帮助我们在生成器函数中产出另一个生成器生成的值(和之前的惰性实现的概念差不多),传统的解决方法是使用嵌套的for
循环,如下:
def gen():
for i in range(0, 3):
yield i
def my_generator():
for item in gen():
yield item
list(my_generator())
# [0, 1, 2, 3, 4]
我们使用yield from
来代替my_generator
中的for
循环,
def gen():
for i in range(0, 5):
yield i
def my_generator():
yield from gen()
list(my_generator())
# [0, 1, 2, 3, 4]
至此,关于Python中可迭代对象,迭代器和生成器的介绍就结束了,我们简单的总结一下:
可迭代对象
- 列表、元组、字典、字符串都是可迭代对象
- 能用for语句迭代的都是可迭代对象
- 实现了
__iter__
方法的对象是可迭代对象 - 实现了
__getitem__
方法的对象是可迭代对象
迭代器
- 实现了
__iter__
方法和__next__
方法的对象是迭代器,同时迭代器应该一直可以迭代,故__iter__
方法应该返回迭代器自身 - 迭代器可以迭代,可以看作是可迭代对象,但是可迭代对象不是迭代器,同时可迭代对象一定不能是自身的迭代器
- 调用
next(obj)
来获取元素,当然一般都是直接使用for...in...
生成器
- 生成器是由生成器函数创建的,而生成器函数是包含
yield
语句的函数 - 对于生成器需要迭代(next)才能够产出值
- 由于生成器和迭代器都是定义了
__iter__
方法和__next__
方法方法,故可将所有的生成器看作是迭代器
由于刚步入Python不久,对于可迭代对象,迭代器和生成器的了解不足,难免会有错误,还望大家指正。
好好学习,天天向上!