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