列表生成式、迭代器与生成器


1 列表生成式

列表生成式又叫列表解析,列表解析就是根据已有列表,高效生成新列表的方式。

列表解析是 Python 迭代机制的一种应用,它常用于实现创建新的列表,因此要置于 [] 中。

列表解析能够使用比for循环快近一倍的方式基于已有的列表来生成新列表。

列表生成式语法:

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

例:

L = [i ** 2 for i in range(9)]
print(L)

现在有个需求,看列表[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],我要求你把列表里的每个值加1,你怎么实现?你可能会想到3种方式

版本一:

a = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
b = []
for i in a:
    b.append(i+1)

print(b)

a = b
print(a)

版本二:

a = [1,3,4,6,7,7,8,9,11]

for index,i in enumerate(a):
    a[index] += 1

print(a)

版本三:

a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
a = map(lambda x:x+1, a)
print(a)    # <map object at 0x101d2c630>
for i in a:
    print(i)

版本四(列表生成式)

a = [i + 1 for i in range(10)]

print(a)

现在有一个列表list1,需要取得列表list1中每一个元素的平方,并生成一个新列表

list1 = [1,2,3,4,5,6,7,8]
list2 = [i ** 2 for i in list1]
print(list2)

只把list1中元素值大于或等于3的计算机其平方,并生成新列表

list1 = [1,2,3,4,5,6,7,8]
list2 = [i ** 2 for i in list1 if i >= 3]
print(list2)

求1到10范围内10个数字的平方除以2的结果

for i in [i ** 2 for i in range(1,11)]:
    print(i / 2)

返回1到10范围内所有偶数的平方除以2的结果

for i in [i ** 2 for i in range(1,11) if i % 2 == 0]:
    print(i / 2)

把/root目录下所有以.log结尾的文件取出来,生成一个新列表

import os

filelist = [i for i in os.listdir("/root") if i.endswith(".log")]

print(filelist)

把/etc目录下所有目录取出来,生成一个新列表

import os

directory1 = [i for i in os.listdir("/etc") if os.path.isdir("/etc/"+i)]
directory2 = ["/etc/" + i for i in os.listdir("/etc") if os.path.isdir("/etc/"+i)]
print(directory1)
print(directory2)

在使用列表解析时,for循环还可以嵌套for循环。举例说明:

假设现在有两个列表list1和list2,现在要实现两个列表的元素交叉相乘

list1 = ['x','y','z']
list2 = [1,2,3]
list3 = [(i,j) for i in list1 for j in list2]
print(list3)

当list2中元素值不为1时与list1交叉相乘

list1 = ['x','y','z']
list2 = [1,2,3]
list3 = [(i,j) for i in list1 for j in list2 if j != 1]
print(list3)

列表生成式与函数结合

def a(x):
    x *= 5
    return x

b = [a(i) for i in range(10)]
print(b)

2 生成器

列表解析会直接返回一个新列表,如果某个原列表里面的元素非常多,而列表解析又以几何倍数向上增长以后,就会极其占用内存,如此将导致效率低下。

但是列表生成以后,一般一次只取一个元素,而for循环一次也只遍历迭代其中的一个元素,基于此,使用直接生成一个列表的方式如此占用内存是极其不好的。

由此我们可以对列表解析作一个扩展,就是把 [] 变成 () ,这就叫做生成器(generator)。

生成器和列表解析的关系就相当于python 2.x系列中xrange和range的关系,它可以让我们使用一个表达式,一次只生成计算出一个值,叫惰性计算方式。

生成器不像列表解析一样,不管你用不用直接就生成了。生成器仅仅是你调用一次,它就可以返回一个,再调用一次,再返回一个。

生成器表达式并不真正创建数字列表,而是返回一个生成器对象,此对象在每次计算出一个条目后,把这个条目“产生”(yield)出来。生成器表达式使用了“惰性计算”或称作“延迟求值”的机制。

当序列过长,并且每次只需要获取一个元素时,应当考虑使用生成器表达式而不是列表解析。

生成器的特点

  • 只有在调用时才会生成相对应的数据
  • 只记录当前位置,无法向前回溯,只能一步步往后走
  • 可以通过__next__()方法一次取一个值
  • 可以通过for循环取得所有值

生成器表达式语法:

(expr for iter_var in iterable)
(expr for iter_var in iterable if cond_expr)

生成器生成方法:
通过列表解析方式生成

a = (i * 2 for i in range(10))
print(a)
print(type(a))

我们可以直接打印出list的每一个元素,但我们怎么打印出generator的每一个元素呢?

如果要一个一个打印出来,可以通过next()函数获得generator的下一个返回值

a = (i * 2 for i in range(10))

print(a.__next__())

print(next(a))
print(next(a))
print(next(a))
print(next(a))

