Python: 推导式和生成器

 引言

       如果想通过操作和处理一个序列(或其他的可迭代对象)来创建一个新的列表时可以使用列表解析(  List comprehensions)生成表达式。同样的还有字典推导式,集合推导式。

一、推导式

1.1 列表推导式list comprehensions)

也叫列表解析式。它的结构是在一个中括号里包含一个表达式,然后是一个for语句,然后是0个或多个for或者if语句。那个表达式可以是任意的,意思是你可以在列表中放入任意类型的对象。返回结果将是一个新的列表,在这个以iffor语句为上下文的表达式运行完成之后产生。

(1)提供了一种简明扼要的方法来创建列表。

(2)在需要改变列表而不是需要新建某列表时,可以使用列表解析。

列表解析表达式为:

[expr for iter_var in iterable] 
[expr for iter_var in iterable if cond_expr]

第一种语法:首先迭代iterable里所有内容,每一次迭代,都把iterable里相应内容放到iter_var中,再在表达式中应用该iter_var的内容,最后用表达式的计算值生成一个列表(每一次迭代的值组成一个列表)。
第二种语法:加入了判断语句,只有满足条件的内容才把iterable里相应内容放到iter_var中,再在表达式中应用该iter_var的内容,最后用表达式的计算值生成一个列表。

示例:

>>> L= [(x+1,y+1) for x in range(3) for y in range(5)] 
>>> L
[(1, 1), (1, 2), (1, 3), (1, 4), (1, 5), (2, 1), (2, 2), (2, 3), (2, 4), (2, 5), (3, 1), (3, 2), (3, 3), (3, 4), (3, 5)]

可以看到迭代顺序是从后往前。先迭代倒数第一个迭代对象,然后迭代倒数第二个迭代对象,...,最后迭代第一个迭代对象。 

>>> N=[x+10 for x in range(10) if x>5]
>>> N
[16, 17, 18, 19]

1.1.1 嵌套循环

嵌套循环(nested loop)该如何改写为列表解析式呢?

下面是一个拉平(flatten)矩阵(以列表为元素的列表)的for循环:

flattened = []
for row in matrix:
    for n in row:
        flattened.append(n)

下面这个列表解析式实现了相同的功能:

flattened = [n for row in matrix for n in row]

列表解析式中的嵌套循环读起来就有点绕口了。

注意:有可能会想把这个列表解析式写成这样:

flattened = [n for n in row for row in matrix]

但是这行代码是错误的。这里我们颠倒了两个for循环的顺序。正确的代码是之前那个。也就是如果要在列表解析式中处理嵌套循环,请记住for循环子句的顺序与我们原来for循环的顺序是一致的。从左到右是第一层循环,第二层循环,...,最后一层循环。

1.1.2 多层嵌套循环

列表推导式可以带任意数量的嵌套 for 循环,并且每一个 for 循环后面都有可选的 if 语句。

通用语法:

[ expression for x in X [if condition]
             for y in Y [if condition]
             ...
             for n in N [if condition] ]

例如,下面的代码输出了0~4之间的偶数和奇数的组合。

>>> [(x, y) for x in range(5) if x % 2 == 0 for y in range(5) if y % 2 == 1]
[(0, 1), (0, 3), (2, 1), (2, 3), (4, 1), (4, 3)]

等价于下面的一般 for 循环:

>>> L = []
>>> for x in range(5):
...     if x % 2 == 0:
...         for y in range(5):
...             if y % 2 == 1:
...                 L.append((x, y))
>>> L
[(0, 1), (0, 3), (2, 1), (2, 3), (4, 1), (4, 3)]

同样地原则也适用集合解析式(set comprehension)和字典解析式(dictionary comprehension)。

1.2 字典推导式(dict comprehensions)

字典推导和列表推导的使用方法是类似的,通过迭代生成一个字典。

字典推导式为:

{expr for iter_var in iterable} 
{expr for iter_var in iterable if cond_expr}

expr可以操作字典中的key和value。

示例:

mcase = {'a': 10, 'b': 34, 'A': 7, 'Z': 3}

mcase_frequency = {
    k.lower(): mcase.get(k.lower(), 0) + mcase.get(k.upper(), 0)
    for k in mcase.keys()
}

# mcase_frequency == {'a': 17, 'z': 3, 'b': 34}

