考虑使用生成器而不是返回列表


对于函数而言,处理一个结果集序列的最简单的方式就是返回一个元素的集合。例如:你想知道一个字符串中每个单词的索引下标。这里,我使用append方法将结果存储于一个列表中,并在函数的最后将这个结果集返回。

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 ad sever years ago..."
result = index_words(address)
print(result[:3])
>>>
[0, 5, 11]

虽然结果可以正常的获取,但是这个函数中却存在两个问题。

  • 一个是代码有点杂乱。没一个新结果被发现就会调用一次append方法。将追加元素之的工作变得散漫化了。并且代码中初始化result集合的时候占据了一行代码,返回结果集result的时候也使用了一行代码。而且代码块中大概有130个字符,但是只有约75个字符是有用的。所以整体看起来,代码的效率和简洁性都不是很高。

    针对上述情况,一个更好一点的解决办法就是使用generator(生成器)。生成器就是使用了yield表达式的函数。当此函数被调用的时候 他不会真正的去执行,而是返回一个迭代器。每次调用这个内置的函数,迭代器就会把生成器推进到下一个yield表达式上。每个被迭代器经过的值就会被返回给调用者。这里我做了个关于生成其表达式的小例子,功能和前面的那段代码一致。

    def index_words_iter(text):
      if text:
          yield 0
      for index, letter in enumerate(text):
          if letter ==" ":
              yield index + 1
    

    很明显,由于淘汰了result集合内部元素之间的相互作用,代码变的更加容易了。结果集被传递到了yield表达式上面。被生成器调用而返回的迭代器可以轻松的被传递给内置的列表函数(详见第9项:为大段代码考虑使用生成器)而转换成我们预期的结果集。

    result = list(index_words_iter(address))
    
  • 第二个问题就是index_words函数需要列表中所有的结果元素是排好序的。对于大量的输入,这就很有可能引发内存危机甚至崩溃。相反,使用了生成器的那个版本就可以很轻松的适配大量数据的输入处理。

    这里,我定义了一个生成器的一次一行的文件流处理例子,并且使用yield来进行一次一个单词的输出。代码运行的内存被限制在了每行输入数据的最大长度。

    def index_file(handle):
      offset = 0
      for line in handle:
          if line:
              yield = offset
          for letter in line:
              offset += 1
              if letter == " ":
                  yield offset
    # 测试运行此函数
    with open('/tmp/address.txt','r')as f:
      it = index_file(f)
      results = islice(it, 0, 3)
      print(results)
    >>>
    [0, 5, 11]
    

是不是很完美啊,唯一的缺点就是此函数的调用方必须了解这个迭代器是状态相关的,并且不能被重用(详见第17项:迭代参数的时候记得保守一点点)。


备忘录

  • 相较于返回一个列表的情况,替代方案中使用生成器可以使得代码变得更加的清晰。
  • 生成器返回的迭代器,是在其生成器内部一个把值传递给了yield变量的集合。
  • 生成器可以处理很大的输出序列就是因为它在处理的时候不会完全的包含所有的数据。

results matching ""

    No results matching ""