每次调用next(g),就计算出g的下一个元素的值,直到计算到最后一个元素,没有更多的元素时,抛出StopIteration的错误。

当然,上面这种不断调用next(g)实在是太变态了,正确的方法是使用for循环,因为generator也是可迭代对象

a = (i for i in range(10))

for i in a:
    print(i)

所以,我们创建了一个generator后,基本上永远不会调用next(),而是通过for循环来迭代它,并且不需要关心StopIteration的错误。

generator非常强大。如果推算的算法比较复杂,用类似列表生成式的for循环无法实现的时候,还可以用函数来实现。

通过函数生成(此处用著名的裴波那契数列来举例)
斐波拉契数列(Fibonacci),除第一个和第二个数外,任意一个数都可由前两个数相加得到:

1, 1, 2, 3, 5, 8, 13, 21, 34, ...

斐波拉契数列用列表生成式写不出来,但是,用函数把它打印出来却很容易

def fib(max):
    n, a, b = 0, 0, 1
    while n < max:
        print(b)
        a, b = b, a + b
        n = n + 1
    return 'done'

注意,赋值语句:

a, b = b, a + b

相当于:

t = (b, a + b)  # t是一个tuple
a = t[0]
b = t[1]

但不必显式写出临时变量t就可以赋值。

上面的函数可以输出斐波那契数列的前N个数

def fib(max):
    n, a, b = 0, 0, 1
    while n < max:
        print(b)
        a, b = b, a + b
        n = n + 1
    return 'done'

fib(10)

仔细观察,可以看出,fib函数实际上是定义了斐波拉契数列的推算规则,可以从第一个元素开始,推算出后续任意的元素,这种逻辑其实非常类似generator。

也就是说,上面的函数和generator仅一步之遥。要把fib函数变成generator,只需要把print(b)改为yield b就可以了

def fib(max):
    n, a, b = 0, 0, 1
    while n < max:
        # print(b)
        yield b
        a, b = b, a + b
        n = n + 1
    return 'done'

a = fib(10)
print(a)

这就是定义generator的另一种方法。如果一个函数定义中包含yield关键字,那么这个函数就不再是一个普通函数,而是一个generator。

这里,最难理解的就是generator和函数的执行流程不一样。函数是顺序执行,遇到return语句或者最后一行函数语句就返回。而变成generator的函数,在每次调用next()的时候执行,遇到yield语句返回,再次执行时从上次返回的yield语句处继续执行。

def fib(max):
    n, a, b = 0, 0, 1
    while n < max:
        # print(b)
        yield b
        a, b = b, a + b
        n = n + 1
    return 'done'

data = fib(10)
print(data)

print(data.__next__())
print(data.__next__())
print("干点别的事")
print(data.__next__())
print(data.__next__())
print(data.__next__())
print(data.__next__())
print(data.__next__())

在上面fib的例子,我们在循环过程中不断调用yield,就会不断中断。当然要给循环设置一个条件来退出循环,不然就会产生一个无限数列出来。

同样的,把函数改成generator后,我们基本上从来不会用next()来获取下一个返回值,而是直接使用for循环来迭代

def fib(max):
    n, a, b = 0, 0, 1
    while n < max:
        # print(b)
        yield b
        a, b = b, a + b
        n = n + 1
    return 'done'

for i in fib(10):
    print(i)

但是用for循环调用generator时,发现拿不到generator的return语句的返回值。如果想要拿到返回值,必须捕获StopIteration错误,返回值包含在StopIteration的value中

def fib(max):
    n, a, b = 0, 0, 1
    while n < max:
        # print(b)
        yield b
        a, b = b, a + b
        n = n + 1
    return 'done'

a = fib(6)
while True:
    try:
        x = next(a)
        print('a:', x)
    except StopIteration as e:
        print('Generator return value:', e.value)
        break

关于如何捕获异常,后面的异常处理还会详细讲解。

yield保存了函数的中断状态。

生成器可以实现在单线程(串行)的情况下实现并发运算的效果

  • 生成器的__next__()方法只调用生成器的状态而不传值;
  • 生成器的send()方法在调用生成器的状态的同时给生成器传一个值

生产消费者模型:

# coding: utf-8

import time

def consumer(name):
    print("%s 准备吃包子啦!" % name)
    while True:
       baozi = yield

       print("包子[%s]来了,被[%s]吃了!" %(baozi,name))


def producer(name):
    c = consumer('A')
    c2 = consumer('B')
    c.__next__()
    c2.__next__()
    print("老子开始准备做包子啦!")
    for i in range(1,11,2):
        time.sleep(1)
        print("做了2个包子!")
        c.send(i)
        c2.send(i+1)

producer("tom")

3 迭代器

