Merge pull request #20 from catbaron0/master

Update 4-mastering-container-types.md
This commit is contained in:
piglei™ 2019-06-11 11:49:47 +08:00 committed by GitHub
commit 27067c132a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 12 additions and 12 deletions

View File

@ -45,15 +45,15 @@ Python 语言自身的内部实现细节也与这些容器类型息息相关。
我在前面给了“容器”一个简单的定义:*专门用来装其他对象的就是容器*。但这个定义太宽泛了,无法对我们的日常编程产生什么指导价值。要真正掌握 Python 里的容器,需要分别从两个层面入手:
- **底层实现**内置容器类型使用了什么数据结构?某项操作如何工作?
- **高层抽象**什么决定了某个对象是不是容器?哪些行为定义了容器?
- **底层实现**内置容器类型使用了什么数据结构?某项操作如何工作?
- **高层抽象**什么决定了某个对象是不是容器?哪些行为定义了容器?
下面,让我们一起站在这两个不同的层面上,重新认识容器。
## 底层看容器
Python 是一门高级编程语言,**它所提供的内置容器类型,都是经过高度封装和抽象后的结果**和“链表”、“红黑树”、“哈希表”这些名字相比,所有 Python 内建类型的名字,都只描述了这个类型的功能特点,其他人完全没法只通过这些名字了解它们的哪怕一丁点内部细节。
Python 是一门高级编程语言,**它所提供的内置容器类型,都是经过高度封装和抽象后的结果**和“链表”、“红黑树”、“哈希表”这些名字相比,所有 Python 内建类型的名字,都只描述了这个类型的功能特点,其他人完全没法只通过这些名字了解它们的哪怕一丁点内部细节。
这是 Python 编程语言的优势之一。相比 C 语言这类更接近计算机底层的编程语言Python 重新设计并实现了对编程者更友好的内置容器类型,屏蔽掉了内存管理等额外工作。为我们提供了更好的开发体验。
@ -73,7 +73,7 @@ Python 是一门高级编程语言,**它所提供的内置容器类型,都
在 Python 2 中,如果你调用 `range(100000000)`,需要等待好几秒才能拿到结果,因为它需要返回一个巨大的列表,花费了非常多的时间在内存分配与计算上。但在 Python 3 中,同样的调用马上就能拿到结果。因为函数返回的不再是列表,而是一个类型为 `range` 的懒惰对象,只有在你迭代它、或是对它进行切片时,它才会返回真正的数字给你。
**所以说,为了提高性能,内建函数 `range` “变懒”了。**而为了避免过于频繁的内存分配,在日常编码中,我们的函数同样也需要变懒,这包括:
**所以说,为了提高性能,内建函数 `range` “变懒”了。** 而为了避免过于频繁的内存分配,在日常编码中,我们的函数同样也需要变懒,这包括:
- 更多的使用 `yield` 关键字,返回生成器对象
- 尽量使用生成器表达式替代列表推导表达式
@ -114,9 +114,9 @@ def validate_name(name):
## 高层看容器
Python 是一门“[鸭子类型](https://en.wikipedia.org/wiki/Duck_typing)”语言:*“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。”*所以,当我们说某个对象是什么类型时,在根本上其实指的是:**这个对象满足了该类型的特定接口规范,可以被当成这个类型来使用。**而对于所有内置容器类型来说,同样如此。
Python 是一门“[鸭子类型](https://en.wikipedia.org/wiki/Duck_typing)”语言:*“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。”* 所以,当我们说某个对象是什么类型时,在根本上其实指的是: **这个对象满足了该类型的特定接口规范,可以被当成这个类型来使用。** 而对于所有内置容器类型来说,同样如此。
打开位于 [collections](https://docs.python.org/3.7/library/collections.html) 模块下的 [abc](https://docs.python.org/3/library/collections.abc.html)*(“抽象类 Abstract Base Classes”的首字母缩写*子模块,可以找到所有与容器相关的接口(抽象类)[[注2]](#annot2)定义。让我们分别看看那些内建容器类型都满足了什么接口:
打开位于 [collections](https://docs.python.org/3.7/library/collections.html) 模块下的 [abc](https://docs.python.org/3/library/collections.abc.html)*(“抽象类 Abstract Base Classes”的首字母缩写* 子模块,可以找到所有与容器相关的接口(抽象类)[[注2]](#annot2)定义。让我们分别看看那些内建容器类型都满足了什么接口:
- **列表list**:满足 `Iterable`、`Sequence`、`MutableSequence` 等接口
- **元组tuple**:满足 `Iterable`、`Sequence`
@ -169,7 +169,7 @@ print("\n".join(add_ellipsis(comments)))
#### 面向容器接口编程
我们需要改进函数来避免这个问题。因为 `add_ellipsis` 函数强依赖了列表类型,所以当参数类型变为元组时,现在的函数就不再适用了*(原因:给 `comments[index]` 赋值的地方会抛出 `TypeError` 异常)*。如何改善这部分的设计?秘诀就是:**让函数依赖“可迭代对象”这个抽象概念,而非实体列表类型。**
我们需要改进函数来避免这个问题。因为 `add_ellipsis` 函数强依赖了列表类型,所以当参数类型变为元组时,现在的函数就不再适用了*(原因:给 `comments[index]` 赋值的地方会抛出 `TypeError` 异常)。* 如何改善这部分的设计?秘诀就是:**让函数依赖“可迭代对象”这个抽象概念,而非实体列表类型。**
使用生成器特性,函数可以被改成这样:
@ -203,7 +203,7 @@ with open("comments") as fp:
将依赖由某个具体的容器类型改为抽象接口后,函数的适用面变得更广了。除此之外,新函数在执行效率等方面也都更有优势。现在让我们再回到之前的问题。**从高层来看,什么定义了容器?**
答案是:**各个容器类型实现的接口协议定义了容器。**不同的容器类型在我们的眼里,应该是 `是否可以迭代`、`是否可以修改`、`有没有长度` 等各种特性的组合。我们需要在编写相关代码时,**更多的关注容器的抽象属性,而非容器类型本身**,这样可以帮助我们写出更优雅、扩展性更好的代码。
答案是: **各个容器类型实现的接口协议定义了容器。** 不同的容器类型在我们的眼里,应该是 `是否可以迭代`、`是否可以修改`、`有没有长度` 等各种特性的组合。我们需要在编写相关代码时,**更多的关注容器的抽象属性,而非容器类型本身**,这样可以帮助我们写出更优雅、扩展性更好的代码。
> Hint在 [itertools](https://docs.python.org/3/library/itertools.html) 内置模块里可以找到更多关于处理可迭代对象的宝藏。
@ -248,9 +248,9 @@ print(from_now(now - 87500))
# 1 days ago
```
上面这个函数挑不出太多毛病,很多很多人都会写出类似的代码。但是,如果你仔细观察它,可以在分支代码部分找到一些明显的**边界**。比如,当函数判断某个时间是否应该用“秒数”展示时,用到了 `60`。而判断是否应该用分钟时,用到了 `3600`
上面这个函数挑不出太多毛病,很多很多人都会写出类似的代码。但是,如果你仔细观察它,可以在分支代码部分找到一些明显的**边界** 比如,当函数判断某个时间是否应该用“秒数”展示时,用到了 `60`。而判断是否应该用分钟时,用到了 `3600`
**从边界提炼规律是优化这段代码的关键。**如果我们将所有的这些边界放在一个有序元组中,然后配合二分查找模块 [bisect](https://docs.python.org/3.7/library/bisect.html)。整个函数的控制流就能被大大简化:
**从边界提炼规律是优化这段代码的关键。** 如果我们将所有的这些边界放在一个有序元组中,然后配合二分查找模块 [bisect](https://docs.python.org/3.7/library/bisect.html)。整个函数的控制流就能被大大简化:
```python
import bisect
@ -360,7 +360,7 @@ def counter_by_collections(l):
return result
```
这样的代码既不用“获取许可”,也无需“请求原谅”。**整个代码的控制流变得更清晰自然了。**所以,如果可能的话,请尽量想办法省略掉那些**非核心**的异常捕获逻辑。一些小提示:
这样的代码既不用“获取许可”,也无需“请求原谅”。 **整个代码的控制流变得更清晰自然了。** 所以,如果可能的话,请尽量想办法省略掉那些 **非核心** 的异常捕获逻辑。一些小提示:
- 操作字典成员时:使用 `collections.defaultdict` 类型
- 或者使用 `dict[key] = dict.setdefault(key, 0) + 1` 内建函数
@ -371,7 +371,7 @@ def counter_by_collections(l):
### 4. 使用 next() 函数
`next()` 是一个非常实用的内建函数,它接收一个迭代器作为参数,然后返回该迭代器的下一个元素。使用它配合生成器表达式,可以高效的实现*从列表中查找第一个满足条件的成员”*之类的需求。
`next()` 是一个非常实用的内建函数,它接收一个迭代器作为参数,然后返回该迭代器的下一个元素。使用它配合生成器表达式,可以高效的实现*从列表中查找第一个满足条件的成员”* 之类的需求。
```python
numbers = [3, 7, 8, 2, 21]