可能经常会有人问到python中的range和xrange有什么区别,你知道range是直接创建了一个列表,而xrange是创建了一个生成器,并且xrange非常适合当需要创建一个很大的列表的时候,因为这将带来很小的内存开销,而如果使用range则带来的系统开销不可想象。但其实这里还有一些可以扒的东西,就是惰性求值的思想。
没错,xrange用的就是惰性求值的思想。

什么是惰性求值

从维基百科上看解释,大概是这样:

惰性求值也叫延迟求值,顾名思义就是表达式不会在它被绑定到变量之后就立即求值,而是等用到时再求值。延迟求值的一个好处是能够建立可计算的无限列表而没有妨碍计算的无限循环或大小问题,

例如,我们可以建立一个斐波那契表达式,并且我们可以用这个表达式无穷的取这个数列中的值。上面提到的xrange,比如:

for i in xrange(999999999):
    print i

那么程序执行xrange(999999999)的时候,会返回一个生成器,每当执行print i的时候,才会计算当前i的值。这正是“延迟求值”,迭代器仅仅在迭代至某个元素时才计算该元素,而在这之前或之后,元素可以不存在或者被销毁。如果你把xrange换成range,那么一次就生成999999999估计够你的内存喝一壶了。

tips:在python3中,xrange被去掉了,但是同时也把range的实现变成了和xrange一样了,所以你可以在python3中放心的使用range。

惰性求值的小例子

对于python的列表表达式,一般是非惰性的,会直接生成结果列表:

print [x*x for x in xrange(5)]

我们执行上面的命令,会直接生成列表[0, 1, 4, 9, 16],但如果我们想让它成为惰性求值怎么办呢?可以这样:

print (x*x for x in xrange(5))

只需把中括号改为小括号,就会是一个生成器,上面代码打印出的是<generator object at 0x004FBE18>。
那么问题来了,我们怎么去访问生成器生成的对象呢?答案就是使用next()方法:

n= (x*x for x in xrange(5))
for i in range(6):
    print n.next()

每次调用next()方法都会生成下一个值,直到抛出异常:

0
1
4
9
16
Traceback (most recent call last):
  File "D:/pythonWorkspace/IocustTest/local.py", line 7, in <module>
    print n.next()
StopIteration

当我们再没有值可生成时,就抛出了StopIteration错误,所以我们可以捕获这个异常来终止next()方法的调用。

一个实例

我们在写代码时可能会有这种场景:

s1 = somewhatLongOperation1()
s2 = somewhatLongOperation2()
if condition:
    return str(s1)+str(s2)

上面的代码片需要分别计算s1和s2,然后如果条件成立,再连接s1和s2。在python中,这个代码段的执行顺序是确定的,就是从上到下。我们的计算函数可能会需要较长的时间,那么能不能缩短这个时间呢?s1和s2的值能不能并行计算呢?可以,只要确保没有函数修改或依赖于全局变量,那么这两个函数是可以经过特殊处理并行执行的。但其实我们还可以优化,借用惰性求值的理念,直到确实需要计算s1和s2的时候再计算:

def concatenate(condition):
    if condition:
        s1 = somewhatLongOperation1()
        s2 = somewhatLongOperation2()
        return str(s1)+str(s2)

上面的代码一开始并不计算s1和s2,直到条件成立,必须要计算的时候才计算,这样改的话明显会提高程序的执行效率。
惰性求值有显著的优化潜力。我们完全可以像数学家面对代数表达式一样,可以消去一部分而完全不去运行它,重新调整代码段以求更高的效率。