1-Python - 迭代器

一个shift引发的血案

开发在测试3000+的函数之后,觉得效率极其低下,这样就无法在规定的时间内完成任务。他就有了一个新的想法,就是拿到当前文件内的函数名,存在列表内,然后循环这个列表,列表内的每个元素都是函数名,那么把这个函数名放到装饰器内执行。这样就一劳永逸了,并单独写出了测试代码:

import time  
def timer(func):  
    def wrapper(*args, **kwargs):  
        start = time.time()  
        eval(func+'()')  
        print('function %s run time %s' % (func, time.time() - start))  
    return wrapper  
def foo():  
    time.sleep(0.2)  
    print('function foo')  
def bar():  
    time.sleep(0.1)  
    print('function bar')  
def f1():  
    time.sleep(0.1)  
    print('function f1')  
l = ['foo', 'bar', 'f1']  
 l = {'foo', 'bar', 'f1'}  
count = 0  
while count < len(l):  
    timer(l[count])()  # l[count]为一个个函数名
    count += 1  
''' 
while循环列表的结果: 
    function foo 
    function foo run time 0.2001662254333496 
    function bar 
    function bar run time 0.10074114799499512 
    function f1 
    function f1 run time 0.10059309005737305 
while循环集合的结果: 
    TypeError: 'set' object does not support indexing 
'''  
# l = ['foo', 'bar', 'f1']  
l = {'foo', 'bar', 'f1'}  
for i in l:  
    timer(i)()  
''' 
for循环列表和集合的结果一致(忽略时间戳的小数位的细微不同): 
    function foo 
    function foo run time 0.20093750953674316 
    function bar 
    function bar run time 0.10007715225219727 
    function f1 
    function f1 run time 0.10010862350463867 
 
'''  

当开发在测试上面这段代码的时候,用while循环元素为函数名的列表,运行无误。就把代码合并到线上的代码中去真正的执行测试任务。而不巧的是在上线的过程中,因为键盘的shift键不好使,开发手一抖把列表的中括号,写成了花括号,导致报错(第32行所示),报错原因是集合不支持索引。在分析原因的时候,用for循环分别执行了原来的列表和失手写成的集合,都能正常运行,那为什么for循环集合不报错?而while循环集合就报错。开发再次请教老大Alex。Alex一看代码,就冷冷的对他俩说道:"回去复习一下可迭代对象和迭代器再来找我",开发掩面走之。

可迭代对象

原来,Python为了给类似集合这种不支持索引的数据类型,却能够像有索引(如列表)一样方便取值,就为一些对象内置__iter__方法来摆脱对象对索引的依赖。即如果这个对象具有__iter__方法,则称为可迭代对象。那我们如何判断这个对象是否是可迭代对象呢?Python为此提供了两种方法来判断。

关于那两个函数的用法
dir(obj)  					# dir(obj) dir方法返回对象obj的所有方法  
isinstance(obj,classinfo)  	# isinstance(obj, classinfo)函数判断一个对象obj是否是一个已知的类型classinfo  
print(dir('123'))   
''' 
['__add__', '__class__', '__contains__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__getitem__', '__getnewargs__', '__gt__', '__hash__', '__init__', '__iter__', '__le__', '__len__', '__lt__', '__mod__', '__mul__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__rmod__', '__rmul__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'capitalize', 'casefold', 'center', 'count', 'encode', 'endswith', 'expandtabs', 'find', 'format', 'format_map', 'index', 'isalnum', 'isalpha', 'isdecimal', 'isdigit', 'isidentifier', 'islower', 'isnumeric', 'isprintable', 'isspace', 'istitle', 'isupper', 'join', 'ljust', 'lower', 'lstrip', 'maketrans', 'partition', 'replace', 'rfind', 'rindex', 'rjust', 'rpartition', 'rsplit', 'rstrip', 'split', 'splitlines', 'startswith', 'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill'] 
'''  
print(isinstance('123', str)) # True 
print(isinstance('123', list)) # False   

