当前位置: 首页 > 科技观察

全面理解Python可迭代对象、迭代器、生成器

时间:2023-03-13 02:42:21 科技观察

在理解Python数据结构、容器、迭代器、迭代器、生成器、列表/集合/字典推导(list、set、dict理解)的诸多概念混杂在一起,难免让人初学者迷茫。我将用一篇文章来尝试阐明这些概念以及它们之间的关系。容器(container)容器是一种将多个元素组织在一起的数据结构。容器中的元素可以一个一个的迭代获取。您可以使用in和notin关键字来确定元素是否包含在容器中。通常这种数据结构将所有元素都存储在内存中(也有一些特殊情况不是所有元素都放在内存中,比如迭代器和生成器对象)。在Python中,常见的容器对象有:list,deque,....set,frozensets,....dict,defaultdict,OrderedDict,Counter,....tuple,namedtuple,...str容器比较容易理解,因为你可以把它想象成一个盒子,一个房子,一个柜子,什么东西都可以装。从技术角度来说,当它可以用来询问一个元素是否包含在其中时,那么这个对象就可以认为是一个容器,比如list、set、tuples都是容器对象:>>>assert1in[1,2,3]#lists>>>assert4notin[1,2,3]>>>assert1in{1,2,3}#sets>>>assert4notin{1,2,3}>>>assert1in(1,2,3)#tuples>>>assert4notin(1,2,3)询问一个元素是否在dict中,中间键为dict:>>>d={1:'foo',2:'bar',3:'qux'}>>>assert1ind>>>assert'foo'notind#'foo'isnotanelementindict询问子串是否在string中:>>>s='foobar'>>>assert'b'ins>>>assert'x'notins>>>assert'foo'ins虽然大多数容器都提供了一些方法来获取其中的每个元素,但这并不是容器本身提供的能力,而是给出containerthis当然不是所有的容器都是可迭代的,比如:Bloomfilter,虽然Bloomfilter可以用来检测一个元素是否包含在容器中,但是它不能从th中获取每一个值e容器,因为Bloomfilter根本不把元素存储在容器中,而是通过hash函数把元素映射成一个值,存储在数组中。可迭代对象(iterable)刚才说了,很多容器都是可迭代对象,还有更多的对象也是可迭代对象,比如openfiles,sockets等等。但是任何可以返回迭代器的对象都可以称为可迭代对象。听起来可能有点乱,没关系,我们先来看一个例子:>>>x=[1,2,3]>>>y=iter(x)>>>z=iter(x)>>>next(y)1>>>next(y)2>>>next(z)1>>>type(x)>>>type(y)这里的x是一个可迭代对象,可迭代对象是容器之类的通俗称呼,不是具体的数据类型,list是可迭代对象,dict是可迭代对象,set也是可迭代对象。y和z是两个独立的迭代器,迭代器内部持有一个状态,用于记录当前迭代的位置,以便在下一次迭代中获取正确的元素。迭代器有特定的迭代器类型,如list_iterator、set_iterator。可迭代对象实现了__iter__方法,该方法返回一个迭代器对象。运行代码时:x=[1,2,3]foreleminx:...实际执行是:反编译代码,可以看到解释器显式调用了GET_ITER指令,相当于调用了iter(x),FOR_ITER指令是调用next()方法不断获取迭代器中的下一个元素,但是从指令中是看不出来的,因为它已经被解释器优化过了。>>>importdis>>>x=[1,2,3]>>>dis.dis('for_inx:pass')10SETUP_LOOP14(to17)3LOAD_NAME0(x)6GET_ITER>>7FOR_ITER6(to16)10STORE_NAME1(_)13JUMP_ABSOLUTE7>>16POP_BLOCK>>17LOAD_CONST0(None)20RETURN_VALUE迭代器(iterator)那么什么是迭代器呢?它是一个有状态的对象,可以在调用next()方法时返回容器中的下一个值,任何实现__iter__和__next__()(python2中实现next()的对象)都是迭代器。__iter__返回迭代器本身,而__next__返回容器中的下一个值。如果容器中没有更多元素,则抛出StopIteration异常,而不管它们是如何实现的。因此,迭代器是一个实现工厂模式的对象,它会在您每次请求下一个值时返回给您。关于迭代器的例子有很多,例如,itertools函数返回迭代器对象。生成***序列:>>>fromitertoolsimportcount>>>counter=count(start=13)>>>next(counter)13>>>next(counter)14从有限序列生成***序列:>>>fromitertoolsimportcycle>>>colors=cycle(['red','white','blue'])>>>next(colors)'red'>>>next(colors)'white'>>>next(colors)'blue'>>>next(colors)'red'从***的序列生成有限序列:>>>fromitertoolsimportislice>>>colors=cycle(['red','white','blue'])#infinite>>>limited=islice(colors,0,4)#finite>>>forxinlimited:...print(x)redwhitebluered为了更直观的体验迭代器内部的执行过程,我们自定义一个迭代器来使用以Bonacci序列为例:=Fib()>>>list(islice(f,0,10))[1,1,2,3,5,8,13,21,34,55]Fib既是一个可迭代对象(因为它实现了__iter__方法)和一个迭代器(因为__next__方法实现)。实例变量prev和curruser维护迭代器内部的状态。每次调用next()方法时做两件事:为下次调用next()方法修改状态,为当前调用生成返回结果迭代器就像一个惰性工厂,直到有人给它需要将生成的值返回,不被调用时处于休眠状态等待下一次调用。生成器(generator)生成器是Python语言中最吸引人的特性之一。生成器其实就是一个特殊的迭代器,只不过这个迭代器更加优雅。它不再需要像上面的类那样写__iter__()和__next__()方法,只需要一个yield关键字。一个生成器一定是一个迭代器(反之亦然),所以任何一个生成器也是以懒加载的方式生成值的。使用生成器实现斐波那契数列的例子是:defib():prev,curr=0,1whileTrue:yieldcurrprev,curr=curr,curr+prev>>>f=fib()>>>list(islice(f,0,10))[1,1,2,3,5,8,13,21,34,55]fib是一个普通的python函数,它的特殊之处在于函数体中没有return关键字,function的返回值是一个生成器对象。当f=fib()的执行返回一个生成器对象时,此时并不会执行函数体中的代码,只有在显式或隐式调用next时才会真正执行里面的代码。生成器是Python中一个非常强大的编程结构,可以用更少的中间变量编写流式代码。另外,它相对于其他容器对象可以节省内存和CPU,当然也可以用更少的代码实现类似的功能。现在您可以手动重构您的代码,但如果您看到类似以下内容:defsomething():result=[]for...in...:result.append(x)returnresult可以替换为生成器函数:defiter_something():for...in...:yieldxgeneratorexpression(generatorexpression)生成器表达式是列表下推的生成器版本,看起来像列表理解,但它返回的是生成器对象而不是列表对象。>>>a=(x*xforxinrange(10))>>>aat0x401f08>>>>sum(a)285摘要容器是元素的集合,str,list,set,dict,file,sockets对象可以看作是容器,容器是可以迭代的(用在for、while等中),所以称为可迭代对象。可迭代对象实现了__iter__方法,该方法返回一个迭代器对象。迭代器持有一个内部状态字段,用于记录下一次迭代的返回值。它实现了__next__和__iter__方法。迭代器不会一次性将所有元素加载到内存中,而是只在需要时才生成返回结果。生成器是一种特殊的迭代器,其返回值不是返回而是产生。参考链接:https://docs.python.org/2/library/stdtypes.html#iterator-types