我们已经知道,可以直接作用于for循环的数据类型有以下几种:

  • 一类是集合数据类型,如list、tuple、dict、set、str等;
  • 一类是generator,包括生成器和带yield的generator function。

这些可以直接作用于for循环的对象统称为可迭代对象:Iterable。

可以使用isinstance()判断一个对象是否是Iterable对象:

from collections.abc import Iterable
print(isinstance([], Iterable))
print(isinstance({}, Iterable))
print(isinstance('abc', Iterable))
print(isinstance((x for x in range(10)), Iterable))
print(isinstance(100, Iterable))

而生成器不但可以作用于for循环,还可以被next()函数不断调用并返回下一个值,直到最后抛出StopIteration错误表示无法继续返回下一个值了。

说了这么多可迭代对象,那么何为迭代?简单的说,迭代就是重复做一件事。

可迭代(iterable)对象有哪些:

  • 支持每次返回自己所包含的一个成员的对象;
  • 对象在内部实现了__iter__方法或者__getitem__方法:
    • 序列类型,如:list、str、tuple
    • 非序列类型,如:dict、file
    • 用户自定义的一些包含了__iter__()或__getitem__()方法的类

可迭代对象能干什么?

  • 可迭代对象支持每次返回自己所包含的一个成员的对象,每次取得一个,从头遍历到尾部。也就是说可迭代对象本身是可遍历的。

一直在说迭代器,那么到底什么是迭代器呢?

迭代器(iterator)又称游标(cursor),它是程序设计的软件设计模式,是一种可以在容器物件(container,如列表等)上实现元素遍历的接口。迭代器是一种特殊的数据结构,它是以对象的形式存在的。

说白点,可以被next()函数调用并不断返回下一个值的对象称为迭代器:Iterator

可以使用isinstance()判断一个对象是否是Iterator对象

from collections.abc import Iterator
print(isinstance([], Iterator))
print(isinstance({}, Iterator))
print(isinstance('abc', Iterator))
print(isinstance((x for x in range(10)), Iterator))
print(isinstance(100, Iterator))

对于集体中每一个元素,想要执行遍历,那么针对这个集体的迭代器定义了遍历集体中每一个元素的顺序或者方法。对于迭代器来说,我们可以对它调用next方法来获取其每一个元素或者说遍历其每一个元素。

迭代器是不可逆的,当遍历结束后抛出异常,不会从头开始再次迭代,若想从头再来一遍只能重新创建一个迭代器。

迭代器生成:
方法一:调用__iter__方法生成一个迭代器

l1 = [1,2,3,4]
print(type(l1))

l2 = l1.__iter__()
print(type(l2))

方法二:调用iter()函数来生成迭代器

l1 = [1,2,3,4]
print(type(l1))

l2 = iter(l1)
print(type(l2))

iter(l1)相当于调用了l1的__iter__方法。

生成器是迭代器对象,但list、tuple、dict、str虽然是可以迭代对象,却不是迭代器对象,因为他们都没有__next__()方法。

我们可以用iter()函数将list、tuple、dict、str等转换成迭代器。

既然我们可以通过iter()函数将list、tuple、dict、str等这些数据类型转换成迭代器,那为什么不一开始就让这些数据类型默认就是一个迭代器对象呢?

这是因为python中的Iterator迭代器对象表示的是一个数据流,Iterator对象可以被next()函数调用并不断返回下一个数据,直到没有数据时抛出StopIteration异常。

我们可以把这个数据流看成是一个有序的序列,但我们不可能知道这个序列的准确长度,只能通过next()函数不断的按需生成下一个数据,所以Iterator的计算是惰性的。

在python中,迭代器是遵循迭代协议的对象,使用iter()可从任何序列对象中得到迭代器,若要实现迭代器,需要在类中定义__next__()方法。

要使得迭代器指向下一个元素,则使用成员函数__next__():

  • 在python中,是函数next(),而非成员函数
  • 当没有元素时,则引发StopIteration异常
l1 = iter([1,2,3,4])

print(l1.__next__())
print(l1.__next__())
print(l1.__next__())
print(l1.__next__())
print(l1.__next__())

# print(next(l1))
# print(next(l1))
# print(next(l1))
# print(next(l1))
# print(next(l1))

Python的for循环本质上就是通过不断调用next()函数实现的,例如:

for x in [1, 2, 3, 4, 5]:
    pass

实际上完全等价于:

# 首先获得Iterator对象:
it = iter([1, 2, 3, 4, 5])
# 循环:
while True:
    try:
        # 获得下一个值:
        x = next(it)
        print(x)
    except StopIteration:
        # 遇到StopIteration就退出循环
        break

for循环内部三件事

  • 调用可迭代对象的iter方法生成一个迭代器对象
  • 不断调用迭代器对象的next方法
  • 处理StopIteration异常