从range和xrange的性能对比到yield关键字(中)
上节提出了range和xrange的效率问题,这节我们来探究其中的原因
yield的使用
我们看下面的程序:
#coding: utf-8 def test(): print 4 print 2 print 5 if __name__ == '__main__': test()
这段代码的运行结果当然是没有任何疑问的。
但是如果我将代码修改一下:
#coding: utf-8 def test(): yield 4 yield 2 yield 5 if __name__ == '__main__': print test()
运行结果有些奇怪:
<generator object test at 0xb71f1144>
我们尝试这样使用:
if __name__ == '__main__': for i in test(): print i
结果却出人意料:
wing@ubuntu:~/Documents/py|⇒ python 17.py
4
2
5
这是什么原因呢?这里看起来,test()好像一个集合,里面存储了4,2,5,所以我们才能够依次遍历。
实际上,原因并非如此。
当一个函数中含有yield时,这个函数就不再是一个普通的函数,而是一个可迭代的对象(实际上叫做生成器,不过现在不必关心概念)。
同样,执行该函数时,不再是马上执行其中的语句,而是生成一个可迭代对象。当执行迭代的时候,才真正运行其中的代码。
当函数体执行到yield时,便退出这个函数,此时yield具有return的功能。但是这里的关键是,当下次执行这个函数时,并不是从头开始执行,而是从上次yield退出的位置继续执行。
尝试下面的代码:
#coding: utf-8 def test(): yield 4 yield 2 yield 5 if __name__ == '__main__': t = test() it = iter(t) print it.next() print it.next() print it.next() print it.next()
运行结果为:
wing@ubuntu:~/Documents/py|⇒ python 17.py 4 2 5 Traceback (most recent call last): File "17.py", line 14, in <module> print it.next() StopIteration
从这里的结果可以看出,test()语句没有执行代码段,而是生成了一个可以迭代的对象。
我们甚至可以得出结论,每当执行一次next,就向后执行到下一个yield语句,或者所有的语句执行完毕。
range的实现
我们尝试实现range:
#coding: utf-8 def _range(value): i = 0 result = [] while i < value: result.append(i) i += 1 return result if __name__ == '__main__': for i in _range(4): print i
range的逻辑比较简单,就是生成一个列表。
xrange的模拟实现
我们根据前面的结论,猜测xrange是一个含有yield的函数,于是:
#coding: utf-8 def _xrange(value): i = 0 while i < value: yield i i += 1 if __name__ == '__main__': for i in _xrange(4): print i
运行一下,结果和我们预期一致。
当然,实际的xrange比我们这里编写的更加复杂,但是基本原理是一致的。
为何xrange比range高效?
答案很明显了,range是一次性生成所有的数据,而xrange,内部使用了yield关键字,每次只运行其中一部分,这样从头到尾都没有占用大量的内存和时间。所以效率较高。
我们再次比较性能,这次比较的是我们自己编写的版本:
#coding: utf-8 import sys from time import time def _range(value): i = 0 result = [] while i < value: result.append(i) i += 1 return result def _xrange(value): i = 0 while i < value: yield i i += 1 def count_time(func): def wrapped(*args, **kargs): begin_time = time() result = func(*args, **kargs) end_time = time() cost_time = end_time - begin_time print '%s called cost time : %s ms' %(func.__name__, float(cost_time)*1000) return result return wrapped @count_time def test1(length): for i in _range(length): pass @count_time def test2(length): for i in _xrange(length): pass if __name__ == '__main__': length = int(sys.argv[1]) test1(length) test2(length)
运行结果为:
wing@ubuntu:~/Documents/py|⇒ python 19.py 1000 test1 called cost time : 0.116109848022 ms test2 called cost time : 0.0619888305664 ms wing@ubuntu:~/Documents/py|⇒ python 19.py 10000 test1 called cost time : 2.39086151123 ms test2 called cost time : 0.566959381104 ms wing@ubuntu:~/Documents/py|⇒ python 19.py 100000 test1 called cost time : 15.5799388885 ms test2 called cost time : 6.41298294067 ms wing@ubuntu:~/Documents/py|⇒ python 19.py 1000000 test1 called cost time : 130.295038223 ms test2 called cost time : 65.4468536377 ms wing@ubuntu:~/Documents/py|⇒ python 19.py 10000000 test1 called cost time : 13238.3038998 ms test2 called cost time : 652.212142944 ms
显然,使用yield的版本更加高效。
下文,我们探究生成器。