【Python笔记】列表解析、迭代器和生成器

文章目录

    • 1. 列表解析
    • 2. 迭代器
      • 2.1 什么是迭代器
      • 2.2 为什么要迭代器
      • 2.3 如何迭代
      • 2.4 使用迭代器
      • 2.5 可变对象和迭代器
      • 2.6 如何创建
    • 3. 生成器
      • 3.1 简单的生成器特性
      • 3.2 加强的生成器特性
      • 3.3 生成器表达式

1. 列表解析

列表解析(List Comprehensions,或缩略为 list comps)来自函数式编程语言 Haskell。它是一个非常有用、简单而且灵活的工具,可以用来动态地创建列表。它在 Python2.0 中被加入。
Python 支持函数式编程,例如 lambda、map() 和 filter()等。通过列表解析,它们可以被简化为一个列表解析式子。

列表解析的语法:

[expr for iter_var in iterable]

这个语句的核心是 for 循环,它迭代 iterable 对象的所有条目。前边的 expr 应用于序列的每个成员,最后的结果值是该表达式产生的列表。迭代变量并不需要是表达式的一部分。

例子,计算序列成员的平方的 lambda 函数表达式:

>>> list(map(lambda x: x**2, range(6)))
[0, 1, 4, 9, 16, 25]

我们可以使用下面这样的列表解析来替换它:

>>> [x**2 for x in range(6)]
[0, 1, 4, 9, 16, 25]

在新语句中,只有一次函数调用(range()),而先前的语句中有四次函数调用(range()、map()、list() 和 lambda)。列表解析的表达式可以取代内建的 map() 函数以及 lambda,而且效率更高。

结合 if 语句,列表解析还提供了一个扩展版本的语法:

[expr for iter_var in iterable if cond_expr]

这个语法在迭代时会过滤或“捕获”满足条件表达式 cond_expr 的序列成员。

odd() 函数用于判断一个数值对象是奇数还是偶数(奇数返回 1,偶数返回 0):

def odd(n):
    return n%2

利用这个函数的核心操作,使用 filter() 和 lambda 挑选出序列中的奇数:

>>> seq = [11, 10, 9, 9, 10, 10, 9, 8, 23, 9, 7, 18, 12, 11, 12]
>>> list(filter(lambda x: x%2, seq))
[11, 9, 9, 9, 23, 9, 7, 11]

和先前的例子一样,即使不用 filter() 和 lambda,我们同样可以使用列表解析来完成操作,获得想要的数字:

>>> [x for x in seq if x%2]
[11, 9, 9, 9, 23, 9, 7, 11]

更多的例子。

1. 矩阵样例
迭代一个有3行5列的矩阵。

>>> [(x+1, y+1) for x in range(3) for y in range(5)]
[(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)]

2. 磁盘文件样例
假设有一个文件 hhga.txt,需要进行统计:
And the Lord spake, saying, “First shalt thou take out the Holy Pin. Then shalt thou count to three, no more, no less. Three shall be the number thou shalt count, and the number of the counting shall be three. Four shalt thou not count, nei- ther count thou two, excepting that thou then proceed to three. Five is right out. Once the number three, being the third number, be reached, then lobbest thou thy Holy Hand Grenade of Antioch towards thy foe, who, being naughty in My sight, shall snuff it.”

迭代文件内容:

