PHP的yield是个什么玩意(一)

其实,我并不是因为迭代或者生成器或者研究PHP手册才认识的yield,要不是协程,我到现在也不知道PHP中还有yield这么个鬼东西。人家这个东西是从PHP 5.5就开始引入了,官方名称叫做生成器。你要说为什么5.5年代的东西,现在才拿出来。我还想问你哟,PHP 5.3就有了的namespace为毛到最近这几年才开始正式投产。那么,问题来了,这东西到底是有何用?先来感受一个问题,给你100Kb的内存(是的,你没有看错,就是100Kb),然后让你迭代输出一个从1开始一直到10000的数组,步进为1。愈先迭代数组,必先创造数组。所以,脑门一拍,代码一坨如下:<?php$start_mem = memory_get_usage();$arr = range( 1, 10000 );foreach( $arr as $item ){ //echo $item.’,’;}$end_mem = memory_get_usage();echo " use mem : “. ( $end_mem - $start_mem ) .‘bytes’.PHP_EOL;一顿操作猛如虎,运行一下成绩1-5,你们感受一下:528440bytes,约莫就是528Kb,几乎是100Kb的五倍了,妈的这日子没法过了。毕竟你们也知道,最近内存价格确实贵,国家也在号召低碳节能减排,你多耗费5倍内存,就意味着多排放5倍的二氧化碳,就意味着要为多用的内存多花钱贡献给棒子… …你想想,那可是棒子。人都是被逼出来的,于是yield可以来救场了,大概代码如下,注意看操作:<?php$start_mem = memory_get_usage();function yield_range( $start, $end ){ while( $start <= $end ){ $start++; yield $start; }}foreach( yield_range( 0, 9999 ) as $item ){ echo $item.’,’;}$end_mem = memory_get_usage();echo " use mem : “. ( $end_mem - $start_mem ) .‘bytes’.PHP_EOL;运行一下,你们感受一下:首先,我们观察一下yield_range这个函数跟普通函数不一样的地方,就是普通函数往往都是使用return来返回结果,而这个中则是yield。其次是普通函数中return只能返回一次,这个yield能返回好多次。那么,我们来分析一波儿这个神奇的yield_range函数。这个yield关键字到底返回的是什么?我们简单看一下:<?phpfunction yield_range( $start, $end ){ while( $start <= $end ){ $start++; yield $start; }}$rs = yield_range( 1, 100 );var_dump( $rs );/object(Generator)#1 (0) {}/yield返回的是一个叫做Generator(中文名就是生成器)的object对象,而这个生成器是实现了Iterator接口(至于Iterator接口,你们去PHP手册上搜索吧)。所以,既然实现了Iterator接口(也正是因为如此,这个东西可以使用foreach进行迭代,明白了吧?),所以可以有如下代码:<?phpfunction yield_range( $start, $end ){ while( $start <= $end ){ yield $start; $start++; }}$generator = yield_range( 1, 10 );// valid() current() next() 都是Iterator接口中的方法while( $generator->valid() ){ echo $generator->current().PHP_EOL; $generator->next();}运行结果如下所示:重点来了:这个yield_range函数似乎能够记住它上一次运行到哪儿了,上一次运行的结果是什么,然后紧接着在下一次运行的时候继续从上次终止的地方继续开始。这不是普通的PHP函数可以做得到的!我们知道,操作系统在调度进程的时候,会触发一个叫做“进程上下文切换”的概念。比如CPU从进程A调度给进程B了,那么当再次从进程B调度给进程A的时候,当初进程A运行到哪儿了、临时的数据结果是什么都是需要被还原的,不然,一切都要从头,那就要出大问题了。而,这个yield关键字,似乎在用户态(非系统内核级)就可以实现这个概念。所以说,用yield搞迭代,怕是真的很没出息的一件事,它能做的太多。紧接着,我们需要认识一个生成器对象的一个方法,叫做send,简单看下下面这坨代码:<?phpfunction yield_range( $start, $end ){ while( $start <= $end ){ $ret = yield $start; $start++; echo “yield receive : “.$ret.PHP_EOL; }}$generator = yield_range( 1, 10 );$generator->send( $generator->current() * 10 );运行结果如图所示:send方法可以修改yield的返回值,但是,你也不能想当然,比如下面这坨代码,你们以为运行结果是什么样呢?<?phpfunction yield_range( $start, $end ){ while( $start <= $end ){ $ret = yield $start; $start++; echo “yield receive : “.$ret.PHP_EOL; }}$generator = yield_range( 1, 10 );foreach( $generator as $item ){ $generator->send( $generator->current() * 10 );}本来以为运行结果是类似于这样的:<?phpyield receive : 10yield receive : 20yield receive : 30yield receive : 40yield receive : 50yield receive : 60yield receive : 70yield receive : 80yield receive : 90yield receive : 100然而,唯物主义告诉我们:结果是打脸的,你们感受一下:原因是什么呢?原因是当你在外部向yield发送send的时候,会自动触发一次next,自己动手试下吧。最近开了一个微信公众号,所有文章都在这里(手贱弄成服务号了) ...

March 11, 2019 · 1 min · jiezi

填坑之PHP的yield和协程在一起的日子里(二)

首先是,这是我第一次把公众号文章复制粘贴到sf.gg来。其次是,很久很久之前,我挖了一个yield的一个坑,自己挖的坑自己填,不然迟早会把自己埋掉。最后是,如果想看之前那个坑,请发送“yield”给文章末尾的公众号,我开通了高大上的自动回复功能,稀罕地不得了!PS:那篇文章中在最后我犯了一个错误,误下了一个结论:foreach中不能使用send并猜测这是PHP的bug,实际上并不是,真实的原因粗暴简单的理解就是send会让生成器继续执行一次导致。这件事情告诉我们:除了装逼之外,甩锅也是有打脸风险的那篇坑里,内容和你能在百毒上搜索到的大多数文章都是差不多的,不过我那篇坑标题起得好:《yield是个什么玩意(上)》,也就是暗示大家还有下篇,所以起标题也是需要一定技术含量的。我坚信,在座的各位辣鸡在看完上篇坑文后最想说的注定是泰迪熊这句话(这是文化属性,不以各位的意志而转移):回到今天主旨上来,强调几点:虽然文章标题中有“yield和协程”这样的关键字,但实际上yield并不是协程,看起来有不少人直接将yield和协程划了等号。yield的本质是生成器,英文名字叫做Generator。yield只能用在function中,但用了yield就已经不是传统意义上的function了,同时如果你企图在function之外的其他地方用yield,你会被打脸。yield的最重要作用就是:自己中断一坨代码的执行,然后主动让出CPU控制权给路人甲;然后又能通过一些方式从刚才中断的地方恢复运行。这个就比较屌了,假如你请求了一个费时10s的服务器API,此时是可以让出CPU给路人甲。粗暴地说上面的过程就算是协程的基本概念。多线程和多进程都是操作系统参与的调度,而协程是用户自主实现的调度,协程的关键点实际上是“用户层实现自主调度”,大概有“翻身农奴把歌唱”的意思。下面我通过一坨代码来体会一把“翻身农奴”,你们感受一下:<?phpfunction gen1() { for( $i = 1; $i <= 10; $i++ ) { echo “GEN1 : {$i}".PHP_EOL; // sleep没啥意思,主要就是运行时候给你一种切实的调度感,你懂么 // 就是那种“你看!你看!尼玛,我调度了!卧槽” sleep( 1 ); // 这句很关键,表示自己主动让出CPU,我不下地狱谁下地狱 yield; }}function gen2() { for( $i = 1; $i <= 10; $i++ ) { echo “GEN2 : {$i}".PHP_EOL; // sleep没啥意思,主要就是运行时候给你一种切实的调度感,你懂么 // 就是那种“你看!你看!尼玛,我调度了!卧槽” sleep( 1 ); // 这句很关键,表示自己主动让出CPU,我不下地狱谁下地狱 yield; }}$task1 = gen1();$task2 = gen2();while( true ) { // 首先我运行task1,然后task1主动下了地狱 echo $task1->current(); // 这会儿我可以让task2介入进来了 echo $task2->current(); // task1恢复中断 $task1->next(); // task2恢复中断 $task2->next();}上面代码执行结果如下图:虽然我话都说到这里了,但是肯定还是有人get不到“所以,到底发生了什么?”。你要知道,如果function gen1和function gen2中没有yield,而是普通函数,你是无法中断其中的for循环的,诸如下面这样的代码:<?phpfunction gen1() { for( $i = 1; $i <= 10; $i++ ) { echo “GEN1 : {$i}".PHP_EOL; sleep( 1 ); }}function gen2() { for( $i = 1; $i <= 10; $i++ ) { echo “GEN2 : {$i}".PHP_EOL; }}gen1();gen2();// 看这里,看这里,看这里!// 上面的代码一旦运行,一定是先运行完gen1函数中的for循环// 其次才能运行完gen2函数中的for循环,绝对不会出现// gen1和gen2交叉运行这种情况我似乎已然精通了yield写到这里后我也开始蹩了,和以往的憋了三天蹦不出来个屁有所不同,我这次蹩出了一个比较典型的应用场景:curl。下面我们基于上面那坨辣鸡代码将gen1修改为一个耗时curl网络请求,gen2将向一个文本文件中写内容,我们的目的就是在耗时的curl开始后主动让出CPU,让gen2去写文件,以实现CPU的最大化利用。<?php$ch1 = curl_init();// 这个地址中的php,我故意sleep了5秒钟,然后输出一坨jsoncurl_setopt( $ch1, CURLOPT_URL, “http://www.selfctrler.com/index.php/test/test1" );curl_setopt( $ch1, CURLOPT_HEADER, 0 );$mh = curl_multi_init();curl_multi_add_handle( $mh, $ch1 );function gen1( $mh, $ch1 ) { do { $mrc = curl_multi_exec( $mh, $running ); // 请求发出后,让出cpu yield; } while( $running > 0 ); $ret = curl_multi_getcontent( $ch1 ); echo $ret.PHP_EOL; return false;}function gen2() { for ( $i = 1; $i <= 10; $i++ ) { echo “gen2 : {$i}".PHP_EOL; file_put_contents( “./yield.log”, “gen2”.$i, FILE_APPEND ); yield; }}$gen1 = gen1( $mh, $ch1 );$gen2 = gen2();while( true ) { echo $gen1->current(); echo $gen2->current(); $gen1->next(); $gen2->next();}上面的代码,运行以后,我们再等待curl发起请求的5秒钟内,同时可以完成文件写入功能,如果换做平时的PHP程序,就只能是先阻塞等待curl拿到结果后才能完成文件写入。文章太长,就像“老太太的裹脚布一样,又臭又长”,所以,最后再对代码做个极小幅度的改动就收尾不写了!<?php$ch1 = curl_init();// 这个地址中的php,我故意sleep了5秒钟,然后输出一坨jsoncurl_setopt( $ch1, CURLOPT_URL, “http://www.selfctrler.com/index.php/test/test1" );curl_setopt( $ch1, CURLOPT_HEADER, 0 );$mh = curl_multi_init();curl_multi_add_handle( $mh, $ch1 );function gen1( $mh, $ch1 ) { do { $mrc = curl_multi_exec( $mh, $running ); // 请求发出后,让出cpu $rs = yield; echo “外部发送数据{$rs}".PHP_EOL; } while( $running > 0 ); $ret = curl_multi_getcontent( $ch1 ); echo $ret.PHP_EOL; return false;}function gen2() { for ( $i = 1; $i <= 10; $i++ ) { echo “gen2 : {$i}".PHP_EOL; file_put_contents( “./yield.log”, “gen2”.$i, FILE_APPEND ); $rs = yield; echo “外部发送数据{$rs}".PHP_EOL; }}$gen1 = gen1( $mh, $ch1 );$gen2 = gen2();while( true ) { echo $gen1->current(); echo $gen2->current(); $gen1->send(“gen1”); $gen2->send(“gen2”);}我们修改了内容:将$gen1->next()修改成了$gen1->send(“gen1”)在function gen1中yield有了返回值,并且将返回值打印出来这件事情告诉我们:yield和send,是可以双向通信的,同时告诉我们send可以用来恢复原来中断的代码,而且在恢复中断的同时可以携带信息回去。写到这里,你是不是觉得这玩意的可利用价值是不是比原来高点儿了?我知道,有人肯定叨叨了:“老李,你代码特么写的真是辣鸡啊!你之前保证过了的 — 只在公司生产环境写辣鸡代码的。可你看看你这辣鸡光环到笼罩都到demo里了,你连demo都不放过了!你怎么说?!”。兄dei,“又不是不能用”。而且我告诉你,上面这点儿curl demo来讲明白yield还是不够的,后面还有两三篇yield呢,照样是烂代码恶心死你,爱看不看。我劝你心放宽,你想想你这么烂的代码都经历了,还有什么不能经历的?文章最后补个小故事:其实yield是PHP 5.5就已经添加进来了,这个模块的作者叫做Nikita Popov,网络上的名称是Nikic。我们知道PHP7这一代主力是惠新宸,下一代PHP主力就是Nikic了。早在2012年,Nikic就发表了一篇关于PHP yield多任务的文章,链接我贴出来大家共赏一下 — http://nikic.github.io/2012/1…最近开了一个微信公众号,所有文章都在这里(手贱弄成服务号了) ...

March 11, 2019 · 2 min · jiezi

Python高级语法之:一篇文章了解yield与Generator生成器

Python高级语法中,由一个yield关键词生成的generator生成器,是精髓中的精髓。它虽然比装饰器、魔法方法更难懂,但是它强大到我们难以想象的地步:小到简单的for loop循环,大到代替多线程做服务器的高并发处理,都可以基于yield来实现。理解yield:代替return的yield简单来说,yield是代替return的另一种方案:return就像人只有一辈子,一个函数一旦return,它的生命就结束了yield就像有“第二人生”、“第三人生”甚至轮回转世一样,函数不但能返回值,“重生”以后还能再接着“上辈子”的记忆继续返回值我的定义:yield在循环中代替return,每次循环返回一次值,而不是全部循环完了才返回值。yield怎么念?return我们念“返回xx值”,我建议:yield可以更形象的念为"呕吐出xx值“,每次呕一点。一般我们进行循环迭代的时候,都必须等待循环结束后才return结果。数量小的时候还行,但是如果循环次数上百万?上亿?我们要等多久?如果循环中不涉及I/O还行,但是如果涉及I/O堵塞,一个堵几秒,后边几百万个客户等着呢,银行柜台还能不能下班了?所以这里肯定是要并行处理的。除了传统的多线程多进程外,我们还可以选择Generator生成器,也就是由yield代替return,每次循环都返回值,而不是全部循环完了才返回结果。这样做的好处就是——极大的节省了内存。如果用return,那么循环中的所有数据都要不断累计到内存里直到循环结束,这个不友好。而yield则是一次一次的返回结果,就不会在内存里累加了。所以数据量越大,优势就越明显。有多明显?如果做一百万的简单数字计算,普通的for loop return会增加300MB+的内存占用!而用yield一次一次返回,增加的内存占用几乎为0MB!yield的位置既然yield不是全部循环完了再返回,而是循环中每次都返回,所以位置自然不是在for loop之后,而是在loop之中。先来看一般的for loop返回:def square(numbers): result = [] for n in numbers: result.append( n2 ) return result #在for之外再来看看yield怎么做:def square(numbers): for n in numbers: yield n2 #在for之中可以看到,yield在for loop之中,且函数完全不需要写return返回。这时候如果你print( square([1,2,3]) )得到的就不是直接的结果,而是一个<generator object>。如果要使用,就必须一次一次的next(…)来获取下一个值:>>> results = square( [1,2,3] )>>> next( result )1>>> next( result )4>>> next( result )9>>> next( result )ERROR: StopIteration这个时候更简单的做法是:for r in results: print( r )因为in这个关键词自动在后台为我们调用生成器的next(..)函数什么是generator生成器?只要我们在一个函数中用了yield关键字,函数就会返回一个<generator object>生成器对象,两者是相辅相成的。有了这个对象后,我们就可以使用一系列的操作来控制这个循环结果了,比如next(..)获取下一个迭代的结果。yield和generator的关系,简单来说就是一个起因一个结果:只要写上yield, 其所在的函数就立马变成一个<generator object>对象。xrange:用生成器实现的rangePython中我们使用range()函数生成数列非常常用。而xrange()的使用方法、效果几乎一模一样,唯一不同的就是——xrange()返回的是生成器,而不是直接的结果。如果数据量大时,xrange()能极大的减小内存占用,带来卓越的性能提升。当然,几百、几千的数量级,就直接用range好了。多重yield有时候我们可能会在一个函数中、或者一个for loop中看到多个yield,这有点不太好理解。但其实很简单!一般情况下,我们写的:for n in [1,2,3]: yield n2实际上它的本质是生成了这个东西:yield 12yield 22yield 32也就是说,不用for loop,我们自己手写一个一个的yield,效果也是一样的。你每次调用一次next(..),就得到一个yield后面的值。然后三个yield的第一个就会被划掉,剩两个。再调用一次,再划掉一个,就剩一个。直到一个都不剩,next(..)就返回异常。一旦了解这个本质,我们就能理解一个函数里写多个yield是什么意思了。更深入理解yield:作为暂停符的yield从多重yield延伸,我们可以开始更进一步了解yield到底做了些什么了。现在,我们不把yield看作是return的替代品了,而是把它看作是一个suspense暂停符。即每次程序遇到yield,都会暂停。当你调用next(..)时候,它再resume继续。比如我们改一下上面的程序:def func(): yield 12 print(‘Hi, Im A!’) yield 22 print(‘Hi, Im B!’) yield 32 print(‘Hi, Im C!’)然后我们调用这个小函数,来看看yield产生的实际效果是什么:>>> f = func()>>> f<generator object func at 0x10d36c840>>>> next( f )1>>> next( f )Hi, Im A!4>>> next( f )Hi, Im B!9>>> next( f )Hi, Im C!ERROR: StopIteration从这里我们可以看到:第一次调用生成器的时候,yield之后的打印没有执行。因为程序yield这里暂停了第二次调用生成器的时候,第一个yield之后的语句执行了,并且再次暂停在第二个yield第三次调用生成器的时候,卡在了第三个yield。第四次调用生成器的时候,最后一个yield以下的内容还是执行了,但是因为没有找到第四个yield,所以报错。所以到了这里,如果我们能理解yield作为暂停符的作用,就可以非常灵活的用起来了。yield from与sub-generator子生成器yield from是Python 3.3开始引入的新特性。它主要作用就是:当我需要在一个生成器函数中使用另一个生成器时,可以用yield from来简化语句。举例,正常情况下我们可能有这么两个生成器,第二个调用第一个:def gen1(): yield 11 yield 22 yield 33def gen2(): for g in gen1(): yield g yield 44 yield 55 yield 66可以看到,我们在gen2()这个生成器中调用了gen1()的结果,并把每次获取到的结果yield转发出去,当成自己的yield出来的值。我们把这种一个生成器中调用的另一个生成器叫做sub-generator子生成器,而这个子生成器由yield from关键字生成。由于sub-generator子生成器很常用,所以Python引入了新的语法来简化这个代码:yield from。上面gen2()的代码可以简化为:def gen2(): yield from gen1() yield 44 yield 55 yield 66这样看起来是不是更"pythonic"了呢?:)所以只要记住:yield from只是把别人呕吐出来的值,直接当成自己的值呕吐出去。递归+yield能产生什么?一般我们只是二选一:要不然递归,要不然for循环中yield。有时候yield就可以解决递归的问题,但是有时候光用yield并不能解决,还是要用递归。那么怎么既用到递归,又用到yield生成器呢?参考:Recursion using yielddef func(n): result = n2 yield result if n < 100: yield from func( result )for x in func(100): print( x )上面代码的逻辑是:如果n小于100,那么每次调用next(..)的时候,都得到n的乘方。下次next,会继续对之前的结果进行乘方,直到结果超过100为止。我们看到代码里利用了yield from子生成器。因为yield出的值不是直接由变量来,而是由“另一个”函数得来了。 ...

February 19, 2019 · 1 min · jiezi

Python yield用法浅析(stackoverflow)

这是stackoverflow上一个关于python中yield用法的帖子,这里翻译自投票最高的一个回答,原文链接 here问题Python中yield关键字的用途是什么?它有什么作用?例如,我试图理解以下代码 &sup1:def _get_child_candidates(self, distance, min_dist, max_dist): if self._leftchild and distance - max_dist < self._median: yield self._leftchild if self._rightchild and distance + max_dist >= self._median: yield self._rightchild 这是调用者(caller):result, candidates = [], [self]while candidates: node = candidates.pop() distance = node._get_dist(obj) if distance <= max_dist and distance >= min_dist: result.extend(node._values) candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))return result当调用方法_get_child_candidates时会发生什么?返回了一个列表(list)?还是返回了一个元素?然后被重复调用了吗?调用何时结束?&sup1 :代码来自 Jochen Schulz (jrschulz), who made a great Python library for metric spaces. 这是完整源代码的链接:Module mspace.回答要想理解yield的作用,你必须了解什么是生成器(generators),在这之前,我们先来看可迭代对象(iterables)。可迭代对象 (iterables)当你创建了一个列表,你可以遍历这个列表读取它的每一个元素,逐个读取列表元素称为迭代(iteration)。>>> mylist = [1, 2, 3]>>> for i in mylist:… print(i)123mylist就是一个可迭代对象(iterable)。当你使用列表生成式(list comprehension)创建一个列表(list),即创建了一个可迭代对象。>>> mylist = [xx for x in range(3)]>>> for i in mylist:… print(i)014可以使用for… in…的所有对象都是可迭代对象:列表(lists)、字符串、文件…这些可迭代对象使用很方便,因为你可以根据需要如你所愿的读取其中的元素。但是,当你有大量数据时把所有值都存储在内存中,这样往往不是你想要的( but you store all the values in memory and this is not always what you want when you have a lot of values.)。生成器 (Generators)生成器是迭代器(iterators),但是只能迭代一次,生成器不会将所有值存储在内存中,而是实时的生成这些值:>>> mygenerator = (xx for x in range(3))>>> for i in mygenerator:… print(i)014看上去除了用()替换了原来的[]外,它们没什么不同。但是,你不可以再次使用for i in mygenerator ,因为生成器只能被迭代一次:计算出0,然后并不保存结果和状态继续计算出1,最后计算出4,逐一生成。yieldyield 是一个类似 return 的关键字,不同的是这个函数将返回一个生成器。>>> def createGenerator():… mylist = range(3)… for i in mylist:… yield i*i…>>> mygenerator = createGenerator() # create a generator>>> print(mygenerator) # mygenerator is an object!<generator object createGenerator at 0xb7555c34>>>> for i in mygenerator:… print(i)014这个例子没有什么实际作用。但是当你知道你的函数将返回大量你只需要读取一次的值时,使用生成器是一个有效的做法。要掌握 yeild,你必须要知道当你调用这个函数时,你在函数体中编写的代码并没有立马执行。该函数仅仅返回一个生成器对象,这有点棘手 :-)然后,你的代码将从for循环每次使用生成器停止的位置继续执行。现在到了关键部分:for第一次调用从函数创建的生成器对象,函数将从头开始执行直到遇到yeild,然后返回yield后的值作为第一次迭代的返回值。接下来每次调用都会再次执行你在函数中定义的循环,并返回(return)下一个值,直到没有值可以返回(return)。当循环结束,或者不满足if/else条件,导致函数运行但不会执行(not hit)yeild,此时生成器被认为是空的。问题代码的解释 (Your code explained)生成器 (Generator):# Here you create the method of the node object that will return the generatordef _get_child_candidates(self, distance, min_dist, max_dist): # Here is the code that will be called each time you use the generator object: # If there is still a child of the node object on its left # AND if distance is ok, return the next child if self._leftchild and distance - max_dist < self._median: yield self._leftchild # If there is still a child of the node object on its right # AND if distance is ok, return the next child if self._rightchild and distance + max_dist >= self._median: yield self._rightchild # If the function arrives here, the generator will be considered empty # there is no more than two values: the left and the right children调用者 (Caller):# Create an empty list and a list with the current object referenceresult, candidates = list(), [self]# Loop on candidates (they contain only one element at the beginning)while candidates: # Get the last candidate and remove it from the list node = candidates.pop() # Get the distance between obj and the candidate distance = node._get_dist(obj) # If distance is ok, then you can fill the result if distance <= max_dist and distance >= min_dist: result.extend(node._values) # Add the children of the candidate in the candidates list # so the loop will keep running until it will have looked # at all the children of the children of the children, etc. of the candidate candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))return result这段代码包含几个高明的部分:这个循环对列表进行迭代,但是迭代中列表还在不断扩展 :-) 这是一种遍历嵌套数据的简明方法,即使这样有些危险,因为你可能会陷入死循环中。在这个例子中,candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))穷尽了生成器产生的所有值,但while不断的创建新的生成器对象加入到列表,因为每个对象作用在不同节点上,所以每个生成器都将生成不同的值。extend()是一个列表(list)对象的方法,作用于可迭代对象(iterable),并将其值添加到列表里。通常,通常我们将列表作为参数传递给它:>>> a = [1, 2]>>> b = [3, 4]>>> a.extend(b)>>> print(a)[1, 2, 3, 4]但是在你的代码里它接收到的是一个生成器(generator),这很好,因为:你不必重复读取这些值你可以有很多子对象,但不需要将它们都存储在内存里。它很有效,因为Python不关心一个方法的参数是否是列表,Python只希望他是一个可迭代对象,所以这个参数可以是列表,元组,字符串和生成器!这就是所谓的duck typing ,这也是Python为何如此酷的原因之一,但这已经是另外一个问题了……你可以在这里停下,来看一些生成器的高级用法:控制生成器的穷尽 (Controlling a generator exhaustion)>>> class Bank(): # Let’s create a bank, building ATMs… crisis = False… def create_atm(self):… while not self.crisis:… yield “$100”>>> hsbc = Bank() # When everything’s ok the ATM gives you as much as you want>>> corner_street_atm = hsbc.create_atm()>>> print(corner_street_atm.next())$100>>> print(corner_street_atm.next())$100>>> print([corner_street_atm.next() for cash in range(5)])[’$100’, ‘$100’, ‘$100’, ‘$100’, ‘$100’]>>> hsbc.crisis = True # Crisis is coming, no more money!>>> print(corner_street_atm.next())<type ’exceptions.StopIteration’>>>> wall_street_atm = hsbc.create_atm() # It’s even true for new ATMs>>> print(wall_street_atm.next())<type ’exceptions.StopIteration’>>>> hsbc.crisis = False # The trouble is, even post-crisis the ATM remains empty>>> print(corner_street_atm.next())<type ’exceptions.StopIteration’>>>> brand_new_atm = hsbc.create_atm() # Build a new one to get back in business>>> for cash in brand_new_atm:… print cash$100$100$100$100$100$100$100$100$100… 注意,对于Python 3,请使用 print(corner_street_atm.next()) 或者 print(next(corner_street_atm)) 这在很多场景都非常有用,例如控制资源的获取。Itertools,你最好的朋友 (Itertools, your best friend)itertools模块包含很多处理可迭代对象的特殊方法。曾经想要复制一个生成器吗?连接两个生成器?用一行代码将嵌套列表中的值进行分组?不创建另一个列表进行Map/Zip?只需要import itertools需要一个例子?让我们来看看4匹马赛跑到达终点先后顺序的所有可能情况:>>> horses = [1, 2, 3, 4]>>> races = itertools.permutations(horses)>>> print(races)<itertools.permutations object at 0xb754f1dc>>>> print(list(itertools.permutations(horses)))[(1, 2, 3, 4), (1, 2, 4, 3), (1, 3, 2, 4), (1, 3, 4, 2), (1, 4, 2, 3), (1, 4, 3, 2), (2, 1, 3, 4), (2, 1, 4, 3), (2, 3, 1, 4), (2, 3, 4, 1), (2, 4, 1, 3), (2, 4, 3, 1), (3, 1, 2, 4), (3, 1, 4, 2), (3, 2, 1, 4), (3, 2, 4, 1), (3, 4, 1, 2), (3, 4, 2, 1), (4, 1, 2, 3), (4, 1, 3, 2), (4, 2, 1, 3), (4, 2, 3, 1), (4, 3, 1, 2), (4, 3, 2, 1)]了解迭代的内部机制 (Understanding the inner mechanisms of iteration)迭代是一个实现可迭代对象(实现的是 iter() 方法)和迭代器(实现的是 next() 方法)的过程。你可以获取一个迭代器的任何对象都是可迭代对象,迭代器可以让你迭代遍历一个可迭代对象(Iterators are objects that let you iterate on iterables.) .在这篇文章中有关于for循环如何工作的更多信息:here ...

December 17, 2018 · 4 min · jiezi