在上面的例子中我们把同一个字母但不同大小写的值合并起来了。

你还可以快速对换一个字典的键和值:

{v: k for k, v in some_dict.items()}

1.3 集合推导式(set comprehensions)

它们跟列表推导式也是类似的。 唯一的区别在于它们使用大括号{}。 举个例子:

squared = {x**2 for x in [1, 1, 2]}
print(squared)
# Output: {1, 4}

 需要保持集合的特点:不能含有重复元素。

二、生成器(Generators)

2.1 迭代器

迭代器是一个让程序员可以遍历一个容器(特别是列表)的对象。然而,一个迭代器在遍历并读取一个容器的数据元素时,并不会执行一个迭代。换句话说这里有三个部分:

  • 可迭代对象(Iterable)
  • 迭代器(Iterator)
  • 迭代(Iteration)

2.1.1 可迭代对象(Iterable)

Python中任意的对象,只要它定义了可以返回一个迭代器的__iter__方法,或者定义了可以支持下标索引的__getitem__方法,那么它就是一个可迭代对象。简单说,可迭代对象就是能提供迭代器的任意对象。

2.1.2 迭代器(Iterator)

任意对象,只要定义了next(Python2) 或者__next__方法,它就是一个迭代器。

2.1.3 迭代(Iteration)

用简单的话讲,它就是从某个地方(比如一个列表)取出一个元素的过程。当我们使用一个循环来遍历某个东西时,这个过程本身就叫迭代

2.2 生成器

       生成器也是一种迭代器,但是你只能对其迭代一次。这是因为它们并没有把所有的值存在内存中,而是在运行时生成值。你通过遍历来使用它们,要么用一个“for”循环,要么将它们传递给任意可以进行迭代的函数和结构。

        生成器包括生成器表达式(类似于推导式)和生成器函数。

2.2.1 生成器表达式

        生成器表达式是在python2.4中引入的,当序列过长, 而每次只需要获取一个元素时,应当考虑使用生成器表达式而不是列表解析。生成器表达式的语法和列表解析一样,只不过生成器表达式是被()括起来的,而不是[],如下:

(expr for iter_var in iterable) 
(expr for iter_var in iterable if cond_expr)
>>> L= (i +1 for i in range(10) if i %2)
>>> L
<generator object <genexpr> at 0xb749a52c>
>>> L1=[]
>>>for i in L:
...     L1.append(i)
... 
>>> L1
[2, 4, 6, 8, 10]

       生成器表达式并不真正创建数字列表, 而是返回一个生成器,这个生成器在每次计算出一个条目后(调用的时候再计算),把这个条目“产生”(yield)出来。 生成器表达式使用了“惰性计算”(lazy evaluation,也有翻译为“延迟求值”,我以为这种按需调用call by need的方式翻译为惰性更好一些),只有在检索时才被赋值( evaluated),所以在列表比较长的情况下使用内存上更有效。

A generator object in python is something like a lazy list. The elements are only evaluated as soon as you iterate over them. 

2.2.2 生成器函数

     大多数时候生成器是以函数来实现的。然而,它们并不返回一个值,而是yield(暂且译作“生出”)一个值。   

示例1:

def generator_function():
    for i in range(10):
        yield i

for item in generator_function():
    print(item)

# Output: 0
# 1
# 2
# 3
# 4
# 5
# 6
# 7
# 8
# 9

示例2:计算斐波那契数列的生成器:。

# generator version
def fibon(n):
    a = b = 1
    for i in range(n):
        yield a
        a, b = b, a + b

函数使用方法如下:

for x in fibon(1000000):
    print(x)

       我们已经讨论过生成器使用一次迭代。在测试前你需要再知道一个Python内置函数:next()。它允许我们获取一个序列的下一个元素。那我们来验证下我们的理解:

def generator_function():
    for i in range(3):
        yield i

gen = generator_function()
print(next(gen))
# Output: 0
print(next(gen))
# Output: 1
print(next(gen))
# Output: 2
print(next(gen))
# Output: Traceback (most recent call last):
#            File "<stdin>", line 1, in <module>
#         StopIteration

       我们可以看到,在yield掉所有的值后,next()触发了一个StopIteration的异常。基本上这个异常告诉我们,所有的值都已经被yield完了。

       为什么我们在使用for循环时没有这个异常呢?因为for循环会自动捕捉到这个异常并停止调用next()

       实际上Python中一些内置数据类型也支持迭代。我们这就去看看:

