忽略的细节之Python 迭代器与生成器,与lambda函数应用
2021/7/11 20:09:38
本文主要是介绍忽略的细节之Python 迭代器与生成器,与lambda函数应用,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
忽略的细节之Python 迭代器与生成器,与lambda函数应用
简介
本文前半部分是迭代器与生成器内容,尽可能地讲得详细,并指出了几处易忽略的细节;后半部分是迭代器结合 lambda 函数的使用,纯 lambda部分较简略。
ps:总结中有干货哦
目录
- 忽略的细节之Python 迭代器与生成器,与lambda函数应用
- 简介
- 前言
- 迭代器与生成器
- 迭代器
- 生成器
- 类作为迭代器使用
- 总结(有干货!!!)
- lambda函数
- 简单lambda的使用
- lambda与 iter() 居然可以这样用
前言
很奇怪为什么把这两块内容放一起是吗?因为,我懒。
最近有接触到涉及这两块的相关内容,有些遗忘,于是重新查看了一些相关文档,把自己的心得体会分享一下,也顺便方便自己以后查阅,毕竟自己写的东西才是最香的。
进入正题,大家清楚以下短短两行代码涉及的各个知识点与细节吗?
def function(*args, **kwgs): yield from iter(lambda params: expression, sentinel)
如果完全清楚,您可以翻篇了,这篇文章可能帮不到您,感谢您的光临!!
如果不是很理解,那么看完这篇文章,相信大家一定能有所收获。里面的细节值得大家体会。
对于上述问题,本着一点点地抽丝剥茧的原则,本人尽量逐个讲清其中的各个知识点。
先简单解释一下里面的各个参数:
- *args, **kwgs:参数个数不确定时,使用 *args, **kwgs 代表函数可能传递的参数列表。*args 没有key值,**kwargs有key值。
- params:由 *args, **kwgs 得到的一组参数,或者为空
- expression:单个表达式
- sentinel:哨兵,就是一个起监督作用的对象,其类型与 iter() 第一个参数有关,下文会进一步介绍。
关于它们的具体内容,就是本文涉及的主要知识点了。
迭代器与生成器
迭代器
先从头说起吧,迭代器 ( iterator ) 是一个可以记住遍历位置的对象,什么意思呢?就是迭代器只能顺序地从第一个位置的元素开始向后遍历,并且可以知道遍历过程中下一个应该访问元素的位置。
""" 本文全文基于 python 3.7 """ # 先使用一下 for 循环 tmp_list = [2, 4, 6, 8, 10] for i in range(0, len(tmp_list)): print(tmp_list[i], end=' ') # 再简单一点 tmp_list = [2, 4, 6, 8, 10] # 替换为()元组,[]列表,{}集合,""字符串均可 for i in tmp_list: print(i, end=' ') # 结果均为:2 4 6 8 10 # 接下来用 迭代器 完成相同效果 tmp_iter = iter(tmp_list) # 用iter()生成一个迭代器对象 while True: try: print (next(tmp_iter), end=' ') # 调用一次next(),就会遍历下一个元素 except StopIteration: break # 结果为:2 4 6 8 10 print(type(tmp_iter)) # <class 'list_iterator'> tmp_set = {2, 4, 6, 8, 10} tmp_iter = iter(tmp_set) print(type(tmp_iter)) # <class 'set_iterator'>
-
上面定义的 tmp_iter 就是一个迭代器对象了,同时也应注意到,我们常说的迭代器 iterator 是一个较宽泛的类型 ( 基类 ) ,其下有各种子类,如 list_iterator, set_iterator. 迭代器有两个基本方法 iter() 和 next() , iter() 返回一个迭代器对象,而每调用一次 next() 就会返回下一个元素值。 或者说, iter() 就像是给迭代器初始化,而 next() 则是从头开始依次访问迭代器对象中的元素。
-
这里使用 StopIteration 原因在于当迭代器最后一个元素都已经访问过了时,已经完成了一次遍历,next() 已经没有下一个元素可以访问了,故停止迭代 ( StopIteration ) 。如果不加 try…except… 异常机制,会出现如下情况,程序执行会抛出停止迭代异常。
tmp_iter = iter(tmp_list) while True: print (next(tmp_iter), end=' ') # Traceback (most recent call last): # File "****.py", line 20, in <module> # print (next(tmp_iter), end=' ') # StopIteration
而使用 for 循环代替 while 循环可以自动停止迭代,不会抛出 StopIteration 异常,下文有例子。关于 iter() 的使用,列表,元组,集合,字符串对象都可用于创建迭代器,即它们可作为 iter() 的参数,能够作为 iter() 参数的对象,称之为可迭代 ( iterable ) 对象。下面方法也是可行的,同时也说明了 for 进行迭代时,隐式的调用了 next().
tmp_list = {2, 4, 6, 8, 10, 12} for i in iter(tmp_list): print(i, end=' ') # 结果仍为:2 4 6 8 10 12,且注意这里没有抛出 StopIteration 异常 print('\n=================') iter1 = iter(tmp_list) for i in iter1: print(i, 'and', next(iter1)) # ================= # 2 and 4 # 6 and 8 # 10 and 12
生成器
再说说生成器 ( generator ) 。迭代器是一个能记住遍历位置的对象,而生成器则是一个函数,是能返回一个迭代器的函数。使用了 yield 的函数被称为生成器。那 yield 又是什么呢?
from 菜鸟教程:
在调用生成器运行的过程中,每次遇到 yield 时函数会暂停并保存当前所有的运行信息,返回 yield 的值, 并在下一次执行 next() 方法时从当前位置继续运行。
为便于理解,大家可以认为:每一次执行 yield 是返回一个值,而整个生成器返回的是一个迭代器。接下来是例子
# 输出2的1~n次幂, 2,4,8,16,... def func(n: int): i, num = 1, 2 while i <= n: yield num num = num * 2 i += 1 for i in func(5): print(i, end=',') # 结果为:2,4,8,16,32, print(list(func(5))) # [2, 4, 8, 16, 32]
每一次程序运行到 yield 便会暂停并保存当前所有的运行信息,yield 会将此刻 num 的值返回,之后再从暂停的位置继续向下运行程序。其实这里说生成器返回值是一个迭代器也不完全准确,事实上
f = func(5) print(f) # <generator object func at 0x000001BF70F123C8> print(type(f)) # <class 'generator'>
生成器的返回值仍是一个生成器对象,不过这个返回值所起的作用和迭代器作用基本是一样的。为什么作用是一样的呢?因为生成器对象也具有 next()
方法,先来对比两个方法 next()
和 __next__()
while True: try: print(next(f), end=' ') except StopIteration: break # 2 4 8 16 32 while True: try: print(f.__next__()) except StopIteration: break # 2 4 8 16 32
next()
是 python3 内置函数,而 __next__()
是生成器 (class generator
) 内部自己定义的方法,用于找到遍历生成器时的下一个待访问元素。当调用next(f)
时,实际上next()
会找到生成器中的__next__()
并执行,因此这两个方法结果是一致的,这也导致了生成器对象具有迭代器的一些特点。
-
还有一个小细节值得注意
f = func(5) print(list(f)) print('=================') print(next(f)) # print(f.__next__())
猜猜代码运行结果是什么?大家不妨先自己想想预期的结果是什么。
[2, 4, 8, 16, 32] ================= Traceback (most recent call last): File "****.py", line 25, in <module> print(f.__next__()) StopIteration
最后一个 print() 并没有如愿输出我们想要的结果 ‘ 2 ’,那么去掉
print(list(f))
呢?f = func(5) print(next(f)) # print(f.__next__()) # 结果为:2 print(next(f)) # print(f.__next__()) # 结果为:4
-
这一次结果符合预期了。这表明,当我们调用 list() 后,实际上是进行了一次对生成器的遍历,并将生成器所有元素按序存入列表,这个过程 list() 隐式地将遍历位置移到了迭代器末尾,导致再执行 next(f) 时,已经没有下一个元素需要遍历了, next() 误认为迭代器已经完成了遍历,于是抛出 StopIteration 异常。
-
另外再拓展一点,前面说的集合{}可用于创建迭代器,那么同样是用花括号{}作为边界的字典 dict 类型是否也可以呢?
tmp_dict = {'a': '1 apple', 'b': '2 banana', 'c': '3 cabbage'} it = iter(tmp_dict) for k, v in it: print(v, end=' ') # Traceback (most recent call last): # File "****.py", line 49, in <module> # for k, v in it: # ValueError: not enough values to unpack (expected 2, got 1)
没有足够的值可取出?Why?我悄悄去掉一个 k ,
for v in it: print(v, end=' ') # 结果:a b c # 为什么只输出了键值对中的 key 值? print(type(it)) # <class 'dict_keyiterator'>
原来这里的迭代器对象 it 是属于
class 'dict_keyiterator'
,即是一个字典 dict 中 key 的迭代器,只会对键值对中的键 key 进行迭代,而键值对的值 value 被 iter 无情抛弃了。
类作为迭代器使用
不光我们熟悉的一些常用类型可以创建迭代器对象,用户自定义的类也可以作为迭代器使用,这样便突破了迭代器只有固定类型的局限性,拓展了迭代器的适用范围,某些情况下可以带来很大的便利。
想把一个类当作迭代器使用,需要我们在类中实现两个方法 __iter__()
与__next__()
。__iter__()
方法返回一个特殊的迭代器对象, 这个迭代器对象要求实现了 __next__()
方法并通过 StopIteration 异常标识迭代的完成。__next__()
方法则返回下一个迭代器对象。
之前提到的生成器类具有迭代器的性质正是因为生成器中也实现了__iter__()
与__next__()
。对于一个生成器对象 gener1,调用iter(gener1)
相当于调用gener1.__iter__()
,调用next(gener1)
相当于调用gener1.__next__()
,这样一来,生成器类可作为迭代器使用,gener1 便成为了可迭代对象。
举个简单例子,注意__iter__()
方法需要返回一个实现了 __next__()
方法的迭代器对象,不一定非要 self。( 文末有__iter__()
返回其他类的例子 )
class MyIter: def __init__(self): self.num = 1 def __iter__(self): self.num += 1 return self def __next__(self): self.num *= 2 return self.num a_iter = MyIter() # self.num = 1 print(next(a_iter), end=' ') print(next(a_iter), end=' ') print(next(a_iter), end=' ') # 2 4 8 b_iter = iter(a_iter) # 这里 self.num 在原来为 8 的基础上 +1 变为 9 print(next(b_iter), end=' ') print(next(b_iter), end=' ') print(next(b_iter), end=' ') # 18 36 72
总结(有干货!!!)
这里总结一下,列表,元组,集合,字符串,迭代器,生成器等等 ( 绝不止这些哦 ) 都是可迭代的 ( iterable ), 它们的实例化对象就是 iterable 对象;迭代器与生成器的区别是:生成器是一个使用了 yield 的函数,这个函数可理解为返回了一个迭代器;而迭代器具有 iter() 与 next() 方法,iter() 返回的是一个迭代器,next() 返回的是迭代器对象具体元素的值,可通过 list() 、set() 等方法可以将迭代器对象的元素打印出来。
此外,yield 也有更灵活的用法,yield 后面可以不返回值或者说返回的是空值,有时候,为了让初学者感到社会的险恶,有些程序会用 yield from 结构简化生成器,代码是简单了,读起来却要略加思索了。yield from 用法如下
""" 格式: def generator(iterator) yield from iterator yield from + 可迭代对象 iter yield 按序依次返回 iter 中元素值 """ def fun(): yield from [1, 12, 23, 34, 45] for i in fun(): print(i, end=',') # 1,12,23,34,45,
-
by the way,我看见网上有人说迭代器相比于列表、集合等类型更省内存空间,于是我用 python3.7 试了试
list1 = [1, 12, 23, 34, 45] # 列表 iter1 = iter(list1) # 迭代器 def fun(lists): # 生成器 yield from iter(lists) # yield from lists gener1 = fun(list1) print(f'list1: {type(list1)}, {len(list1)}') # list1: <class 'list'> 5 print(f'iter1: {type(iter1)}, {len(iter1)}') # TypeError: object of type 'list_iterator' has no len() print(f'gener1: {type(gener1)}, {len(gener1)}') # TypeError: object of type 'generator' has no len()
陷入沉思……真的省了空间吗?我读书少,不太确定……
这条路不行,我们换条路:
def fun(list1): for i in list1: print(i,end=' ') yield list1 = [1, 12, 23, 34, 45] i = fun(list1) print(i) # 结果为:<generator object fun at 0x0000021F048E23C8>
并没有执行 fun() 中 for 循环的 print(),也就是说,整个函数并没有真正以我们认为的方式执行,实际上,调用 fun(list1) 不会执行 fun 函数,而是返回一个 iterable 对象!当我们开始迭代时, fun( list1) 才会真正执行:
for ele in fun(list1): print(ele, end='; ') #结果为:1 None; 12 None; 23 None; 34 None; 45 None;
-
两处 print() 中 end 不同是为了看清楚两处 print() 的执行先后顺序,从结果中可以分析出,fun() 中 print() 先于 for 循环内的 print() ,且由于 yield 后没有值可返回,ele 收到了 yield 返回的空值,输出 None,此后二者交替执行打印功能。
-
也就是说,只是调用生成器 fun() 时,是没有真正执行 fun() 的,只有当进入迭代时才会开始执行,而执行过程中的交替输出也说明 fun() 并不是一次性返回所有 list1 的元素,而是先执行一次
print(i,end=' ')
,然后执行到 yield 便中止,同时 yield 返回空值给 ele,让 for 中的print(ele, end='; ')
进行输出,然后进入下一次迭代,如此循环交替执行两个 print() 直至迭代终止。这个过程中 fun( list1) 所占空间确实是少于 list1 所占空间的,可能这样说还不严谨,因为本身 fun() 作为一个生成器对象也是要占内存空间的,但当 list1 足够长,含有 1000,10000…个元素时,fun( list1) 大小肯定是小于 list1的,这点大家应该不难理解。
差点忘了 list() 这一类捣蛋的家伙,不同于 for,while 正大光明的迭代,它们的执行过程中是隐含了迭代的,基于上面代码,我们略作调整:
def fun(list1): for i in list1: print(i, end=' ') yield list1 = [1, 12, 23, 34, 45] i = fun(list1) i = list(i) # i = set(i) print('\n+++++++++++++++') print(i) # 1 12 23 34 45 # +++++++++++++++ # [None, None, None, None, None]
这就充分说明 list(),set() 是 “隐式” 进行迭代的,同时也印证了上文 list() 与 next() 的冲突。有兴趣的同学可以自己去搜搜相关函数的实现。
-
lambda函数
简单lambda的使用
匿名函数lambda:是指一类无需定义标识符(函数名)的函数或子程序。
lambda 函数是一种匿名函数,格式为:
""" lambda 参数列表 : 表达式 参数列表可为空,既没有参数,多参数间用 ',' 隔开 表达式不能超过一个,即该表达式是可以在普通函数定义中一行内写下的 lambda 函数的返回值是是一个函数的地址,也就是函数对象。 """ a = lambda : print("1") # lambda 函数定义,并将lambda 函数返回的函数对象命名为 a # 注意区分 a 与 a() print(a) # 输出函数对象 # <function <lambda> at 0x000001EB61A680D8> print(type(a)) # <class 'function'> a() # 调用函数 # 1 print(a()) # 调用函数,并返回表达式的值,表达式 print("1") 的值为 None # 1 # None print(type(a())) # 1 # <class 'NoneType'>
可以这么理解:a 是一个lambda匿名函数的名字,而 a() 则代表执行lambda函数后返回的表达式的值。
aa = lambda : 1 # 表达式恒为 1 print(aa) print(aa()) # <function <lambda> at 0x0000014D7D6180D8> # 1 aaa = lambda x: x + 1 # 参数列表不再是空的,而是要求传入一个 x print(aaa) print(aaa(2)) # 传入参数 # <function <lambda> at 0x000001B55ACBA1F8> # 3 print((lambda y: y * 2)(3)) # 传入 3 给lambda函数并执行 # 6
lambd函数其实本质就是一个函数,普通函数怎么用,lambda函数也怎么用,实际上,任何lambda函数都可以改写为一个普通函数。对于一些简单易读的一次性使用的单行函数,改为lambda函数省去了那些格式化的 def…: return… ,使代码更加优雅。比如使用 map() 函数时:
aaa = lambda x: x + 1 def bbb(x): return x + 1 print(aaa) print(lambda x: x + 1) print(bbb) i = map(aaa, [1, 2, 3]) j = map(lambda x: x + 1, [1, 2, 3]) k = map(bbb, [1, 2, 3]) print(i) print(j) print(k) print(list(i)) print(list(j)) print(list(k)) # <function <lambda> at 0x000002C20A06E948> # <function <lambda> at 0x000002C20A06E8B8> # <function bbb at 0x000002C20A06E678> # <map object at 0x000002C20A22BE88> # <map object at 0x000002C20A2356C8> # <map object at 0x000002C20A235748> # [2, 3, 4] # [2, 3, 4] # [2, 3, 4]
- 这里注意输出的第一行与第二行分别对应的是两个不同的lambda函数,为什么是不同的呢?两个lambda函数从参数到表达式不是完全一样吗?但仔细看输出的第一第二行会发现,两个函数的位置并不一样,实际上,每次用lambda来定义匿名函数时,都会分配一块新的内存空间给函数,这例子中两个lambda函数功能虽一样,但却是处于不同空间的函数。
- 这里 map() 的第一个参数是一个函数 func,第二个参数是一个可迭代对象或者说是一个或多个序列,map() 作用是返回一个可迭代对象,即第4-6行输出中 map 对象 ( map object ) 是可迭代的。map() 功能是按序依次从第二个参数 ( 即那个序列 ) 中取出元素 e 传给第一个参数 func,并执行 func(e),执行结果作为返回的可迭代对象 map object 中的元素。
lambda与 iter() 居然可以这样用
终于到这块了,这里就是我写这篇文章的初衷了,我偶然间遇到了如下形式的函数定义:
def function(*args, **kwgs): yield from iter(lambda params: expression, sentinel)
第一眼看去,
???这是啥啊
通常的 iter() 在使用时只需传入一个可迭代的参数,如列表,集合等,这里的 iter() 怎么不一样?原来,iter() 函数的形式其实是这样的:
iter(object[, sentinel])
其中 [ ] 内的内容代表可以有选择地省略。
- object – 支持迭代的集合对象。
- sentinel – 如果传递了第二个参数,则参数 object 必须是一个可调用的对象(如,函数),此时,iter 创建了一个迭代器对象,每次调用这个迭代器对象的__next__()方法时,都会调用 object。如果__next__的返回值等于sentinel,则抛出StopIteration异常,否则返回下一个值。
首先明确一点,iter() 无论如何,返回的都是一个迭代器对象 iter01。而上面一段话换句话说就是,当我们想给 iter() 传两个参数时,第一个参数应该为 callable 对象,即可以调用的对象,函数可以被调用,所以函数属于 callable 对象,这里不妨假设传入的第一个参数是一个函数 func()。第二个参数 sentinel,它的类型和第一个参数 func() 的返回值相同,sentinel 的作用就是当 iter01 开始迭代时,
- 宏观角度:每迭代一次,就会调用一次 func(),这时将 func() 的返回值与 sentinel 相比较,若两者不相等,则将 func() 返回值传给 iter01 作为其元素;否则,停止迭代。
- 类内部实现角度:每一次迭代实际上会隐式调用 iter01 所属类 (
class callable_iterator
) 中定义的__next__()
方法。而每次调用这个迭代器对象的__next__()
方法时,都会调用 func() , 当 func() 返回值不等于 sentinel 时,__next__()
返回该值,否则,抛出 StopIteration 异常。
x = 0 def bbb(): global x # 声明使用 x 这个全局变量 x += 1 return x iter01 = iter(bbb, 10) print(iter01) for i in iter01: print(i, end=' ') # <callable_iterator object at 0x000001A41A96E688> # 1 2 3 4 5 6 7 8 9
- iter(object, sentinel) 返回的对象属于类
callable_iterator
- 就本人目前接触和使用到的 iter(object, sentinel),若 object 是函数时,该函数不需要传入参数;若 object 是可调用的类时,类的初始化
__init__()
也不需要传入参数。
了解了 iter() 的另一种用法,再回到本节开头的
def function(*args, **kwgs): yield from iter(lambda params: expression, sentinel)
涉及的知识点都讲的差不多了,下面便举个例子方便大家进一步理解与巩固。若能轻松看懂示例,相信大家对这部分的知识已经初步掌握了。IT技术深似海,我们一起学习一起加油!
"""EASY 模式""" def easy_func(): yield from iter(lambda :f.readline(), "") f = open('hello.txt', encoding='utf-8') for i in easy_func(): print(i, end='') f.close() # 这个程序可以按行输出整个 hello 文件 """HARD 模式""" class Iter1: def __iter__(self): self.num1 = 1 return Iter2(self.num1) class Iter2: def __init__(self, num): self.num2 = num def __next__(self): self.num2 *= 2 return self.num2 def last_func(a): yield from iter(lambda : next(aa), 16) aa = iter(Iter1()) # print(next(aa), end=' ') # print(next(aa), end=' ') # print(next(aa), end=' ') for i in last_func(aa): if i <= 1024: # 为什么加个if?把 iter() 第二个参数 16 改小就知道了 print(i, end=' ') else: break
觉得有收获的话,不妨点赞收藏哟,本人励志做一个没有水文的博主~
这篇关于忽略的细节之Python 迭代器与生成器,与lambda函数应用的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-11-02Python编程基础
- 2024-11-01Python 基础教程
- 2024-11-01用Python探索可解与不可解方程的问题
- 2024-11-01Python编程入门指南
- 2024-11-01Python编程基础知识
- 2024-11-01Python编程基础
- 2024-10-31Python基础入门:理解变量与数据类型
- 2024-10-30Python股票自动化交易资料详解与实战指南
- 2024-10-30Python入行:新手必读的Python编程入门指南
- 2024-10-30Python入行:初学者必备的编程指南