每天一点动力——python基础(三)
前言
记录一下复习python的过程,有不对的地方欢迎在评论区批评指正,感谢!!
一、迭代器、生成器
迭代器和生成器这一部分的内容不是很好理解,很多时候我们在使用的过程中都会用到迭代器和生成器,但是其具体原理和实现我们可能不太关注,而且在使用迭代过程中程序出错了可能我们也不知道到底是哪里出错了,然后使用一大堆自己都不太懂的方法改,结果就是可能某一种修改方法正好管用了。这样一来一回就可能会出现这么一个情况:我的程序怎么错了?我的程序怎么又好了?这个问题可能是个玄学问题吧?……这些问题其实我在平时都碰见过,但是查阅过相关资料后就会发现,除了逻辑上的错误,很多时候的错误都是因为对基础知识点不了解,尤其是那种容易混淆的知识点。
对于迭代器和生成器我们需要一步一步的理解,其实任何事情都有个头,任何原理都是有依据的,把那个“源头”,然后一步一步去推,总会能理解的。
要想理解迭代器和生成器,需要把以下几个概念搞懂:容器、可迭代对象、迭代器、生成器。
先盗一张图,感谢这位大神
1.容器
日常生活中,我们说的容器是可以盛放东西的一种器具,例如,一个盒子、一间屋子、一个瓶子……,只要能盛放东西,我们都可以称之为容器,而在python中,也可以把可以盛放元素的对象叫做容器,例如列表、字典、元组、集合等,这些对象都有一个共性就是可以存放元素(尽管存放的形式不同),因此,可以通过迭代的方式来获取其中的元素,当然,也可以通过in或not in等方式判断元素是否在这些容器中。大部分的容器对象都是把元素放在内存中,不过今天所记录的迭代器和生成器除外(下面会记录)。
大部分容器对象都可以通过for循环等某种方式获取其中的元素,但这并不是容器本身的特性,而是可迭代对象赋予的功能。而有些容器对象是不能获取到里面的元素的(比如Bloom filter),所以归根到底,列表、字典等这些容器之所以能够进行元素的迭代获取,是因为其本事是可迭代的对象。
2.可迭代对象
在python中,序列(列表、元组、集合等)、字典、迭代器、生成器等都属于可迭代对象,通俗理解就是,我们可以通过不断迭代的方式从这些对象中获取元素;
具体来说,内部含有__iter__方法的对象都是可迭代对象。以列表为例:
>>> list1 = [1,2,3,4,5]
>>> dir(list1) # dir方法用于检查对象内部的方法
['__add__', '__class__', '__contains__', '__delattr__', '__delitem__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__gt__', '__hash__', '__iadd__', '__imul__', '__init__', '__init_subclass__', '__iter__', '__le__', '__len__', '__lt__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__reversed__', '__rmul__', '__setattr__', '__setitem__', '__sizeof__', '__str__', '__subclasshook__', 'append', 'clear', 'copy', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']
对于列表来说,可以通过for循环来获取列表中的每一个元素:
>>> for i in list1:
... print(i)
...
1
2
3
4
5
实际上,在for循环中,是通过以下方式来执行的:
- 首先,列表list1是一个可迭代对象;
- 在for循环执行时,首先调用可迭代对象内部的__iter__()方法将可迭代对象变成迭代器;
- 然后,使用迭代器中的__next__()方法迭代的输出列表中的每一个值;
判断一个对象是否是可迭代对象,有两种方法:
方法一:判断该对象中是否有__iter__方法:
>>> '__iter__' in dir([1,2,3])
True
>>> '__iter__' in dir((1,2,3))
True
>>> '__iter__' in dir("hello")
True
>>> '__iter__' in dir("123")
True
>>> '__iter__' in dir(123)
False
可以看出,字符串是可迭代对象,但是数字不是可迭代对象。换句话说,字符串可以使用for循环获取每一个字符,而数字就没有这个功能了。
方法二:使用collections中的Iterable方法进行判断:
from collections.abc import Iterable
# isinstance():判断对象的类型,返回的是布尔值
print(isinstance([1, 2, 3], Iterable)) #True
print(isinstance((1, 2, 3), Iterable)) # True
print(isinstance("hello", Iterable)) # True
print(isinstance("123", Iterable)) # True
print(isinstance(123, Iterable)) # False
3.迭代器
迭代器是一种可迭代对象,换句话说,迭代器中的元素也可以通过某种方法进行获取,迭代器除了包含__iter__方法外,还包括__next__方法;换句话说,包括__iter__和__next__方法的都是迭代器。
注:可迭代对象和迭代器不等同(要不然也不会用两个概念了),可迭代对象和迭代器的关系如下:
- 可迭代对象包含迭代器。换句话说,迭代器只是可迭代对象中的一类;
- 如果一个对象有__iter__方法,那么这个对象是一个可迭代对象;如果一个可迭代对象有__next__方法,那么这个对象就是一个迭代器对象;
- 定义一个可迭代对象,要实现__iter__方法;定义一个迭代器,要同时实现__iter__方法和__next__方法;
使用迭代器的好处是:
- 可以节省内存空间;迭代器中的值是在使用的时候在生成,所以同一时刻在内存中只有一个值;
- 提供了一种通用不依赖索引的迭代取值方式;
而迭代器也是有缺点的:
- 取值不如按照索引的方式灵活,不能取指定的某一个值,只能往后取,不能往前去;
- 无法预测迭代器的长度;
迭代器中的__iter__()方法返回的是迭代器自身,而__next__()方法返回的是迭代器的下一个值,如果迭代器中没有更多的元素,就会抛出StopIteration异常;
迭代器就像一个懒惰的加工厂,有人用时才产生数据放入内存,没人用时就处于休眠状态,等待下一次调用。
我们可以将一个可迭代对象构造成迭代器(比如在做文本预处理的时候,如果原始文本数据过大,超出了机器内存范围,这个时候就可以把数据放在迭代器中,调用时才获取到元素),使用iter()方法或者__iter__()都可以将可迭代对象转化成迭代器:
list1 = [1, 2, 3, 4, 5, 6]
# iter()和__iter__()两种方式都可以将一个可迭代对象转化成迭代器
list2 = iter(list1)
list3 = list1.__iter__()
print(list2)
print(list3)
# 执行结果
<list_iterator object at 0x0000022357EDFB08>
<list_iterator object at 0x0000022357EF81C8>
迭代器中的数据只能通过__next__()方法或next()进行获取,且只能向后获取,当迭代器中没有更多元素的时候,抛出StopIteration异常,以列表为例:
for i in range(6):
print(f"list2:{list2.__next__()}")
# 执行结果:
list2:1
list2:2
list2:3
list2:4
list2:5
list2:6
print(next(list3))
print(next(list3))
print(next(list3))
print(next(list3))
print(next(list3))
print(next(list3))
print(next(list3)) # 此时list3中已经没有元素了
# 执行结果
1
Traceback (most recent call last):
2
3
File "E:/python/echart练习/1111.py", line 18, in <module>
4
print(next(list3))
StopIteration
5
6
其他可迭代对象构造成迭代器的方法也是一样的。
此外,在一些特殊需求上,我们也可以自己构造一个迭代器(迭代器中必须包含__iter__()方法和__next__()方法)。
实现一个迭代生成26个英文小写字母的功能:
class MyIter(object):
def __iter__(self): # 定义__iter__()方法
self.start = 'a' # 迭代器的初始值
return self # __iter__返回迭代器自身
def __next__(self):
if ord(self.start) <= 122: # ord(str):返回字符的ASCII码值
str_ = self.start
self.start = chr(ord(self.start) + 1) # chr():将ASCII转化成字符
return str_
else:
raise StopIteration # 如果ASCII值超过122,即超过了‘z’后,就处罚StopIteration异常
my_iter = MyIter()
my_iter.__iter__() # 调用了__iter__()后才是一个迭代器
print(my_iter)
for i in range(26):
print(my_iter.__next__(),end=" ")
程序执行结果:
<__main__.MyIter object at 0x000001449503B288>
a b c d e f g h i j k l m n o p q r s t u v w x y z
通俗来说,迭代器是一种特殊的可迭代对象。一般的可迭代对象(如列表、元组)都是事先存储好元素,然后将元素全部放在内存中,用时直接来取就行;而迭代器属于“用时再做”,即当被调用时,迭代器才产生元素,然后放在内存中,并且每次只产生一个元素;没有被调用时,迭代器就处于休眠状态,等待下一次被调用。这样做的好处就是可以节省内存空间,不好的地方就是只能一个一个向后获取元素,并且事先无法判断这个容器的长度。
4.生成器
在记录生成器之前,先补充一下迭代器。当我们创建好一个迭代器或者将可迭代对象转化成迭代器后,其实迭代器中的元素已经固定了,我们只能通过__next__()方法进行元素的获取,且获取到的元素是我们提前指定好的。如果我们在使用迭代器时,在某一个迭代过程中,突然想要改变一下迭代器的值,这个时候就用到了生成器。
生成器是一种特殊的迭代器,其本质还是迭代器。生成器是一种带有yield的函数。
yield和return一样,都是用于返回函数执行的结果。不同的是,return在返回结果后结束函数的运行,而yield则让函数变成一个生成器,生成器每次产生一个值(yield语句),函数被冻结,被唤醒后,再次产生一个值,而唤醒生成器的方式就是__next__(),即取下一个值。
在调用生成器运行的过程中,每次遇到 yield 时函数会暂停并保存当前所有的运行信息,返回 yield 的值, 并在下一次执行 next() 方法时从当前位置继续运行。调用一个生成器函数,返回的是一个迭代器对象。
换句话说,带有yield关键字的都是生成器。
关于yield,需要注意以下几点:
- yield只能在函数内使用;
- yield可以保存函数暂停的状态;
- yield对比return:相同点,都可以返回值,值得类型与个数没有限制,不同点:yield可以返回多次值,而return只能返回一次值函数就会结束。
一个最简单的生成器:
def func():
print("first")
# 在直接调用__next__方法或用for语句进行下一次迭代时,生成器会从yield下一句开始执行,直至遇到下一个yield。
yield 1 # 第一次返回1,第一次调用__next__时,程序走到这里就会暂停,下面的语句就不再执行
print("second")
yield 2 # 第二次返回2,第二次调用__next__时,程序走到这里暂停,下面的语句就不再执行
# 注:第二次调用__next__的时候,只执行print("second")和yield 2,前后程序都不会执行
print("three") # 第三次执行__next__时,程序才会走这条语句,不过第三次调用时会报错,因为这个迭代器只返回了两个值,第三次调用就会抛出异常
fun = func()
print(fun)
print(dir(fun))
print(fun.__next__())
print("*"*30)
print(fun.__next__())
执行结果:
<generator object func at 0x00000232D327D448>
['__class__', '__del__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__lt__', '__name__', '__ne__', '__new__', '__next__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'gi_code', 'gi_frame', 'gi_running', 'gi_yieldfrom', 'send', 'throw']
first
1
******************************
second
2
在生成器执行过程中:在直接调用__next__方法或用for语句进行下一次迭代时,生成器会从yield下一句开始执行,直至遇到下一个yield。
生成器中有__iter__和__next__方法,所以也是一个迭代器。
实现一个生成26个小写英文字母的生成器:
def func():
a = 'a'
while True:
if ord(a) <= 122:
yield a # 每次调用遇见yield后,函数都会暂停,等下次调用的时候才会走下面的语句
a = chr(ord(a) + 1)
fun = func()
for i in range(26):
print(fun.__next__(), end=" ")
执行结果:
a b c d e f g h i j k l m n o p q r s t u v w x y z
到目前来看,生成器与迭代器实现的功能基本一样,那为什么还要使用生成器呢?
这时候就涉及到生成器的另一个性质:在迭代过程中,生成器可以修改当前迭代中的函数返回值,例如:
def myList(num): # 定义生成器
now = 0 # 当前迭代值,初始为0
while now < num:
val = (yield now) # 返回当前迭代值,并接受可能的send发送值;yield在下面会解释
now = now + 1 if val is None else val # val为None,迭代值自增1,否则重新设定当前迭代值为val
my_list = myList(5) # 得到一个生成器对象
print(my_list.__next__()) # 返回当前迭代值
print(my_list.__next__())
my_list.send(3) # 重新设定当前的迭代值
print(my_list.__next__())
执行结果:
0
1
4
5.迭代器和生成器对比
-
迭代器
迭代器(iterator)是一个实现了迭代器协议的对象,我们可以使用iter()方法将可迭代对象转化成迭代器,也可以自己创建一个可实现迭代器协议的容器并且通过for,next()等方法进行迭代。在迭代的末尾,会触发stopIteration异常。 -
生成器
生成器是一种特殊的迭代器,可以通过yield语句快速生成可迭代对象,并且不使用__iter__和__next__方法。yield可以使一个普通函数变成一个生成器,并且相应的next()方法返回是yield后的值。一种更直观的解释是:程序执行到yield时会返回结果并暂停,再次调用next时会从上次暂停的地方继续开始执行。 -
迭代器和生成器对比
相同点:
生成器是一种特殊的迭代器;
不同点:
语法上:生成器通过函数的形式中调用yield语句来实现,即在函数中有yield就是生成器;而迭代器是通过iter()方法或可迭代对象内部的__iter__()方法来实现;
用法上:生成器在调用next()函数或for循环中,所有过程被执行,且有返回值,返回值使用yield返回;迭代器在调用next()函数或for循环中,所有值被返回,没有其他过程或说动作。
写在最后
本文是个人的一些学习笔记,如有侵权,请及时联系我进行删除,谢谢大家.