>>> f = open('hhga.txt', 'r')
>>> [line for line in f]
['And the Lord spake, saying, "First shalt thou take out the Holy Pin. Then shal
t thou count to three, no more, no less. Three shall be the number thou shalt co
unt, and the number of the counting shall be three. Four shalt thou not count, n
either count thou two, excepting that thou then proceed to three. Five is right
out. Once the number three, being the third number, be reached, then lobbest tho
u thy Holy Hand Grenade of Antioch towards thy foe, who, being naughty in My sig
ht, shall snuff it."']

把每行分割(split)为单词,计算单词个数:

>>> f = open('hhga.txt', 'r')
>>> len([word for line in f for word in line.split()])
91

快速地计算文件大小:

>>> import os
>>> os.stat('hhga.txt').st_size   # 返回文件大小,以“位”为单位
498

将每个单词的长度加起来,得到和:

>>> f.seek(0)                     # 返回文件开头
0
>>> sum([len(word) for line in f for word in line.split()])
408

2. 迭代器

2.1 什么是迭代器

迭代器是在版本2.2被加入Python的,它为类序列对象(sequence-like object)提供了一个类序列(sequence-like)的接口。序列是一组数据结构,你可以利用它们的索引从0开始一直“迭代”到序列的最后一个条目。用“计数”的方法迭代序列是很简单的。Python的迭代无缝地支持序列对象,而且它还允许程序员迭代非序列类型,包括用户定义的对象。
迭代器用起来很灵巧,你可以迭代不是序列但是表现出序列行为的对象。例如字典的键、一个文件的行,等等。当你使用循环迭代一个对象条目时,你几乎分辨不出它是迭代器还是序列。你不必去关注这些,因为Python让它像一个序列那样操作。

2.2 为什么要迭代器

引用 PEP(234)中对迭代器的定义:

  • 提供了可扩展的迭代器接口;
  • 对列表迭代带来了性能上的增强;
  • 在字典迭代中性能提升;
  • 创建真正的迭代接口,而不是原来的随机对象访问;
  • 与所有已经存在的用户定义的类以及扩展的模拟序列和映射的对象向后兼容;
  • 迭代非序列集合(例如映射和文件)时,可以创建更间接可读的代码。

2.3 如何迭代

根本上说,迭代器就是有一个 next() 方法的对象,而不是通过索引来计数。当你或是一个循环机制(例如 for 语句)需要下一个项时,调用迭代器的 next() 方法就可以获得它。条目全部取出后,会引发一个 StopIteration 异常,这并不表示错误发生,只是告诉外部调用者,迭代完成。
不过,迭代器也有一些限制。例如你不能向后移动,不能回到开始,也不能复制一个迭代器。如果你要再次(或者是同时)迭代同个对象,你只能去创建另一个迭代器对象。不过,这并不糟糕,因为还有其它的工具帮助你使用迭代器。
reversed() 内建函数将返回一个反序访问的迭代器。enumerate() 内建函数同样也返回迭代器。另外两个内建函数,any() 和 all(),是在 Python2.5 中新增的,如果迭代器中某个/所有条目的值都为布尔真时,则他们返回值为真。在 for 循环中通过索引或是迭代对象可以遍历条目。同时 Python 还提供一整个 itertools 模块,它包含各种有用的迭代器。

2.4 使用迭代器

1. 序列
迭代序列对象:

>>> myTuple = (123, 'xyz', 45.67)
>>> i = iter(myTuple)
>>> next(i)
123
>>> next(i)
'xyz'
>>> next(i)
45.67
>>> next(i)
Traceback (most recent call last):
  File "", line 1, in <module>
StopIteration

如果这是一个实际应用程序,那么我们需要把代码放在一个 try-except 块中。序列现在会自动地产生它们自己的迭代器,所以一个 for 循环:

for i in seq:
    do_something_to(i)

under the covers now really behaves like this:
(实际上是这样工作的:)

fetch = iter(seq)
while True:
    try:
        i = next(fetch)
    except StopIteration:
        break
    do_something_to(i)

不过,你不需要改动你的代码,因为 for 循环会自动调用迭代器的 next() 方法(以及监视 StopIteration 异常)。

2. 字典
字典和文件是另外两个可迭代的 Python 数据类型。字典的迭代器会遍历它的键(key)。语句 for eachKey in myDict.keys() 可以缩写为 for eachKey in myDict,例如:

>>> legends = {
...     ('Poe', 'author'): (1809, 1849, 1976),
...     ('Gaudi', 'architect'): (1852, 1906, 1987),
...     ('Freud', 'psychoanalyst'): (1856, 1939,1990)
... }
>>> for eachLegend in legends:
...     print('Name: %s\tOccupation: %s' % eachLegend)
...     print('  Birth: %s\tDeath: %s\tAlbum: %s\n' % legends[eachLegend])
...
Name: Poe       Occupation: author
  Birth: 1809   Death: 1849     Album: 1976

Name: Gaudi     Occupation: architect
  Birth: 1852   Death: 1906     Album: 1987

Name: Freud     Occupation: psychoanalyst
  Birth: 1856   Death: 1939     Album: 1990

python2 中具有三个内建字典方法进行迭代:myDict.iterkeys()(通过键迭代)、myDict.itervalues()(通过值迭代)及 myDict.iteritems()(通过键-值对来迭代)。
python3 优化了 myDict.keys()、myDict.values() 及 myDict.items(),取消了上述三个内置字典方法。

3. 文件
文件对象生成的迭代器会自动调用 readline() 方法。这样,循环就可以访问文本文件的所有行。我们可以使用简单的 for eachLine in myFile 替换 for eachLine in myFile.readlines():

>>> myFile = open('config-win.txt')
>>> for eachLine in myFile:
...     print(eachLine.strip())
...     
[EditorWindow]
font-name: courier new
font-size: 10
>>> myFile.close()

2.5 可变对象和迭代器

记住,在迭代可变对象的时候修改它们并不是个好主意。这在迭代器出现之前就是一个问题。一个流行的例子就是循环列表的手删除满足(或不满足)特定条件的项:

for eachURL in allURLs:
    if not eachURL.startswith('http://'):
        allURLs.remove(eachURL)            # YIKES!!

除列表外的其他序列都是不可变的,所以危险就发生在这里。一个序列的迭代器只是记录你当前到达第多少个元素,所以如果你在迭代时改变了元素,更新会立即反映到你所迭代的条目上。在迭代字典的键时,你绝对不能改变这个字典。使用字典的 keys() 方法是可以的,因为 keys() 返回一个独立于字典的列表。而迭代器是与实际对象绑定在一起的,它将不会继续执行下去:

>>> myDict = {'a': 1, 'b': 2, 'c': 3, 'd': 4}
>>> for eachKey in myDict:
...     print(eachKey, myDict[eachKey])
...     del myDict[eachKey]
...
a 1
Traceback (most recent call last):
  File "", line 1, in <module>
RuntimeError: dictionary changed size during iteration

这样可以避免有缺陷的代码。更多有关迭代器的细节请参阅 PEP234。

2.6 如何创建

1. iter() 函数
对一个对象调用 iter() 就可以得到它的迭代器。它的语法如下:
iter(obj)
iter(func, sentinel)
如果你传递一个参数给 iter(),它会检查你传递的是不是一个序列,如果是,那么很简单:根据索引从 0 一直迭代到序列结束。另一个创建迭代器的方法是使用类,一个实现了 __iter__() 和 __next__() 方法的类可以作为迭代器使用。
如果是传递两个参数给 iter(),它会重复地调用 func,直到迭代器的下个值等于 sentinel。

2. 可迭代的类
把一个类作为一个迭代器使用需要在类中实现两个方法 __iter__() 与 __next__() 。
__iter__() 方法返回一个特殊的迭代器对象, 这个迭代器对象实现了 __next__() 方法并通过 StopIteration 异常标识迭代的完成。
__next__() 方法(Python 2 里是 next())会返回下一个迭代器对象。

创建一个返回数字的迭代器,初始值为 1,逐步递增 1:

class MyNumbers:
    def __iter__(self):
        self.a = 1
        return self

    def __next__(self):
        x = self.a
        self.a += 1
        return x


mynum = MyNumbers()
myiter = iter(mynum)

print(next(myiter))
print(next(myiter))
print(next(myiter))
print(next(myiter))
print(next(myiter))
1
2
3
4

StopIteration 异常用于标识迭代的完成,防止出现无限循环的情况,在 next() 方法中我们可以设置在完成指定循环次数后触发 StopIteration 异常来结束迭代。在 10 次迭代后停止执行:

class MyNumbers:
    def __iter__(self):
        self.a = 1
        return self

    def __next__(self):
        if self.a <= 10:
            x = self.a
            self.a += 1
            return x
        else:
            raise StopIteration

mynum = MyNumbers()
myiter = iter(mynum)

for x in myiter:
    print(x)
1
2
3
4
5
6
7
8
9
10

3. 生成器

前面我们讨论了迭代器背后的有效性以及它们如何给非序列对象一个像序列的迭代器接口。这很容易明白因为他们仅仅只有一个方法,用于调用获得下个元素的next()。然而,除非你实现了一个迭代器的类,迭代器真正的并没有那么“聪明“。难道调用函数还没有强大到在迭代中以某种方式生成下一个值并且返回和next()调用一样简单的东西?那就是生成器的动机之一。
生成器的另外一个方面甚至更加强力——协同程序的概念。协同程序是可以运行的独立函数调用,可以暂停或者挂起,并从程序离开的地方继续或者重新开始。在有调用者和(被调用的)协同程序也有通信。举例来说,当协同程序暂停的时候,我们能从其中获得一个中间的返回值,当调用回到程序中时,能够传入额外或者改变了的参数,但仍能够从我们上次离开的地方继续,并且所有状态完整。挂起返回出中间值并多次继续的协同程序被称为生成器,那就是python 的生成器真正在做的事。在2.2 的时候,生成器被加入到python 中接着在2.3 中成为标准(见PEP255),虽然之前足够强大,但是在Python2.5 的时候,得到了显著的提高(见PEP342)。这些提升让生成器更加接近一个完全的协同程序,因为允许值(和异常)能传回到一个继续的函数中。同样地,当等待一个生成器的时候,生成器现在能返回控制。在调用的生成器能挂起(返回一个结果)之前,调用生成器返回一个结果而不是阻塞等待那个结果返回。让我们更进一步观察生成器自顶向下的启动.
什么是python 式的生成器?从句法上讲,生成器是一个带yield 语句的函数。一个函数或者子程序只返回一次,但一个生成器能暂停执行并返回一个中间的结果----那就是yield 语句的功能, 返回一个值给调用者并暂停执行。当生成器的next()方法被调用的时候,它会准确地从离开地方继续(当它返回[一个值以及]控制给调用者时)。

3.1 简单的生成器特性

与迭代器相似,生成器以另外的方式来运作:当到达一个真正的返回或者函数结束没有更多的
值返回(当调用next()),一个StopIteration 异常就会抛出。这里有个例子,简单的生成器:

def simpleGen():
    yield 1
    yield '2 --> punch!'

现在我们有自己的生成器函数,让我们调用他来获得和保存一个生成器对象(以便我们能调用它的next()方法从这个对象中获得连续的中间值):

>>> myG = simpleGen()
>>> next(myG)
1
>>> next(myG)
'2 --> punch!'
>>> next(myG)
Traceback (most recent call last):
  File "", line 1, in <module>
StopIteration

由于python 的for 循环有next()调用和对StopIteration 的处理,使用一个for 循环而不是手
动迭代穿过一个生成器(或者那种事物的迭代器)总是要简洁漂亮得多。

>>> for eachItem in simpleGen():
...     print(eachItem)
...
1
2 --> punch!

当然这是个挺傻的例子:为什么不对这使用真正的迭代器呢?许多动机源自能够迭代穿越序列,而这需要函数威力而不是已经在某个序列中静态对象。

在接下来的例子中,我们将要创建一个带序列并从那个序列中返回一个随机元素的随机迭代器:

from random import randint
def randGen(aList):
    while len(aList) > 0:
        yield aList.pop(randint(0, len(aList)-1))
>>> for item in randGen(['rock', 'paper', 'scissors']):
...     print(item)
...     
rock
paper
scissors

这些简单的例子应该让你有点明白生成器是如何工作的,但你或许会问。"在我的应用中,我可以在哪使用生成器?“或许,你会问“最适合使用这些个强大的构建的地方在哪?“
使用生成器最好的地方就是当你正迭代穿越一个巨大的数据集合,而重复迭代这个数据集合是一个很麻烦的事,比如一个巨大的磁盘文件,或者一个复杂的数据库查询。对于每行的数据,你希望执行非元素的操作以及处理,但当正指向和迭代过它的时候,你“不想失去你的地盘“。
你想要抓取一块数据,比如,将它返回给调用者来处理以及可能的对(另外一个)数据库的插入,接着你想要运行一次next()来获得下一块的数据,等等。状态在挂起和再继续的过程中是保留了的,所以你会觉得很舒服有一个安全的处理数据的环境。没有生成器的话,你的程序代码很有可能会有很长的函数,里面有一个很长的循环。当然,这仅仅是因为一个语言这样的特征不意味着你需要用它。如果在你程序里没有明显适合的话,那就别增加多余的复杂性!当你遇到合适的情况时,你便会知道什么时候生成器正是要使用的东西。

3.2 加强的生成器特性

在python2.5 中,一些加强特性加入到生成器中,所以除了next()来获得下个生成的值,用户可以将值回送给生成器[send()],在生成器中抛出异常,以及要求生成器退出[close()]。
由于双向的动作涉及到叫做 send()的代码来向生成器发送值(以及生成器返回的值发送回来),现在yield 语句必须是一个表达式,因为当回到生成器中继续执行的时候,你或许正在接收一个进入的对象。下面是一个展示了这些特性的,简单的例子。我们用简单的闭包例子,counter:

def counter(start_at=0):
    count = start_at
    while True:
        val = (yield count)
        if val is not None:
            count = val
        else:
            count += 1

生成器带有一个初始化的值,对每次对生成器[next()]调用以1 累加计数。用户已可以选择重置这个值,如果他们非常想要用新的值来调用send()不是调用next()。这个生成器是永远运行的,所以如果你想要终结它,调用close()方法。如果我们交互的运行这段代码,会得到如下输出:

>>> count = counter(5)
>>> next(count)
5
>>> next(count)
6
>>> count.send(9)
9
>>> next(count)
10
>>> count.close()
>>> next(count)
Traceback (most recent call last):
  File "", line 1, in <module>
StopIteration

你可以在PEP 的255 和342 中,以及给读者介绍python2.2 中新特性的linux 期刊文章中阅读
到更多关于生成器的资料:
http://www.linuxjournal.com/article/5597

3.3 生成器表达式

生成器表达式是列表解析的一个扩展. 在 Python 2.0 中我们加入了列表解析, 使语言有了一次革命化的发展, 提供给用户了一个强大的工具, 只用一行代码就可以创建包含特定内容的列表.你可以去问一个有多年 Python 经验的程序员是什么改变了他们编写 Python 程序的方式, 那么列表解析一定会是最多的答案.
列表解析的一个不足就是必要生成所有的数据, 用以创建整个列表. 这可能对有大量数据的迭代器有负面效应. 生成器表达式通过结合列表解析和生成器解决了这个问题.生成器表达式在 Python 2.4 被引入, 它与列表解析非常相似,而且它们的基本语法基本相同;不过它并不真正创建数字列表, 而是返回一个生成器,这个生成器在每次计算出一个条目后,把这个条目“产生”(yield)出来. 生成器表达式使用了"延迟计算"(lazy evaluation), 所以它在使用内存上更有效. 我们来看看它和列表解析到底有多相似:
列表解析:

[expr for iter_var in iterable if cond_expr]

生成器表达式:

(expr for iter_var in iterable if cond_expr)

生成器并不会让列表解析废弃, 它只是一个内存使用更友好的结构, 基于此, 有很多使用生成器地方. 下面我们提供了一些使用生成器表达式的例子, 最后例举一个冗长的样例, 从它你可以感觉到 Python 代码在这些年来的变化.

1. 磁盘文件样例
在前边列表解析一节, 我们计算文本文件中非空白字符总和. 最后的代码中, 我们展示了如何使用一行列表解析代码做所有的事. 如果这个文件的大小变得很大, 那么这行代码的内存性能会很低, 因为我们要创建一个很长的列表用于存放单词的长度.
为了避免创建庞大的列表, 我们可以使用生成器表达式来完成求和操作. 它会计算每个单词的长度然后传递给 sum() 函数(它的参数不仅可以是列表,还可以是可迭代对象,比如生成器表达式).这样, 我们可以得到优化后的代码(代码长度, 还有执行效率都很高效):

>>> f = open('hhga.txt', 'r')
>>> sum(len(word) for line in f for word in line.split())
408

我们所做的只是把方括号删除: 少了两字节, 而且更节省内存 … 非常地环保!

2. 交叉配对例子
生成器表达式就好像是懒惰的列表解析(这反而成了它主要的优势). 它还可以用来处理其他列
表或生成器, 例如这里的 rows 和 cols :

rows = [1, 2, 3, 17]

def cols():
    yield 56
    yield 2
    yield 1

不需要创建新的列表, 直接就可以创建配对. 我们可以使用下面的生成器表达式:

x_product_pairs = ((i, j) for i in rows for j in cols())

现在我们可以循环 x_product_pairs , 它会懒惰地循环 rows 和 cols :

>> for pair in x_product_pairs:
...     print(pair)
...     
(1, 56)
(1, 2)
(1, 1)
(2, 56)
(2, 2)
(2, 1)
(3, 56)
(3, 2)
(3, 1)
(17, 56)
(17, 2)
(17, 1)

3. 重构样例
我们通过一个寻找文件最长的行的例子来看看如何改进代码. 在以前, 我们这样读取文件:

f = open('hhga.txt', 'r')
longest = 0
while True:
    linelen = len(f.readline().strip())
    if not linelen: break
    if linelen > longest:
        longest = linelen
f.close()
print(longest)

事实上, 这还不够老. 真正的旧版本 Python 代码中, 布尔常量应该写是整数 1 , 而且我们应该使用 string 模块而不是字符串的 strip() 方法:

import string
    :
    len(string.strip(f.readline()))

从那时起, 我们认识到如果读取了所有的行, 那么应该尽早释放文件资源. 如果这是一个很多进程都要用到的日志文件, 那么理所当然我们不能一直拿着它的句柄不释放. 是的, 我们的例子是用来展示的, 但是你应该得到这个理念. 所以读取文件的行的首选方法应该是这样:

f = open('hhga.txt', 'r')
longest = 0
allLines = f.readlines()
f.close()
for line in allLines:
    linelen = len(line.strip())
    if linelen > longest:
        longest = linelen
print(longest)

列表解析允许我们稍微简化我们代码, 而且我们可以在得到行的集合前做一定的处理. 在下段代码中,除了读取文件中的行之外,我们还调用了字符串的 strip() 方法处理行内容.

f = open('hhga.txt', 'r')
longest = 0
allLines = [x.strip() for x in f.readlines()]
f.close()
for line in allLines:
    linelen = len(line)
    if linelen > longest:
        longest = linelen
print(longest)

然而, 两个例子在处理大文件时候都有问题, 因为 readlines() 会读取文件的所有行. 后来我们有了迭代器, 文件本身就成为了它自己的迭代器, 不需要调用 readlines() 函数. 我们已经做到了这一步, 为什么不去直接获得行长度的集合呢(之前我们得到的是行的集合)? 这样, 我们就可以使用 max() 内建函数得到最长的字符串长度:

f = open('hhga.txt', 'r')
allLines = [len(x.strip()) for x in f]
f.close()
longest = max(allLines)
print(longest)

这里唯一的问题就是你一行一行迭代 f 的时候, 列表解析需要文件的所有行读取到内存中, 然后生成列表. 我们可以进一步简化代码: 使用生成器表达式替换列表解析, 然后把它移到 max() 函数里, 这样, 所有的核心部分只有一行:

f = open('hhga.txt', 'r')
longest = max(len(x.strip()) for x in f)
f.close()
print(longest)

最后, 我们可以去掉文件打开模式(默认为读取), 然后让 Python 去处理打开的文件. 当然, 文件用于写入的时候不能这么做, 但这里我们不需要考虑太多:

print(max(len(x.strip()) for x in open('hhga.txt', 'r')))

我们走了好长一段路. 注意,即便是这只有一行的 Python 程序也不是很晦涩. 生成器表达式在 Python 2.4 中被加入, 你可以在 PEP 289 中找到更多相关内容.

你可能感兴趣的:(深入理解,Python,笔记)