通过dir函数的打印结果,我们找到了返回的列表中的__iter__方法。这说明字符串为可迭代对象。而isinstance函数判断这个对象是否是指定类型,如第7行,字符串123为str类型,则返回True。而第8行,字符串不是list类型,则返回False。我们稍后会用到这个函数。接下来学习第一种判断对象是否为可迭代对象的方法。

# 方法1:采用dir函数判断  
print('str is iterable:', '__iter__' in dir('123'))  
print('int is iterable:', '__iter__' in dir(123))  
print('list is iterable:', '__iter__' in dir([1, 2]))  
print('set is iterable:', '__iter__' in dir({1, 2}))  
print('dict is iterable:', '__iter__' in dir({'a': 1}))  
print('tuple is iterable:', '__iter__' in dir((1, 2)))  
print('range is iterable:', '__iter__' in dir(range(10)))  
  
# 方法2:通过isinstance函数判断  
from collections import Iterable  
print('str is iterable:', isinstance('123', Iterable))  
print('int is iterable:', isinstance(123, Iterable))  
print('list is iterable:', isinstance([1, 2], Iterable))  
print('set is iterable:', isinstance({1, 2}, Iterable))  
print('dict is iterable:', isinstance({'a': 1}, Iterable))  
print('tuple is iterable:', isinstance((1, 2), Iterable))  
print('range is iterable:', isinstance(range(10), Iterable))  
''' 
    str is iterable: True 
    int is iterable: False 
    list is iterable: True 
    set is iterable: True 
    dict is iterable: True 
    tuple is iterable: True 
    range is iterable: True 
'''  

上例中,dir函数返回一个对象所有的方法,而我们通过成员测试符in来判断__iter__在不在dir(obj)中,来判断这个对象是否为可迭代对象。通过各自的打印结果,只有int返回了False。也就是说int为不可迭代对象。而isinstance函数则借助collections模块的Iterable类型来判断,如果一个对象是Iterable。返回True,否则返回False。而执行结果与dir函数执行结果一致。

我们通过上例可以得出常见的可迭代对象有str、list、set、dict、tuple、range。

迭代器

既然知道了可迭代对象,那什么是迭代器呢?

it = {1, 2, 3}  
it = it.__iter__()  
print(it)       				# <str_iterator object at 0x00913210>  
print(it.__next__())    		# 1  
print(it.__next__())    		# 2  
print(it.__next__())    		# 3  
# print(it.__next__())    	# StopIteration  

可迭代对象执行__iter__方法返回的结果称为迭代器(第3行),而迭代器又具有__next__方法。我们通过执行迭代器的__next__方法获取到了set中的每个元素。而当取值完毕,迭代器内值为空,就会抛出StopIteration错误提示(第7行),提示迭代取值完毕。

那么迭代器和可迭代对象有什么区别呢?

print('iterable have __iter__:', '__iter__' in dir('12'))          
print('iterable have __next__:', '__next__' in dir('12'))           
print('iterator have __iter__:', '__iter__' in dir('12'.__iter__()))
print('iterator have __next__:', '__next__' in dir('12'.__iter__()))
''' 
    iterable have __iter__: True 
    iterable have __next__: False 
    iterator have __iter__: True 
    iterator have __next__: True 
'''  

通过打印结果可以看到。可迭代对象只有__iter__方法,而迭代器则有__iter____next__两个方法。

print('str: %s' % '123'.__iter__())  
print('list: %s' % [1, 2].__iter__())  
print('set: %s' % {1, 2}.__iter__())  
print('dic: %s' % {'a': 1}.__iter__())  
print('tuple: %s' % (1, 2).__iter__())  
print('range: %s' % range(10).__iter__())  
''' 
    str: <str_iterator object at 0x013DD050> 
    list: <list_iterator object at 0x013DD050> 
   set: <set_iterator object at 0x01684468> 
   dic: <dict_keyiterator object at 0x013C5AB0> 
   tuple: <tuple_iterator object at 0x013DD050> 
   range: <range_iterator object at 0x014C9188> 
'''  

通过上面的例子,我们也可以发现,不同的可迭代对象返回不同类型的迭代器。

迭代器的特点是重复,下一次的重复是基于上一次结果。