my_string = "Yasoob"
next(my_string)
# Output: Traceback (most recent call last):
#      File "<stdin>", line 1, in <module>
#    TypeError: str object is not an iterator

        好吧,这不是我们预期的。这个异常说那个str对象不是一个迭代器。对,就是这样!它是一个可迭代对象,而不是一个迭代器。这意味着它支持迭代,但我们不能直接对其进行迭代操作。那我们怎样才能对它实施迭代呢?是时候学习下另一个内置函数,iter。它将根据一个可迭代对象返回一个迭代器对象。这里是我们如何使用它:

my_string = "Yasoob"
my_iter = iter(my_string)
next(my_iter)
# Output: 'Y'

2.2.3 生成器应用场景 

    生成器最佳应用场景是:不想同一时间将所有计算出来的大量结果集分配到内存当中,特别是结果集里还包含循环。特别是循环很大,计算量也很大。

    许多Python 2里的标准库函数都会返回列表,而Python 3都修改成了返回生成器,因为生成器占用更少的资源。

三、range和xrange

3.1 概念

多用于循环生成一组数值。

range
     函数说明:range([start,] stop[, step]),根据start与stop指定的范围以及step设定的步长,生成一个序列。不包含stop。

     start 的默认值为 0。唯一要求的参数值是 stop,产生的最后一个数的值是 stop 的前一个,并且 step 的默认值是 1。当然,也可以反向创建自然数序列,这时 step 的值为 -1。

像 zip()、range() 这些函数返回的是一个可迭代的对象,所以可以使用 for ... in 的结构遍历,或者把这个对象转化为一个序列(例如列表)。
range示例: 

>>> range(5) 
[0, 1, 2, 3, 4] 
>>> range(1,5) 
[1, 2, 3, 4] 
>>> range(0,6,2)
[0, 2, 4]
>>> range(0,-10,-1)            #起点是1,终点是10,步长为-1 
[0, -1, -2, -3, -4, -5, -6, -7, -8, -9]
>>> range(0,-10,1)            #起点是0,终点是-10,终点为负数时,步长只能为负数,否则返回空
[]
>>> range(0)               #起点是0,返回空列表
[]
>>> range(1,0)              #起点大于终点,返回空列表
[]

xrange
     函数说明:用法与range完全相同,所不同的是生成的不是一个数组,而是一个生成器。不包含stop。
xrange示例: 

>>> xrange(5)
xrange(5)
>>> list(xrange(5))
[0, 1, 2, 3, 4]
>>> xrange(1,5)
xrange(1, 5)
>>> list(xrange(1,5))
[1, 2, 3, 4]
>>> xrange(0,6,2)
xrange(0, 6, 2)
>>> list(xrange(0,6,2))
[0, 2, 4]

所以不同点就是range类似于推导式,而xrange则是生成器。range产生一个列表,而xrange则是在使用的时候产生每一个列表值。所以range需要为产生的列表开辟所有内存,并且产生整个列表,而xrange只在调用时产生每一个值。

由上面的示例可以知道:要生成很大的数字序列的时候,用xrange会比range性能优很多,因为不需要一上来就开辟一块很大的内存空间。在小序列时,两者区别不大。除非是要返回一个列表,所以推荐使用xrange函数。Python3 里,range 已经被 xrange 取代了。

四、总结:

1. 当需要只是执行一个循环的时候尽量使用循环而不是列表解析,这样更符合python提倡的直观性。

2. 当有内建的操作或者类型能够以更直接的方式实现的,不要使用列表解析。

例如复制一个列表时,使用:L1=list(L)即可,不必使用:L1=[x for x in L]

3. 当序列过长, 而每次只需要获取一个元素时,使用生成器表达式。

4. 列表解析的性能相比要比map要好,实现相同功能的for循环效率最差(和列表解析相比差两倍)。

5. 列表解析可以转换为 for循环或者使用map(其中可能会用到filter、lambda函数)表达式,但是列表解析更为简单明了,后者会带来更复杂和深层的嵌套。

6. 解析式如果很长,可以利用断行(换行)来增加代码的可读性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值