• 猿创征文|Python迭代器、生成器、装饰器、函数闭包


    在这里插入图片描述

    欢迎关注博主 Mindtechnist 或加入【Linux C/C++/Python社区】一起探讨和分享Linux C/C++/Python/Shell编程、机器人技术、机器学习、机器视觉、嵌入式AI相关领域的知识和技术。



    专栏:Python从入门到实战


    1. 迭代器 iterator

    ☞迭代器协议

    迭代合递归

    • 递归:一层一层的调用,然后一层一层的返回,A调用B,B调用C,…,然后C返回给B,B返回给A;
    • 迭代:每次循环得到一个结果,并且都依赖于上一次的结果,迭代是一个重复的过程,每次重复即一次迭代,并且每次迭代的结果都是下一次迭代的初始值;

    迭代器协议与可迭代对象

    • 迭代器协议是指,对象必须提供一个next方法,执行该方法要么返回迭代中的下一项,要么就引起一个StopIteration异常来终止迭代,即只能往后走,不能往回倒退。
    • 可迭代对象是指实现了迭代器协议的对象,即对象内部提供了一个__iter__()方法。
    • 协议是一种约定,可迭代对象实现了迭代器协议,python内部工具比如for循环、sum/min/max等函数通过使用迭代器协议来访问对象。
    ☞Python中的for循环

    for循环的原理
    其实for循环的本质就是通过迭代器协议循环所有对象,需要说明的是,for循环的本质就是遵循迭代器协议去访问对象,也就是说for循环的对象都应该是可迭代对象。我们使用for循环可以遍历字符串、列表、元组、字典、集合、文件等等,但是这些数据类型内部并没有__next__()方法,这是为什么呢?实际上它们本身并不是可迭代对象,只不过是在for循环的时候,调用了它们内部的__iter__()方法,把它们变成了可迭代对象,然后for循环再去调用这些可迭代对象的__next__()方法去访问,并捕捉StopIteration异常来终止迭代。
    我们定义的字符串、列表、元组、字典、集合、文件等对象,内部都含有一个__iter__()方法,通过调用这个方法可以把对象变成可迭代对象,变成可迭代对象之后就可以使用__next__()方法了。for循环就是通过这个过程去迭代上面这些对象的,当迭代到对象最后一个元素的时候,会自动捕捉StopIteration异常停止迭代。

    #for i in list:  → it = list.__iter__() → it.__next__()
    #捕捉到异常StopIteration则停止迭代
    
    • 1
    • 2

    在这里插入图片描述

    for循环的实现
    对列表取值的方式:一是索引:l[0],二是迭代器:it = l.iter() 、it.next()。
    python中的for循环不是靠索引实现的,而是通过迭代器实现的。
    对列表、元组、字符串这种有序的序列进行遍历的方式有:一是通过for循环迭代,即迭代器;二是通过while循环即索引 while index 非序列类型(无序的)字典、集合、文件对象就不能通过索引去遍历了,只能通过for循环迭代,for循环基于迭代器协议提供了一个统一的可以遍历所有对象的方法,for循环总共做了三件事,首先,在遍历前调用对象的__iter__()方法把对象转换成一个迭代器,然后通过迭代器协议去实现循环访问也就是__next__()方法,最后捕捉StopIteration异常来结束循环。通过for循环实现了一个统一的迭代方法,不需要区分是否有索引/下标/有序,只要是含有__iter__()方法的对象,都是可以for循环的。注意,字典可迭代对象的__next__()方法返回的是字典的key值,所以for循环字典的时候默认是根据key值来的;文件的可迭代对象的__next__()方法是按照行去遍历,在for循环文件对象时,首先for循环根据文件对象转换一个iter_f,此时得到的是一个迭代器,每次执行iter_f.next()的时候只取了一行,也就是说每次循环只取了文件的一行内容放入内存,如果没有变量去保存这一行数据,那么执行完这次循环,这一块内存就立马被python释放了,然后进行下次循环,这样大大节省了内存提高了效率。

    #用while模拟for循环
    it_l = l.__iter__()
    while True:
    	try:
        	print(it_l.__next__())
    	except StopIteration:
        	break
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    python提供的内置方法next()方法,实际上也是在调用可迭代对象内置的it.next()方法。总结来说,可迭代对象就是迭代器、即遵循迭代器协议、即包含内置方法__iter__()

    2. 生成器 generator

    ☞什么是生成器

    生成器可以理解为一种数据类型,这种数据类型自动实现了迭代器协议,而其它的数据类型需要调用自己内置的__iter__()方法来实现,所以说,生成器就是可迭代对象。
    生成器分类及在python中的表现形式
    python有两种方式提供生成器:

    • 生成器函数:常规函数的定义,但是使用yield语句返回结果(常规函数使用return返回结果),yield语句一次返回一个结果,在每个结果中间挂起函数的状态以便下次从离开的地方重新执行。也就是说,只要是函数内部带有yield语句,那么这个函数得到的就是一个生成器。
    • 生成器表达式:类似与列表推导,但是生成器返回按需产生结果的一个对象,而不是一次构建一个结果列表。it = (‘string %s’ %i for i in range(10)),通过it.next()便可以在列表中取值了,该表达式生成的就是一个迭代器。

    生成器的优点
    python使用生成器对延迟操作提供了支持,所谓延迟操作就是指在需要的时候才产生结果,而不是立即产生结果。
    总结
    生成器是可迭代对象,它实现了延迟计算,节省内存,生成器的本质和其它数据类型一样,都实现了迭代器协议,只不过生成器附加了一个延迟计算来节省内存。

    ☞三元表达式
    ‘A’ if name == ‘name’ else ‘B’ # → 如果表达式结果为真,则返回’A’否则返回’B’
    
    #列表解析 → 生成一个列表
    [i for i in range(5)] #→ [0, 1, 2, 3, 4]
    
    str_list=[]
    for i in range(10)
    	str_list.append(‘string %s’ %i)
    print(str_list)
    # -----→ 相当于
    [‘string %s’ %i for i in range(10)]
    
    [‘string %s’ %i for i in range(10) if i > 5]  # → 三元表达式
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    ☞生成器表达式

    把列表解析的[]换成()就是生成器表达式,生成器表达式得到的是一个迭代器。相对于列表解析,生成器表达式更节省内存(占用很少内存),因为列表解析是一下子就把整个列表生成了,列表中的全部元素都放在内存中;而生成器表达式是通过__next__()方法取值,每次调用只取一个值,也就是每次只取一个值放到内存中,所以更节省内存。
    map、reduce、filter、sum、for、max、min等都是python中使用迭代器协议的例子。
    为了节省内存,我们可以在这些函数的参数是[]列表的时候,用()生成器表达式来代替,也就是说如果一个方法的参数如果是可迭代对象,那么就可以使用生成器表达式来作为它的参数。

    #比如说我们需要一个列表
    [i for i in range(100000000)] # 直接就得到这个列表
    
    #这个数据量非常大,非常耗时间耗内存,我们可以用生成器表达式来代替
    (i for i in range(100000000)) # 得到一个迭代器,内部有__next__()方法
    
    #生成器自动实现了迭代器协议
    it = (i for i in range(100000000))
    it. __next__()
    #注意,__next__()只能不停的去取下一个值,当某个值已经被__next__()取过了,那么我们就无法再次取到这个值了。生成器只能遍历一次。
    def test():
    	for i in range(4):
        	yield i
    t = test()
    t1 = (i for i in t)
    t2 = (i for i in t1)
    print(list(t1)) # 0 1 2 3
    print(list(t2)) # 空
    
    #生成器在产生的时候,不会进行任何操作,也无法知道列表里面都有什么
    #值,只有运行__next__()才能获取一个值,不运行__next__()就无法知道里
    #面有什么
    #t1和t2都是由生成器表达式生成的生成器,但是他们只是生成器
    #里面有什么是未知的(它们不是列表,只有执行next才会得到值)
    #当print函数中对t1和t2进行list操作的时候,也就是for循环遍历
    #此时,才会通过一次一次的__next__()不停的取值
    #list(t1)就相当于取t的值,list之后t1就已经被遍历完一次了
    #list(t2)就相当于取t1的值,t1已经被遍历过一次了,
    #所以只能取到list最后的元素后面的位置,也就是空
    #生成器只能被遍历一次,__next__()只能往下走,不能往回走
    #遍历不只是__next__(),list、for、sum等都是遍历
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    ☞生成器函数

    在函数内用yield来代替return,return在函数内只能执行一次,因为执行一次就返回出去了,即便写了多个return,那么后面的return语句也没有机会执行了,而yield语句可以执行多次。

    def func():
    	yield 1
    	yield 2
    	yield 3
    	yield 4
    gen = func()  #  得到一个generator
    print(gen.__next__()) # 打印1  此时函数func停留在yield 1处
    print(gen.__next__()) # 打印2  在yield 1的基础上执行
    print(gen.__next__()) # 打印3  在yield 2的基础上执行
    #迭代器每次都是在上一次执行结果的基础上执行并得到本次结果
    #实际上,可以把yield来理解为一个断点,每次执行__next__()就相当于
    #执行到下一个断点,并且执行完会停留在当前断点处
    def func():
    	print('begin')
    	print('call __next__ : 1')
    	yield 1
    	print('call __next__ : 2')
    	yield 2
    	print('call __next__ : 3')
    	yield 3
    	print('end')
    
    gen = func()  #仅仅是拿到了一个生成器,函数内的语句一句都不会执行
    print(gen.__next__()) 
    print(gen.__next__()) 
    print(gen.__next__())
    #yield的作用:返回值、保留程序运行状态
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    使用生成器函数的好处:通过生成器函数取值,每次取到一个值就可以立即对这个值操作,不用等待函数执行后续操作,比如说你获取了num1就可以立即操作num1,并且yield会保留运行状态,当你操作完num1并且需要num2的时候,通过__next__获取num2。如果是普通函数,你需要等到100个数num1-num100全部生成才能对num1进行操作。

    def get_num():
    	for i in range(100)
    		yield 'num%s' %i
    		
    it = get_num()
    ret1 = it.__next__()
    ret2 = it.__next__()
    #可以直接对it进行for循环
    for temp in it:
    	print(temp)
    #相当于对一个函数进行for循环
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    生成器函数的特点

    • 语法上和函数相似:生成器函数和常规函数都是使用def语句进行定义,区别在于生成器函数使用yield函数返回一个值,而常规函数使用return语句返回一个值。
    • 自动实现迭代器协议:对于生成器来说,python会自动实现迭代器协议,所以我们可以直接调用它的__next__()方法,并且没有值返回的时候,生成器会自动产生StopIteration异常。
    • 状态挂起:生成器使用yield语句返回一个值,yield语句挂起该生成器函数的状态,保留足够的信息,以便于之后从它离开的地方继续执行。
    • 生成器函数只能遍历一次。
    ☞生成器函数应用实例

    使用生成器函数来处理大量数据

    '''
    txt: 
    {'str':'str1', 'len':1} 
    {'str':'str2', 'len':3} 
    {'str':'str3', 'len':2} 
    ...
    '''
    def get_file_line():
    	with open('txt', 'r', encoding = 'utf-8') as f:
    	    for i in f:
    			yield i #每次返回文件的一行记录
    it = get_file_line()
    #print(it.__next__())-->it.__next__()得到的是一行字符串,而不是字典
    #得到的是文件一行记录 "{'str':'str1', 'len':1}"
    #要想把字符串中的字典提出来,使用函数eval
    # "{'str':'str1', 'len':1}" -->eval-->{'str':'str1', 'len':1}		
    print(it.__next__())
    dic = eval(it.__next__())
    print(type(dic)
    print(dic['len']) #取出'len'对应的value
    
    '''
    #如果想全部取出,可以使用for循环
    for i in it:
    	temp = eval(i)
    print(temp['len'])
    
    #按'len'求和,所有len的长度之和
    ret = sum(eval(i)['len'] for i in it)
    print(ret)
    ret2 = sum(eval(i)['len'] for i in it)
    print(ret2)
    #生成器对象不能迭代第二次,上面ret的时候,it已经被迭代完了
    #ret2的时候只能得到文件最后一行的记录,因为yeild就停留在这里
    #生成器只能for循环一次
    '''
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    生成器的.send()方法

    def producer():
    	print(‘first produce’)
    	first = yield 1 #send发送的值被yield接收并传递给first
    	print(‘second product’, first)
    	yield 2
    	print(‘third product’)
    	yield 3
    #def consumer():
        
    gen = producer()
    ret = gen.__next__()
    print(ret)
    gen.send(None)
    #gen.send(‘第一次yield完成’)
    #send的作用:1.传递一个值给当前yield 2.触发执行到下一个yield
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    生产者消费者模型

    #在一个进程中实现并行 → 协程
    def producer():
    	c = consumer()
    	c.__next__()
    	for i in range(10):
        	time.sleep(1)
        	c.send(i)
    def consumer():
    	print(‘consumer’)
    	while True:
        	temp = yield
    		time.sleep(1)
        	print(‘consumer %s’ %temp)
    producer()
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    3. 装饰器函数与函数闭包

    ☞装饰器

    什么是装饰器

    • 装饰器
      器的本质是一个函数,装饰是指修饰其他函数,为其他函数添加附加功能。
      装饰器两个原则:一是不修改被修饰函数的源代码;二是不修改被修饰函数的调用方式。 (必须遵循)
      装饰器 = 高阶函数 + 函数嵌套 + 闭包
    • 高阶函数
      高阶函数的定义主要有两个原则:一是函数接收的参数是一个函数名;二是函数的返回值是一个函数名。满足二者之一就是高阶函数。

    示例
    实现一个功能:统计函数foo的执行时间,并且不能修改foo的源代码,不能修改foo的调用方式。

    import time
    def foo():
    	time.sleep(3)
    	print(‘function foo’)
    def timer(func)
    	start_time = time.time()
    	func()
    	stop_time = time.time()
    	print(‘func run time %s’ %(stop_time – start_time))
    	return func
    #timer(foo) 修改了foo的调用方式,原调用方式为foo()
    foo = timer(foo)
    #再次强调:函数名是函数地址,函数名加括号才是函数调用
    foo() #这样会执行两次foo函数
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    函数嵌套
    在函数内部定义函数(在函数内部定义函数,而不是调用函数)

    def func1(arg):
    	print(‘func1’)
    	def func2(): #作用域在func1内,只能在func1内调用,是局部变量
        	print(‘func2 %s’ %arg)
        	arg = 100
        	def func3():
            	print(‘func3 %s’ %arg) #本层未找到,则去上一层寻找
    	print(locals()) #函数即变量 func2 arg
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    ☞函数闭包

    闭即封装(变量),包即一层,闭包即作用域。
    在这里插入图片描述
    实现装饰器
    实现一个功能:给test增加一个功能,统计函数test的执行时间,不修改test的源码,不改变test的调用方式

    import time
    def test():
    	time.sleep(3)
    	print(‘function test’)
    def timmer(func)
        def wrapper():
    		start_time = time.time()
    		func()
    		stop_time = time.time()
    		print(‘func run time %s’ %(stop_time – start_time))
    	return wrapper
    test = timmer(test)
    test()
    #缺点:调用test之前需要一个赋值操作test = timmer(test)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    改进,使用@语法糖

    @timmer #相当于test = timmer(test)
    def test():
    	time.sleep(3)
    	print(‘function test’)
    
    test()
    """
    只要把@timmer放在被装饰的函数前就行,它就相当于把被装饰函数的函数名test传递给timmer()并把返回值重新赋给test。
    """
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    改进1: 加上返回值,即被修饰函数有返回值

    import time
    def timmer(func)
        def wrapper():
    		start_time = time.time()
    		ret = func()
    		stop_time = time.time()
    		print(‘func run time %s’ %(stop_time – start_time))
    		return ret #返回test的返回值
    	return wrapper
    @temmer
    def test():
    	time.sleep(3)
    	print(‘function test’)
    	return ‘test return’
    
    ret = test() #表面上运行的是test实际上运行的是wrapper
    #要想得到test()函数的返回值,就应该把test的返回值加在wrapper
    #函数的return语句中
    print(ret)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    改进2: 给函数闭包加上参数,即被修饰函数有参数
    可变参数

    #   *args – 接收全部位置参数
    #   **kwargs – 接收关键字参数key=value
    def func(*args, **kwargs): #就相当于,把所有位置参数传给args组成一个元组,把所有关键字参数传给kwargs组成一个字典。
    
    • 1
    • 2
    • 3
    import time
    def timmer(func)
        def wrapper(*args, **kwargs): #可变参数
    		start_time = time.time()
    		ret = func(*args, **kwargs)
    		stop_time = time.time()
    		print('func run time %s' %(stop_time – start_time))
    		return ret #返回test的返回值
    	return wrapper
    @temmer
    def test(name, age):
    	time.sleep(3)
    	print('function test [%s - %s]' %(name, age))
    	return 'test return'
    
    ret = test('su', age = 18)
    """
    wrapper(*args, **kwargs): 接收到的参数
    args – ('su')
    kwargs – {'age':18}
    所以,当wrapper传参给test的时候,必须是*('su') **{'age':18}才行,*就是把列表里面的东西拿出来
    """
    print(ret)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    为其他函数做装饰

    @temmer
    def test2(name, age, gender):
    	time.sleep(3)
    	print('function test2 [%s - %s - %s]' %(name, age, gender))
    	return 'test return'
    ret = test2('su', 18, 'girl')
    # ret = test2(*('su', 18), **{'gender':'girl'}) #传参方式2
    
    """
    解压序列:
    test2(name, age, gender)  (*(‘su’, 18), **{‘gender’:‘girl’})
    name, age = (‘su’, 18), gender = ‘girl’
    一一对应去赋值
    比如 a, b, c = (1, 2, 3) #a=1, b=2, c=3
    a, b, c = (1, 2, 3, 4) #err
    """
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    序列解压
    a, b, c = ‘hel’
    a, b, c = (1, 2, 3)
    a, b, c = [1, 2, 3]
    只要后面是一个序列就行,并且必须要一一对应。
    如果有一个很长很长的序列li,我们只想取出该序列的第一个和最后一个元素,应该怎么办呢?
    first, *_, last = li ,这样就可以可,*表示中间所有的元素组成的子序列
    first, *mid, last = li , mid = [ ] , 对li去头去尾后的子序列
    交换a, b的值可以直接a, b = b, a

    有参装饰器
    在原来的装饰器外面再套一层函数,根据参数可以进行一些逻辑判断。

    def timmer_type(type = '1'):
    def timmer(func):
        #原来的逻辑不变,可以加一些判断
        if type == '1'
            print('type1')
            #逻辑1
        else:
            print('other')
            #其他逻辑
    return timmer
    @timmer_type(type = '1') # timmer = timmer_type(type = '1')
    def test():
        print('test')
    """
    这里的@timmer_type(type = '1') 就相当于 
     timmer = timmer_type(type = '1') 也就相当于 
    @timmer() 只不过在timmer的基础上增加了一个外层函数 
    通过这个外层函数可以传入参数,并在内部根据参数增加一些逻辑
    """
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    举例

    def auth(driver='file'):
        def auth2(func):
            def wrapper(*args,**kwargs):
                name=input("user: ")
                pwd=input("pwd: ")
    
                if driver == 'file':
                    if name == 'egon' and pwd == '123':
                        print('login successful')
                        res=func(*args,**kwargs)
                        return res
                elif driver == 'ldap':
                    print('ldap')
            return wrapper
        return auth2
    
    @auth(driver='file')
    def foo(name):
        print(name)
    
    foo('egon')
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    在这里插入图片描述
    在这里插入图片描述


  • 相关阅读:
    L2-026 小字辈 - java
    Flink系列之Flink中Window原理及实践
    js正则表达式
    中台架构究竟是什么?为什么这么火?通过阿里中台架构实战让你的架构思维有所成长
    有了PMP证书,还用考CSPM吗?
    隐私计算FATE-离线预测
    华为OD机试 - 最远足迹(2022Q4 100分)
    【Python3】random.choices 权重随机选取 详解
    6.【刷爆LeetCode】电话号码的字母组合(多方法、多思路)
    使用Jenkins自动化部署项目
  • 原文地址:https://blog.csdn.net/qq_43471489/article/details/127579675