使用迭代器的优点

  • 提供一种不依赖于索引的取值方式,迭代器通过__next__方法取值。

  • 惰性计算,节省内存空间。迭代器每执行__next__方法一次,则“动作”一次,返回一个元素。就像懒驴似的,我们踹一脚,这懒驴(迭代器)才走一步,不踹不动弹。

迭代器的缺点也很明显:

  • 取值不如索引方便。要每次执行__next__方法取值。

  • 迭代过程不可逆。也就是说这个懒驴(迭代器的迭代过程)走的是一条通往悬崖的路,每次执行__next__方法返回结果的同时都会向悬崖靠近一步。直到跳下悬崖(迭代完毕,抛出StopIteration异常)。所以说,迭代过程是无法回头的,只能一条路走到黑。

  • 无法获取迭代器的长度。因为可迭代对象通过__iter__方法返回的是迭代器(内存地址)。所以,无法获取这个迭代器内的元素有多少。

迭代器协议版本差异

关于迭代器的学习,我们都是在Python 3.x解释器环境下学习的。但众所周知,Python2.x和Python3.x是有些许区别的,现在通过Python 2.x解释器来了解下版本的不同之处。

Python 2.7.14 (v2.7.14:84471935ed, Sep 16 2017, 20:19:30) [MSC v.1500 32 bit (Intel)] on win32  
Type "help", "copyright", "credits" or "license" for more information.  
>>> s = 'abc'  
>>> s.__iter__()  
Traceback (most recent call last):  
  File "<stdin>", line 1, in <module>  
AttributeError: 'str' object has no attribute '__iter__'  
>>> s2 = iter('abc')  
>>> s2  
<iterator object at 0x03729FF0>  
>>> s2.next()  
'a'  

上例中第4行,可以发现,Python 2.x中字符串并没有__iter__方法,但我们可以通过使用iter函数返回迭代器,
iter函数将可迭代带对象返回为迭代器,此迭代器内的每一个元素都有一个next方法,我们在循环的时侯调用next方法(第11行),直到没有更多元素时,next方法会抛出StopIteration终止循环。
但需要注意上面的情况只是Python 2.x的字符串转为迭代器的独特情况。其它的可迭代对象都有__iter__方法可以返回迭代器。只是迭代器也同时没有__next__方法,只能通过调用next方法来取值。
我们来做总结。

  • 在Python 3.x中,按照我们之前的学习,通过__iter__方法返回迭代器,迭代器再执行__next__方法取值。
  • 在Python 2.x中,先说特殊的字符串,字符串没有__iter__方法,只能通过iter函数返回迭代器,再调用next方法取值。
  • 在Python 2.x中,除了字符串的其他的可迭代对象都有__iter__方法,并通过此方法返回迭代器。关键点:Python 2.x中所有的迭代器都没有__next__方法,只有__iter__方法。所以,Python 2.x中的迭代器都要通过next方法取值。
  • 为了避免记混出错,我们使用一个折中的方法,这个方法Python2.x和Python3.x兼容,那就是使用iter函数和next函数。
Python 2.7.14 (v2.7.14:84471935ed, Sep 16 2017, 20:19:30) [MSC v.1500 32 bit (Intel)] on win32  
Type "help", "copyright", "credits" or "license" for more information.  
>>> s1 = iter('ab')  
>>> s1  
<iterator object at 0x03735150>  
>>> next(s1)  
'a'  
>>> next(s1)  
'b'  
>>> next(s1)  
Traceback (most recent call last):  
  File "<stdin>", line 1, in <module>  
StopIteration  
>>> l1 = iter(['a', 'b'])  
>>> l1  
<listiterator object at 0x03735110>  
>>> next(l1)  
'a'  
>>> next(l1)  
'b'  
>>> next(l1)  
Traceback (most recent call last):  
  File "<stdin>", line 1, in <module>  
StopIteration  
  
