Python教程

忽略的细节之Python 迭代器与生成器,与lambda函数应用

本文主要是介绍忽略的细节之Python 迭代器与生成器,与lambda函数应用,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

忽略的细节之Python 迭代器与生成器,与lambda函数应用

简介

本文前半部分是迭代器与生成器内容,尽可能地讲得详细,并指出了几处易忽略的细节;后半部分是迭代器结合 lambda 函数的使用,纯 lambda部分较简略。
ps:总结中有干货哦

目录

  • 忽略的细节之Python 迭代器与生成器,与lambda函数应用
    • 简介
    • 前言
    • 迭代器与生成器
      • 迭代器
      • 生成器
      • 类作为迭代器使用
    • 总结(有干货!!!)
    • lambda函数
      • 简单lambda的使用
      • lambda与 iter() 居然可以这样用

前言

很奇怪为什么把这两块内容放一起是吗?因为,我懒。

最近有接触到涉及这两块的相关内容,有些遗忘,于是重新查看了一些相关文档,把自己的心得体会分享一下,也顺便方便自己以后查阅,毕竟自己写的东西才是最香的。

进入正题,大家清楚以下短短两行代码涉及的各个知识点与细节吗?

def function(*args, **kwgs):
	yield from iter(lambda params: expression, sentinel)

如果完全清楚,您可以翻篇了,这篇文章可能帮不到您,感谢您的光临!!

如果不是很理解,那么看完这篇文章,相信大家一定能有所收获。里面的细节值得大家体会。

对于上述问题,本着一点点地抽丝剥茧的原则,本人尽量逐个讲清其中的各个知识点。

先简单解释一下里面的各个参数:

  1. *args, **kwgs:参数个数不确定时,使用 *args, **kwgs 代表函数可能传递的参数列表。*args 没有key值,**kwargs有key值。
  2. params:由 *args, **kwgs 得到的一组参数,或者为空
  3. expression:单个表达式
  4. 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 开始迭代时,

  1. 宏观角度:每迭代一次,就会调用一次 func(),这时将 func() 的返回值与 sentinel 相比较,若两者不相等,则将 func() 返回值传给 iter01 作为其元素;否则,停止迭代。
  2. 类内部实现角度:每一次迭代实际上会隐式调用 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函数应用的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!