第 30 条 不要让函数直接返回列表,应该让它逐个生成列表里的值
如果函数要返回的是个包含许多结果的序列,那么最简单的办法是把这戏结果放到列表中。例如,我们要返回字符串里每个单词的首字母所对应的下标。下面这种写法,会把每次遇到的新单词所在的位置追加到存放结果的 result 列表中,在函数末尾返回这个列表。
def index_words(text):
result = []
if text:
result.append(0)
for index, letter in enumerate(text):
if letter == ' ':
result.append(index + 1)
return result
我们把一段实例文档传给这个函数,它可以返回正确的结果。
address = 'Four score and seven years ago...'
result = index_words(address)
print(result[:10])
>>>
[0, 5, 11, 15, 21, 27]
index_words 函数有两个缺点。
第一个缺点是,它的代码看起来有点杂乱。每找到一个新单词,它都要调用 append 方法,而调用这个方法时,必须写上 result.append 这样一串字符串,这就把我们想要强调的重点,也就是这个新单词在字符串中的位置(index + 1)淡化了。另外,函数还必须专门用一行代码创建这个保存结果的 result 列表,并且要用一条 return 语句把它返回给调用者。这样算下来,虽然函数的主体部分大约有 130 个字符(非空白的),但真正重要的大约在 75 个左右。
这种函数改用 生成器来实现会比较好。生成器由包含 yield 表达式的函数创建。下面就定义一个生成器函数,实现与刚才那个函数相同的效果。
def index_words(text):
if text:
yield 0
for index, letter in enumerate(text):
if letter == ' ':
yield index + 1
调用生成器函数并不会让其中的代码立刻得到执行,它会返回一个迭代器。把这个迭代器传给 Python 内置函数的 next 函数,就可以把生成器函数推进到它的下一条表达式。生成器会把 yield 表达式的值通过迭代器返回给调用者。
address = 'Four score and seven years ago...'
it = index_words(address)
print(next(it))
print(next(it))
>>>
0
5
这次的 index_words_list 函数,比刚才的那个函数好懂的多,因为它把设计到列表的操作全部都简化掉了。它通过 yield 表达式来传递结果,而不像刚才那样,要把结果返回到列表之中。如果确实要制作一份列表,那可以把生成器函数返回到迭代器传递内置函数(原理参见第 32 条)。
address = 'Four score and seven years ago...'
it = list(index_words(address))
print(it[:10])
>>>
[0, 5, 11, 15, 21, 27]
index_words 函数的第二个缺点是,它必须把所有结果保存到列表中,然后才能返回列表。如果输入到数据特别多,那么程序可能会因为耗尽内存而崩溃。
相反,采用生成器函数来实现,就不会有那么多问题。它可以接受长度任意多输入信息,并把内存消耗量压得比较低。例如下面这个生成器,只需要把当前这行文字从文件度进来就行,每次推进的时候,它都只处理一个单词,直到把这行文字处理完毕,才读入下一行文字。
import itertools
def index_file(handle):
offset = 0
for line in handle:
if line:
yield offset
for letter in line:
offset += 1
if letter == ' ':
yield offset
该函数运行时所消耗的内存,取决于文件中最长的那一行包含的字符数。把刚才那份输入数据存放在 address.txt 文件,让这个函数去读取并用它返回的生成器构建一份列表,可以看到跟原来相同的效果(islice 函数的详细用法,参见第 36 条)。
with open ('address.txt', 'r') as f:
it = index_file(f)
results = itertools.islice(it, 0 ,10)
print(list(results))
>>>
[0, 5, 11, 15, 21, 27, 31, 35, 43, 51]
定义这种生成器函数的时候,只有一个地方需要注意,就是调用者无法重复使用函数所返回的迭代器,因为这些迭代器是有状态的(参见 第 31 条)。