Python 3.5.4 (v3.5.4:3f56838, Aug  8 2017, 02:07:06) [MSC v.1900 32 bit (Intel)] on win32  
Type "help", "copyright", "credits" or "license" for more information.  
>>> s1 = iter('ab')  
>>> s1  
<str_iterator object at 0x00C9BE70>  
>>> next(s1)  
'a'  
>>> next(s1)  
'b'  
>>> next(s1)  
Traceback (most recent call last):  
  File "<stdin>", line 1, in <module>  
StopIteration  
>>> l1 = iter(['a', 'b'])  
>>> l1  
<list_iterator object at 0x00C9BB50>  
>>> next(l1)  
'a'  
>>> next(l1)  
'b'  
>>> next(l1)  
Traceback (most recent call last):  
  File "<stdin>", line 1, in <module>  
StopIteration  

上例中,通过iter函数返回迭代器,next函数获取迭代器内的元素,通过StopIteration异常结束循环。
我们可以发现,Python 2.x和Python 3.x对于迭代器的实现是有些微差异,但也有共同的方法去解决这些差异。是因为两个解释器版本遵守的迭代器协议不同,也牵扯解释器在设计迭代器协议的内部机制不同。
注意:迭代器协议指对象需要提供next方法,它要么返回迭代中的下一项,直至元素为空引起一个StopIteration异常,终止循环。而可迭代对象则是实现了迭代器协议的对象。

class Reverse:  
    """Iterator for looping over a sequence backwards."""  
    def __init__(self, data):  
        self.data = data  
        self.index = len(data)  
  
    def __iter__(self):  
        return self  
  
   def next(self):  
       if self.index == 0:  
           raise StopIteration  
       self.index = self.index - 1  
       return self.data[self.index]  

可以从Python2.x解释器的源码中发现(首先我们忽略类,只要知道在类中定义的函数,称为方法),可迭代对象通过__iter__方法返回迭代器,而迭代器内的每个元素都有next方法,我们在循环时调用next方法取值,next方法也负责当迭代器内为空时,抛出异常终止循环。
简单来说,迭代的过程相当于有一坛子(容器)咸鸭蛋(元素),next一次,坛子抛出一个鸭蛋,而坛子内就少一个鸭蛋,当坛子里没了鸭蛋,next就抛出了StopIteration异常,提示循环该结束了——没鸭蛋了!

少年,你对for循环一无所知

我们通过集合出错的例子,来了解一下for循环的内部实现原理。

s = {1, 2, 3}  
for item in s:  
    print(item, end=' ')     # 1 2 3 
  
count = 0  
while count < len(s):  
    print(s[count])     # TypeError: 'set' object does not support indexing  
    count += 1  

首先,第3行的end='',意为用空格代替默认的换行。
for循环顺利的打印了set内的每个元素,而while循环却提示set对象不支持索引。既然如此,那么for循环是如何做到的呢?
其实在for循环的幕后,for语句在可迭代对象上调用iter函数,iter函数返回一个迭代器,for语句循环调用迭代器内的每个元素的next方法,当迭代器为空时,next方法会引发一个StopIteration异常。这个异常告诉for语句终止循环。
注意。for循环的对象必须是可迭代对象,如果这个对象不是可迭代对象,那么for循环的时候会报错。

x = 111  
for i in x:  
    print(i)    # TypeError: 'int' object is not iterable  

既然知道了for循环的内部原理,我们试着用while循环模拟for循环的过程:

def while_iterator(iterable_obj):  
   iterator_obj = iter(iterable_obj)   			# iter函数返回迭代器iterator_obj  
   while 1:  
       try:  
           print(next(iterator_obj), end=' ')   	# next方法返回每个元素  
       except StopIteration:  
           return  
while_iterator('123')                   			# 1 2 3  
while_iterator({'a', 'b', 'c'})         			# b c a   

首先解释,第4到第7行,我们用到异常处理中的try和except语句,用来捕获异常,并对异常作出我们想要实现的逻辑——退出循环并终止函数的执行。
我们通过上面的函数,用while循环实现了for循环的内部实现原理。而且是Python 2.x和Python 3.x兼容。


欢迎斧正,that's all
posted @ 2020-11-09 11:57  听雨危楼  阅读(371)  评论(0编辑  收藏  举报