Python 容器使用的 5 个技巧和 2 个误区( 六 )


 
常见误区1. 当心那些已经枯竭的迭代器在文章前面,我们提到了使用“懒惰”生成器的种种好处 。但是,所有事物都有它的两面性 。生成器的最大的缺点之一就是:它会枯竭 。当你完整遍历过它们后,之后的重复遍历就不能拿到任何新内容了 。
 

  1. numbers = [1, 2, 3]
  2. numbers = (i * 2 for i in numbers)

  3. # 第一次循环会输出 2, 4, 6
  4. for number in numbers:
  5. print(number)

  6. # 这次循环什么都不会输出,因为迭代器已经枯竭了
  7. for number in numbers:
  8. print(number)
而且不光是生成器表达式,Python 3 里的 map、filter 内建函数也都有一样的特点 。忽视这个特点很容易导致代码中出现一些难以察觉的 Bug 。
Instagram 就在项目从 Python 2 到 Python 3 的迁移过程中碰到了这个问题 。它们在 PyCon 2017 上分享了对付这个问题的故事 。访问文章 Instagram 在 PyCon 2017 的演讲摘要,搜索“迭代器”可以查看详细内容 。
 
2. 别在循环体内修改被迭代对象这是一个很多 Python 初学者会犯的错误 。比如,我们需要一个函数来删掉列表里的所有偶数:
 
  1. def remove_even(numbers):
  2. """去掉列表里所有的偶数
  3. """
  4. for i, number in enumerate(numbers):
  5. if number % 2 == 0:
  6. # 有问题的代码
  7. del numbers[i]


  8.  
  9. numbers = [1, 2, 7, 4, 8, 11]
  10. remove_even(numbers)
  11. print(numbers)
  12. # OUTPUT: [1, 7, 8, 11]
注意到结果里那个多出来的 “8” 了吗?当你在遍历一个列表的同时修改它,就会出现这样的事情 。因为被迭代的对象 numbers在循环过程中被修改了 。遍历的下标在不断增长,而列表本身的长度同时又在不断缩减 。这样就会导致列表里的一些成员其实根本就没有被遍历到 。
所以对于这类操作,请使用一个新的空列表保存结果,或者利用 yield返回一个生成器 。而不是修改被迭代的列表或是字典对象本身 。
 
总结在这篇文章中,我们首先从“容器类型”的定义出发,在底层和高层两个层面探讨了容器类型 。之后遵循系列文章传统,提供了一些编写容器相关代码时的技巧 。
让我们最后再总结一下要点:
  • 了解容器类型的底层实现,可以帮助你写出性能更好的代码
  • 提炼需求里的抽象概念,面向接口而非实现编程
  • 多使用“懒惰”的对象,少生成“迫切”的列表
  • 使用元组和字典可以简化分支代码结构
  • 使用 next函数配合迭代器可以高效完成很多事情,但是也需要注意“枯竭”问题
  • collections、itertools 模块里有非常多有用的工具,快去看看吧!
看完文章的你,有没有什么想吐槽的?请留言或者在 项目 Github Issues 告诉我吧 。
 
注解
  1. Python 这门语言除了 CPython 外,还有许许多多的其他版本实现 。如无特别说明,本文以及 “Python 工匠” 系列里出现的所有 Python 都特指 Python 的 C 语言实现 CPython
  2. Python 里没有类似其他编程语言里的“Interface 接口”类型,只有类似的“抽象类”概念 。为了表达方便,后面的内容均统一使用“接口”来替代“抽象类” 。
  3. 有没有只实现了 Mapping 但又不是 MutableMapping 的类型?试试 MappingProxyType({})
  4. 有没有只实现了 Set 但又不是 MutableSet 的类型?试试 frozenset
     




推荐阅读