Merge pull request #79 from wgzhao/fix_md_syntax_error
Fix some markdown syntax errors
This commit is contained in:
commit
df664c3bc2
|
@ -18,25 +18,29 @@
|
|||
|
||||
### 内容目录
|
||||
|
||||
* [如何为变量起名](#如何为变量起名)
|
||||
* [1. 变量名要有描述性,不能太宽泛](#1-变量名要有描述性不能太宽泛)
|
||||
* [2. 变量名最好让人能猜出类型](#2-变量名最好让人能猜出类型)
|
||||
* [『什么样的名字会被当成 bool 类型?』](#什么样的名字会被当成-bool-类型)
|
||||
* [『什么样的名字会被当成 int/float 类型?』](#什么样的名字会被当成-intfloat-类型)
|
||||
* [其他类型](#其他类型)
|
||||
* [3. 适当使用『匈牙利命名法』](#3-适当使用匈牙利命名法)
|
||||
* [4. 变量名尽量短,但是绝对不要太短](#4-变量名尽量短但是绝对不要太短)
|
||||
* [使用短名字的例外情况](#使用短名字的例外情况)
|
||||
* [5. 其他注意事项](#5-其他注意事项)
|
||||
* [更好的使用变量](#更好的使用变量)
|
||||
* [1. 保持一致性](#1-保持一致性)
|
||||
* [2. 尽量不要用 globals()/locals()](#2-尽量不要用-globalslocals)
|
||||
* [3. 变量定义尽量靠近使用](#3-变量定义尽量靠近使用)
|
||||
* [4. 合理使用 namedtuple/dict 来让函数返回多个值](#4-合理使用-namedtupledict-来让函数返回多个值)
|
||||
* [5. 控制单个函数内的变量数量](#5-控制单个函数内的变量数量)
|
||||
* [6. 及时删掉那些没用的变量](#6-及时删掉那些没用的变量)
|
||||
* [7. 能不定义变量就不定义](#7-能不定义变量就不定义)
|
||||
* [结语](#结语)
|
||||
- [Python 工匠:善用变量来改善代码质量](#python-工匠善用变量来改善代码质量)
|
||||
- [『Python 工匠』是什么?](#python-工匠是什么)
|
||||
- [变量和代码质量](#变量和代码质量)
|
||||
- [内容目录](#内容目录)
|
||||
- [如何为变量起名](#如何为变量起名)
|
||||
- [1. 变量名要有描述性,不能太宽泛](#1-变量名要有描述性不能太宽泛)
|
||||
- [2. 变量名最好让人能猜出类型](#2-变量名最好让人能猜出类型)
|
||||
- [『什么样的名字会被当成 bool 类型?』](#什么样的名字会被当成-bool-类型)
|
||||
- [『什么样的名字会被当成 int/float 类型?』](#什么样的名字会被当成-intfloat-类型)
|
||||
- [其他类型](#其他类型)
|
||||
- [3. 适当使用『匈牙利命名法』](#3-适当使用匈牙利命名法)
|
||||
- [4. 变量名尽量短,但是绝对不要太短](#4-变量名尽量短但是绝对不要太短)
|
||||
- [使用短名字的例外情况](#使用短名字的例外情况)
|
||||
- [5. 其他注意事项](#5-其他注意事项)
|
||||
- [更好的使用变量](#更好的使用变量)
|
||||
- [1. 保持一致性](#1-保持一致性)
|
||||
- [2. 尽量不要用 globals()/locals()](#2-尽量不要用-globalslocals)
|
||||
- [3. 变量定义尽量靠近使用](#3-变量定义尽量靠近使用)
|
||||
- [4. 合理使用 namedtuple/dict 来让函数返回多个值](#4-合理使用-namedtupledict-来让函数返回多个值)
|
||||
- [5. 控制单个函数内的变量数量](#5-控制单个函数内的变量数量)
|
||||
- [6. 及时删掉那些没用的变量](#6-及时删掉那些没用的变量)
|
||||
- [7. 定义临时变量提升可读性](#7-定义临时变量提升可读性)
|
||||
- [结语](#结语)
|
||||
|
||||
## 如何为变量起名
|
||||
|
||||
|
|
|
@ -50,7 +50,7 @@ users_visited_nz = [
|
|||
]
|
||||
```
|
||||
|
||||
每份数据里面都有着`姓`、`名`、`手机号码`、`旅游时间` 四个字段。基于这份数据,商务同学提出了一个*(听上去毫无道理)*的假设:“去过普吉岛的人,应该对去新西兰旅游也很有兴趣。我们需要从这份数据里,找出那些**去过普吉岛但没有去过新西兰的人**,针对性的卖产品给他们。
|
||||
每份数据里面都有着`姓`、`名`、`手机号码`、`旅游时间` 四个字段。基于这份数据,商务同学提出了一个 *(听上去毫无道理)* 的假设:“去过普吉岛的人,应该对去新西兰旅游也很有兴趣。我们需要从这份数据里,找出那些**去过普吉岛但没有去过新西兰的人**,针对性的卖产品给他们。
|
||||
|
||||
### 第一次蛮力尝试
|
||||
|
||||
|
@ -73,11 +73,11 @@ def find_potential_customers_v1():
|
|||
yield phuket_record
|
||||
```
|
||||
|
||||
因为原始数据里没有*“用户 ID”*之类的唯一标示,所以我们只能把“姓名和电话号码完全相同”作为判断是不是同一个人的标准。
|
||||
因为原始数据里没有 *“用户 ID”* 之类的唯一标示,所以我们只能把“姓名和电话号码完全相同”作为判断是不是同一个人的标准。
|
||||
|
||||
`find_potential_customers_v1` 函数通过循环的方式,先遍历所有去过普吉岛的人,然后再遍历新西兰的人,如果在新西兰的记录中找不到完全匹配的记录,就把它当做“潜在客户”返回。
|
||||
|
||||
这个函数虽然可以完成任务,但是相信不用我说你也能发现。**它有着非常严重的性能问题。**对于每一条去过普吉岛的记录,我们都需要遍历所有新西兰访问记录,尝试找到匹配。整个算法的时间复杂度是可怕的 `O(n*m)`,如果新西兰的访问条目数很多的话,那么执行它将耗费非常长的时间。
|
||||
这个函数虽然可以完成任务,但是相信不用我说你也能发现。**它有着非常严重的性能问题**。对于每一条去过普吉岛的记录,我们都需要遍历所有新西兰访问记录,尝试找到匹配。整个算法的时间复杂度是可怕的 `O(n*m)`,如果新西兰的访问条目数很多的话,那么执行它将耗费非常长的时间。
|
||||
|
||||
为了优化内层循环性能,我们需要减少线性查找匹配部分的开销。
|
||||
|
||||
|
@ -109,7 +109,7 @@ def find_potential_customers_v2():
|
|||
|
||||
### 对问题的重新思考
|
||||
|
||||
让我们来尝试重新抽象思考一下问题的本质。首先,我们有一份装了很多东西的容器 A*(普吉岛访问记录)*,然后给我们另一个装了很多东西的容器 B*(新西兰访问记录)*,之后定义相等规则:“姓名与电话一致”。最后基于这个相等规则,求 A 和 B 之间的**“差集”**。
|
||||
让我们来尝试重新抽象思考一下问题的本质。首先,我们有一份装了很多东西的容器 A*(普吉岛访问记录)*,然后给我们另一个装了很多东西的容器 B*(新西兰访问记录)*,之后定义相等规则:“姓名与电话一致”。最后基于这个相等规则,求 A 和 B 之间的 **“差集”**。
|
||||
|
||||
如果你对 Python 里的集合不是特别熟悉,我就稍微多介绍一点。假如我们拥有两个集合 A 和 B,那么我们可以直接使用 `A - B` 这样的数学运算表达式来计算二者之间的 **差集**。
|
||||
|
||||
|
@ -307,7 +307,7 @@ if not events.is_empty():
|
|||
print(events.list_events_by_range(1, 3))
|
||||
```
|
||||
|
||||
但是,这样并非最好的做法。因为 Python 已经为我们提供了一套对象规则,所以我们不需要像写其他语言的 OO*(面向对象)* 代码那样去自己定义额外方法。我们有更好的选择:
|
||||
但是,这样并非最好的做法。因为 Python 已经为我们提供了一套对象规则,所以我们不需要像写其他语言的 OO *(面向对象)* 代码那样去自己定义额外方法。我们有更好的选择:
|
||||
|
||||
```python
|
||||
|
||||
|
|
|
@ -258,7 +258,7 @@ print(count_vowels('small_file.txt'))
|
|||
2. 为了准备测试用例,我要么提供几个样板文件,要么写一些临时文件
|
||||
3. 而文件是否能被正常打开、读取,也成了我们需要测试的边界情况
|
||||
|
||||
**如果,你发现你的函数难以编写单元测试,那通常意味着你应该改进它的设计。**上面的函数应该如何改进呢?答案是:*让函数依赖“文件对象”而不是文件路径*。
|
||||
**如果,你发现你的函数难以编写单元测试,那通常意味着你应该改进它的设计**。上面的函数应该如何改进呢?答案是:*让函数依赖“文件对象”而不是文件路径*。
|
||||
|
||||
修改后的函数代码如下:
|
||||
|
||||
|
@ -280,7 +280,7 @@ with open('small_file.txt') as fp:
|
|||
print(count_vowels_v2(fp))
|
||||
```
|
||||
|
||||
**这个改动带来的主要变化,在于它提升了函数的适用面。**因为 Python 是“鸭子类型”的,虽然函数需要接受文件对象,但其实我们可以把任何实现了文件协议的 “类文件对象(file-like object)” 传入 `count_vowels_v2` 函数中。
|
||||
**这个改动带来的主要变化,在于它提升了函数的适用面**。因为 Python 是“鸭子类型”的,虽然函数需要接受文件对象,但其实我们可以把任何实现了文件协议的 “类文件对象(file-like object)” 传入 `count_vowels_v2` 函数中。
|
||||
|
||||
而 Python 中有着非常多“类文件对象”。比如 io 模块内的 [StringIO](https://docs.python.org/3/library/io.html#io.StringIO) 对象就是其中之一。它是一种基于内存的特殊对象,拥有和文件对象几乎一致的接口设计。
|
||||
|
||||
|
|
|
@ -167,7 +167,7 @@ if __name__ == '__main__':
|
|||
|
||||
## S:单一职责原则
|
||||
|
||||
SOLID 设计原则里的第一个字母 S 来自于 “Single responsibility principle(单一职责原则)” 的首字母。这个原则认为:**“一个类应该仅仅只有一个被修改的理由。”**换句话说,每个类都应该只有一种职责。
|
||||
SOLID 设计原则里的第一个字母 S 来自于 “Single responsibility principle(单一职责原则)” 的首字母。这个原则认为:**“一个类应该仅仅只有一个被修改的理由。”** 换句话说,每个类都应该只有一种职责。
|
||||
|
||||
而在上面的代码中,`HNTopPostsSpider` 这个类违反了这个原则。因为我们可以很容易的找到两个不同的修改它的理由:
|
||||
|
||||
|
@ -272,7 +272,7 @@ def main():
|
|||
write_posts_to_file(posts, sys.stdout, file_title)
|
||||
```
|
||||
|
||||
将“文件写入”职责拆分为新函数是一个 Python 特色的解决方案,它虽然没有那么 OO*(面向对象)*,但是同样满足“单一职责原则”,而且在很多场景下更灵活与高效。
|
||||
将“文件写入”职责拆分为新函数是一个 Python 特色的解决方案,它虽然没有那么 OO *(面向对象)*,但是同样满足“单一职责原则”,而且在很多场景下更灵活与高效。
|
||||
|
||||
## O:开放-关闭原则
|
||||
|
||||
|
@ -558,7 +558,7 @@ def main():
|
|||
|
||||
在这篇文章中,我通过一个具体的 Python 代码案例,向你描述了 “SOLID” 设计原则中的前两位成员:**“单一职责原则”** 与 **“开放-关闭原则”**。
|
||||
|
||||
这两个原则虽然看上去很简单,但是它们背后蕴藏了许多从好代码中提炼而来的智慧。它们的适用范围也不仅仅局限在 OOP 中。一旦你深入理解它们后,你可能会惊奇的在许多设计模式和框架中发现它们的影子*(比如这篇文章就出现了至少 3 种设计模式,你知道是哪些吗?)*。
|
||||
这两个原则虽然看上去很简单,但是它们背后蕴藏了许多从好代码中提炼而来的智慧。它们的适用范围也不仅仅局限在 OOP 中。一旦你深入理解它们后,你可能会惊奇的在许多设计模式和框架中发现它们的影子 *(比如这篇文章就出现了至少 3 种设计模式,你知道是哪些吗?)*。
|
||||
|
||||
让我们最后再总结一下吧:
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<img src="https://www.zlovezl.cn/static/uploaded/2019/11/neonbrand-CXDw96Oy-Yw-unsplash_w1280.jpg" width="100%" />
|
||||
</div>
|
||||
|
||||
在 [上一篇文章](https://www.zlovezl.cn/articles/write-solid-python-codes-part-1/) 里,我用一个虚拟小项目作为例子,讲解了“SOLID”设计原则中的前两位成员:S*(单一职责原则)*与 O*(开放-关闭原则)*。
|
||||
在 [上一篇文章](https://www.zlovezl.cn/articles/write-solid-python-codes-part-1/) 里,我用一个虚拟小项目作为例子,讲解了“SOLID”设计原则中的前两位成员:S *(单一职责原则)* 与 O *(开放-关闭原则)*。
|
||||
|
||||
在这篇文章中,我将继续介绍 SOLID 原则的第三位成员:**L(里氏替换原则)**。
|
||||
|
||||
|
@ -26,7 +26,7 @@
|
|||
|
||||
## L:里氏替换原则
|
||||
|
||||
同前面的 S 与 O 两个原则的命名方式不同,里氏替换原则*(Liskov Substitution Principle)*是直接用它的发明者 [Barbara Liskov](https://en.wikipedia.org/wiki/Barbara_Liskov) 命名的,原文看起来像一个复杂的数学公式:
|
||||
同前面的 S 与 O 两个原则的命名方式不同,里氏替换原则 *(Liskov Substitution Principle)* 是直接用它的发明者 [Barbara Liskov](https://en.wikipedia.org/wiki/Barbara_Liskov) 命名的,原文看起来像一个复杂的数学公式:
|
||||
|
||||
> Let q(x) be a property provable about objects of x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T.
|
||||
> - 出处: [Liskov substitution principle - Wikipedia](https://en.wikipedia.org/wiki/Liskov_substitution_principle)
|
||||
|
@ -127,11 +127,11 @@ def deactivate_users(users: Iterable[User]):
|
|||
|
||||
既然为函数增加类型判断无法让代码变得更好,那我们就应该从别的方面入手。
|
||||
|
||||
“里氏替换原则”提到,**子类*(Admin)*应该可以随意替换它的父类*(User)*,而不破坏程序*(deactivate_users)*本身的功能。**我们试过直接修改类的使用者来遵守这条原则,但是失败了。所以这次,让我们试着从源头上解决问题:重新设计类之间的继承关系。
|
||||
“里氏替换原则”提到,**子类 *(Admin)* 应该可以随意替换它的父类 *(User)*,而不破坏程序 *(deactivate_users)* 本身的功能**。 我们试过直接修改类的使用者来遵守这条原则,但是失败了。所以这次,让我们试着从源头上解决问题:重新设计类之间的继承关系。
|
||||
|
||||
具体点来说,子类不能只是简单通过抛出异常的方式对某个类方法进行“退化”。如果 *“对象不能支持某种操作”* 本身就是这个类型的 **核心特征** 之一,那我们在进行父类设计时,就应该把这个 **核心特征** 设计进去。
|
||||
|
||||
拿用户类型举例,*“用户可能无法被停用”* 就是 `User` 类的核心特征之一,所以在设计父类时,我们就应该把它作为类方法*(或属性)*写进去。
|
||||
拿用户类型举例,*“用户可能无法被停用”* 就是 `User` 类的核心特征之一,所以在设计父类时,我们就应该把它作为类方法 *(或属性)* 写进去。
|
||||
|
||||
让我们看看调整后的代码:
|
||||
|
||||
|
@ -214,7 +214,7 @@ def list_user_post_titles(user: User) -> Iterable[str]:
|
|||
yield session.query(Post).get(post_id).title
|
||||
```
|
||||
|
||||
对于上面的 `list_user_post_titles` 函数来说,无论传入的 `user` 参数是 `User` 还是 `Admin` 类型,它都能正常工作。因为,虽然普通用户和管理员类型的 `list_related_posts` 方法返回结果略有区别,但它们都是**“可迭代的帖子 ID”**,所以函数里的循环在碰到不同的用户类型时都能正常进行。
|
||||
对于上面的 `list_user_post_titles` 函数来说,无论传入的 `user` 参数是 `User` 还是 `Admin` 类型,它都能正常工作。因为,虽然普通用户和管理员类型的 `list_related_posts` 方法返回结果略有区别,但它们都是 **“可迭代的帖子 ID”**,所以函数里的循环在碰到不同的用户类型时都能正常进行。
|
||||
|
||||
既然如此,那上面的代码符合“里氏替换原则”吗?答案是否定的。因为虽然在当前 `list_user_post_titles` 函数的视角看来,子类 `Admin` 可以任意替代父类 `User` 使用,但这只是特殊用例下的一个巧合,并没有通用性。请看看下面这个场景。
|
||||
|
||||
|
@ -246,7 +246,7 @@ def get_user_posts_count(user: User) -> int:
|
|||
|
||||
而现在的设计没做到这点,现在的子类返回值所支持的操作,只是父类的一个子集。`Admin` 子类的 `list_related_posts` 方法所返回的生成器,只支持父类 `User` 返回列表里的“迭代操作”,而不支持其他行为(比如 `len()`)。所以我们没办法随意的用子类替换父类,自然也就无法符合里氏替换原则。
|
||||
|
||||
> **注意:**此处说“生成器”支持的操作是“列表”的子集其实不是特别严谨,因为生成器还支持 `.send()` 等其他操作。不过在这里,我们可以只关注它的可迭代特性。
|
||||
> 注意:此处说“生成器”支持的操作是“列表”的子集其实不是特别严谨,因为生成器还支持 `.send()` 等其他操作。不过在这里,我们可以只关注它的可迭代特性。
|
||||
|
||||
### 如何修改代码
|
||||
|
||||
|
@ -328,7 +328,7 @@ class Admin(User):
|
|||
|
||||
让我们最后再总结一下吧:
|
||||
|
||||
- **“L:里氏替换原则”**认为子类应该可以任意替换父类被使用
|
||||
- **“L:里氏替换原则”** 认为子类应该可以任意替换父类被使用
|
||||
- 在类的使用方增加具体的类型判断(*isinstance*),通常不是最佳解决方案
|
||||
- 违反里氏替换原则,通常也会导致违反“开放-关闭”原则
|
||||
- 考虑什么是类的核心特征,然后为父类增加新的方法和属性可以帮到你
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
|
||||
## 前言
|
||||
|
||||
> 这是 “Python 工匠”系列的第 14 篇文章。[[查看系列所有文章]](https://github.com/piglei/one-python-craftsman)
|
||||
|
||||
|
||||
<div style="text-align: center; color: #999; margin: 14px 0 14px;font-size: 12px;">
|
||||
<img src="https://www.zlovezl.cn/static/uploaded/2020/02/carolina-garcia-tavizon-w1280.jpg" width="100%" />
|
||||
> 这是 “Python 工匠”系列的第 14 篇文章。[[查看系列所有文章]](https://github.com/piglei/one-python-craftsman)
|
||||
|
||||
|
||||
<div style="text-align: center; color: #999; margin: 14px 0 14px;font-size: 12px;">
|
||||
<img src="https://www.zlovezl.cn/static/uploaded/2020/02/carolina-garcia-tavizon-w1280.jpg" width="100%" />
|
||||
</div>
|
||||
|
||||
在这篇文章中,我将继续介绍 SOLID 原则剩下的两位成员:**I(接口隔离原则)** 和 **D(依赖倒置原则)**。为了方便,这篇文章将会使用先 D 后 I 的顺序。
|
||||
|
@ -17,18 +17,18 @@
|
|||
|
||||
有了模块,模块间自然就有了依赖关系。比如,你的个人博客可能依赖着 Flask 框架,而 Flask 又依赖了 Werkzeug,Werkzeug 又由更多个低层模块组成。
|
||||
|
||||
依赖倒置原则(Dependency Inversion Principle)就是一条和依赖关系相关的原则。它认为:**“高层模块不应该依赖于低层模块,二者都应该依赖于抽象。”**
|
||||
|
||||
依赖倒置原则(Dependency Inversion Principle)就是一条和依赖关系相关的原则。它认为:**“高层模块不应该依赖于低层模块,二者都应该依赖于抽象。”**
|
||||
|
||||
> High-level modules should not depend on low-level modules. Both should depend on abstractions.
|
||||
|
||||
这个原则看上去有点反直觉。毕竟,在我们的第一堂编程课上,老师就是这么教我们写代码的:*“高层模块要依赖低层模块,hello world 程序依赖 printf()。”*那为什么这条原则又说不要这样做呢?而依赖倒置原则里的“倒置”又是指什么?
|
||||
这个原则看上去有点反直觉。毕竟,在我们的第一堂编程课上,老师就是这么教我们写代码的:*“高层模块要依赖低层模块,hello world 程序依赖 printf()。”* 那为什么这条原则又说不要这样做呢?而依赖倒置原则里的“倒置”又是指什么?
|
||||
|
||||
让我们先把这些问题放在一边,看看下面这个小需求。上面这些问题的答案都藏在这个需求中。
|
||||
|
||||
### 需求:按域名分组统计 HN 新闻数量
|
||||
|
||||
这次出场的还是我们的老朋友:新闻站点 [Hacker News](https://news.ycombinator.com/)。在 HN 上,每个用户提交的条目标题后面,都跟着这条内容的来源域名。
|
||||
|
||||
这次出场的还是我们的老朋友:新闻站点 [Hacker News](https://news.ycombinator.com/)。在 HN 上,每个用户提交的条目标题后面,都跟着这条内容的来源域名。
|
||||
|
||||
我想要按照来源域名来分组统计条目数量,这样就能知道哪个站在 HN 上最受欢迎。
|
||||
|
||||
<div style="text-align: center; color: #999; margin: 14px 0 14px;font-size: 12px;">
|
||||
|
@ -167,11 +167,11 @@ def test_grouper_returning_valid_types():
|
|||
|
||||
### 实现依赖倒置原则
|
||||
|
||||
首先,让我们重温一下“依赖倒置原则”*(后简称 D 原则)*的内容:**“高层模块不应该依赖于低层模块,二者都应该依赖于抽象。”**
|
||||
首先,让我们重温一下“依赖倒置原则”*(后简称 D 原则)* 的内容:**“高层模块不应该依赖于低层模块,二者都应该依赖于抽象。”**
|
||||
|
||||
在上面的代码里,高层模块 `SiteSourceGrouper` 就直接依赖了低层模块 `requests`。为了让代码符合 D 原则,我们首先需要创造一个处于二者中间的抽象,然后让两个模块可以都依赖这个新的抽象层。
|
||||
|
||||
创建抽象的第一步*(可能也是最重要的一步)*,就是确定这个抽象层的职责。在例子中,高层模块主要依赖 `requests` 做了这些事:
|
||||
创建抽象的第一步 *(可能也是最重要的一步)*,就是确定这个抽象层的职责。在例子中,高层模块主要依赖 `requests` 做了这些事:
|
||||
|
||||
- 通过 `requests.get()` 获取 response
|
||||
- 通过 `response.text` 获取响应文本
|
||||
|
@ -191,7 +191,7 @@ type HNWebPage interface {
|
|||
|
||||
不过,Python 根本没有接口这种东西。那该怎么办呢?虽然 Python 没有接口,但是有一个非常类似的东西:**“抽象类(Abstrace Class)”**。使用 [`abc`](https://docs.python.org/3/library/abc.html) 模块就可以轻松定义出一个抽象类:
|
||||
|
||||
```
|
||||
```python
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
|
||||
|
@ -206,13 +206,13 @@ class HNWebPage(metaclass=ABCMeta):
|
|||
|
||||
抽象类和普通类的区别之一就是你不能将它实例化。如果你尝试实例化一个抽象类,解释器会报出下面的错误:
|
||||
|
||||
```
|
||||
```python
|
||||
TypeError: Can't instantiate abstract class HNWebPage with abstract methods get_text
|
||||
```
|
||||
|
||||
所以,光有抽象类还不能算完事,我们还得定义几个依赖这个抽象类的实体。首先定义的是 `RemoteHNWebPage` 类。它的作用就是通过 requests 模块请求 HN 页面,返回页面内容。
|
||||
|
||||
```
|
||||
```python
|
||||
class RemoteHNWebPage(HNWebPage):
|
||||
"""远程页面,通过请求 HN 站点返回内容"""
|
||||
|
||||
|
@ -226,34 +226,34 @@ class RemoteHNWebPage(HNWebPage):
|
|||
|
||||
定义了 `RemoteHNWebPage` 类后,`SiteSourceGrouper` 类的初始化方法和 `get_groups` 也需要做对应的调整:
|
||||
|
||||
```
|
||||
class SiteSourceGrouper:
|
||||
"""对 HN 页面的新闻来源站点进行分组统计
|
||||
"""
|
||||
|
||||
def __init__(self, page: HNWebPage):
|
||||
self.page = page
|
||||
|
||||
def get_groups(self) -> Dict[str, int]:
|
||||
"""获取 (域名, 个数) 分组
|
||||
"""
|
||||
html = etree.HTML(self.page.get_text())
|
||||
# 通过 xpath 语法筛选新闻域名标签
|
||||
elems = html.xpath('//table[@class="itemlist"]//span[@class="sitestr"]')
|
||||
|
||||
groups = Counter()
|
||||
for elem in elems:
|
||||
groups.update([elem.text])
|
||||
```python
|
||||
class SiteSourceGrouper:
|
||||
"""对 HN 页面的新闻来源站点进行分组统计
|
||||
"""
|
||||
|
||||
def __init__(self, page: HNWebPage):
|
||||
self.page = page
|
||||
|
||||
def get_groups(self) -> Dict[str, int]:
|
||||
"""获取 (域名, 个数) 分组
|
||||
"""
|
||||
html = etree.HTML(self.page.get_text())
|
||||
# 通过 xpath 语法筛选新闻域名标签
|
||||
elems = html.xpath('//table[@class="itemlist"]//span[@class="sitestr"]')
|
||||
|
||||
groups = Counter()
|
||||
for elem in elems:
|
||||
groups.update([elem.text])
|
||||
return groups
|
||||
|
||||
|
||||
def main():
|
||||
# 实例化 page,传入 SiteSourceGrouper
|
||||
page = RemoteHNWebPage(url="https://news.ycombinator.com/")
|
||||
def main():
|
||||
# 实例化 page,传入 SiteSourceGrouper
|
||||
page = RemoteHNWebPage(url="https://news.ycombinator.com/")
|
||||
grouper = SiteSourceGrouper(page).get_groups()
|
||||
```
|
||||
|
||||
做完这些修改后,让我们再看看现在的模块依赖关系:
|
||||
做完这些修改后,让我们再看看现在的模块依赖关系:
|
||||
|
||||
<div style="text-align: center; color: #999; margin: 14px 0 14px;font-size: 12px;">
|
||||
<img src="https://www.zlovezl.cn/static/uploaded/2020/02/SOLID_D_after.png" width="100%" />
|
||||
|
@ -266,7 +266,7 @@ def main():
|
|||
|
||||
再回到之前的单元测试上来。通过引入了新的抽象层 `HNWebPage`,我们可以实现一个不依赖外部网络的新类型 `LocalHNWebPage`。
|
||||
|
||||
```
|
||||
```python
|
||||
class LocalHNWebPage(HNWebPage):
|
||||
"""本地页面,根据本地文件返回页面内容"""
|
||||
|
||||
|
@ -280,7 +280,7 @@ class LocalHNWebPage(HNWebPage):
|
|||
|
||||
所以,单元测试也可以改为使用 `LocalHNWebPage`:
|
||||
|
||||
```
|
||||
```python
|
||||
def test_grouper_from_local():
|
||||
page = LocalHNWebPage(path="./static_hn.html")
|
||||
grouper = SiteSourceGrouper(page)
|
||||
|
@ -290,8 +290,8 @@ def test_grouper_from_local():
|
|||
|
||||
这样就可以在没有外网的服务器上测试 `SiteSourceGrouper` 类的核心逻辑了。
|
||||
|
||||
> Hint:其实上面的测试函数 `test_grouper_from_local` 远远算不上一个合格的测试用例。
|
||||
>
|
||||
> Hint:其实上面的测试函数 `test_grouper_from_local` 远远算不上一个合格的测试用例。
|
||||
>
|
||||
> 如果真要测试 `SiteSourceGrouper` 的核心逻辑。我们应该准备一个虚构的 Hacker News 页面 *(比如刚好包含 5 个 来源自 github.com 的条目)*,然后判断结果是否包含 `assert result['github.com] == 5`
|
||||
|
||||
### 问题:一定要使用抽象类 abc 吗?
|
||||
|
@ -316,235 +316,235 @@ def test_grouper_from_local():
|
|||
- 比如代码里的字符串字面量也是具体实现,我是不是得定义一个 *"StringLike"* 类型把它抽象进去?
|
||||
- ... ...
|
||||
|
||||
事实上,抽象的好处显而易见:**它解耦了高层模块和低层模块间的依赖关系,让代码变得更灵活。** 但抽象同时也带来了额外的编码与理解成本。所以,了解何时 **不** 抽象与何时抽象同样重要。**只有对代码中那些现在或未来会发生变化的东西进行抽象,才能获得最大的收益。**
|
||||
事实上,抽象的好处显而易见:**它解耦了高层模块和低层模块间的依赖关系,让代码变得更灵活。** 但抽象同时也带来了额外的编码与理解成本。所以,了解何时 **不** 抽象与何时抽象同样重要。**只有对代码中那些现在或未来会发生变化的东西进行抽象,才能获得最大的收益。**
|
||||
|
||||
## I:接口隔离原则
|
||||
|
||||
接口隔离原则*(后简称 I 原则)*全称为 *“Interface Segregation Principles”*。顾名思义,它是一条和“接口(Interface)”有关的原则。
|
||||
|
||||
我在前面解释过何为“接口(Interface)”。**接口是模块间相互交流的抽象协议**,它在不同的编程语言里有着不同的表现形态。比如在 Go 里它是 `type ... interface`,而在 Python 中它可以是抽象类、普通类或者函数,甚至某个只在你大脑里存在的一套协议。
|
||||
|
||||
I 原则认为:**“客户(client)应该不依赖于它不使用的方法”**
|
||||
|
||||
> The interface-segregation principle (ISP) states that no client should be forced to depend on methods it does not use.
|
||||
|
||||
这里说的“客户(Client)”指的是接口的使用方 *(客户程序)*,也就是调用接口方法的高层模块。拿上一个统计 HN 页面条目的例子来说:
|
||||
|
||||
- `使用方(客户程序)`:SiteSourceGrouper
|
||||
- `接口(其实是抽象类)`:HNWebPage
|
||||
- `依赖关系`:调用接口方法:`get_text()` 获取页面文本
|
||||
|
||||
在 I 原则看来,**一个接口所提供的方法,应该就是使用方所需要的方法,不多不少刚刚好。** 所以,在上个例子里,我们设计的接口 `HNWebPage` 是符合接口隔离原则的。因为它没有向使用方提供任何后者不需要的方法 。
|
||||
|
||||
> 你需要 get_text()!我提供 get_text()!刚刚好!
|
||||
|
||||
所以,这条原则看上去似乎很容易遵守。既然如此,让我们试试来违反它吧!
|
||||
|
||||
### 例子:开发页面归档功能
|
||||
|
||||
让我们接着上一个例子开始。在实现了上个需求后,我现在有一个代表 Hacker News 站点页面的抽象类 `HNWebPage`,它只提供了一种行为,就是获取当前页面的文本内容。
|
||||
|
||||
```python
|
||||
class HNWebPage(metaclass=ABCMeta):
|
||||
|
||||
@abstractmethod
|
||||
def get_text(self) -> str:
|
||||
"""获取页面文本内容"""
|
||||
```
|
||||
|
||||
现在,假设我要开发一个和 HN 页面有关的新功能: **我想在不同时间点对 HN 首页内容进行归档,观察热点新闻在不同时间点发生的变化。** 所以除了页面文本内容外,我还需要拿到页面的大小、生成时间这些额外信息,然后将它们都保存到数据库中。
|
||||
|
||||
为了做到这一点,现在的 `HNWebPage` 类需要被扩展一下:
|
||||
|
||||
```python
|
||||
class HNWebPage(metaclass=ABCMeta):
|
||||
|
||||
@abstractmethod
|
||||
def get_text(self) -> str:
|
||||
"""获取页面文本内容"""
|
||||
|
||||
# 新增 get_size 与 get_generated_at
|
||||
|
||||
@abstractmethod
|
||||
def get_size(self) -> int:
|
||||
"""获取页面大小"""
|
||||
|
||||
@abstractmethod
|
||||
def get_generated_at(self) -> datetime.datetime:
|
||||
"""获取页面生成时间"""
|
||||
```
|
||||
|
||||
我在原来的类上增加了两个新的抽象方法:`get_size` 和 `get_generated_at`。这样归档程序就能通过它们拿到页面大小和生成时间了。
|
||||
|
||||
改完抽象类后,紧接着的任务就是修改依赖它的实体类。
|
||||
|
||||
### 问题:实体类不符合 HNWebPage 接口规范
|
||||
|
||||
在修改抽象类前,我们有两个实现了它协议的实体类:`RemoteHNWebPage` 和 `LocalHNWebPage`。如今,`HNWebPage` 增加了两个新方法 `get_size` 和 `get_generated_at`。我们自然需要把这两个实体类也加上这两个方法。
|
||||
|
||||
`RemoteHNWebPage` 类的修改很好做,我们只要让 `get_size` 放回页面长度,让 `get_generated_at` 返回当前时间就行了。
|
||||
|
||||
```python
|
||||
# class RemoteHNWebPage:
|
||||
#
|
||||
def get_generated_at(self) -> datetime.datetime:
|
||||
# 页面生成时间等同于通过 requests 请求的时间
|
||||
return datetime.datetime.now()
|
||||
```
|
||||
|
||||
但是,在给 `LocalHNWebPage` 添加 `get_generated_at` 方法时,我碰到了一个问题。`LocalHNWebPage` 是一个完全基于本地页面文件作为数据来源的类,仅仅通过 “static_hn.html” 这么一个本地文件,我根本就没法知道它的内容是什么时候生成的。
|
||||
|
||||
这时我只能选择让它的 `get_generated_at` 方法返回一个错误的结果 *(比如文件的修改时间)*,或者直接抛出异常。无论是哪种做法,我都可能违反 [里式替换原则](https://www.zlovezl.cn/articles/write-solid-python-codes-part-2/)。
|
||||
|
||||
> Hint:里式替换原则认为子类(派生类)对象应该可以在程序中替代父类(基类)对象使用,而不破坏程序原本的功能。让方法抛出异常显然破坏了这一点。
|
||||
|
||||
```python
|
||||
# class LocalHNWebPage:
|
||||
#
|
||||
def get_generated_at(self) -> datetime.datetime:
|
||||
raise NotImplementedError("local web page can not provide generate_at info")
|
||||
```
|
||||
|
||||
所以,对现有接口的盲目扩展暴露出来一个问题:**更多的接口方法意味着更高的实现成本,给实现方带来麻烦的概率也变高了。**
|
||||
|
||||
不过现在让我们暂且把这个问题放到一边,继续写一个 `SiteAchiever` 类完成归档任务:
|
||||
|
||||
```python
|
||||
class SiteAchiever:
|
||||
"""将不同时间点的 HN 页面归档"""
|
||||
|
||||
def save_page(self, page: HNWebPage):
|
||||
"""将页面保存到后端数据库
|
||||
"""
|
||||
data = {
|
||||
"content": page.get_text(),
|
||||
"generated_at": page.get_generated_at(),
|
||||
"size": page.get_size(),
|
||||
}
|
||||
# 将 data 保存到数据库中
|
||||
```
|
||||
|
||||
### 成功违反 I 协议
|
||||
|
||||
代码写到这,让我们回头看看上个例子里的 *条目来源分组类 `SiteSourceGrouper`* 。
|
||||
|
||||
## I:接口隔离原则
|
||||
|
||||
接口隔离原则*(后简称 I 原则)*全称为 *“Interface Segregation Principles”*。顾名思义,它是一条和“接口(Interface)”有关的原则。
|
||||
|
||||
我在前面解释过何为“接口(Interface)”。**接口是模块间相互交流的抽象协议**,它在不同的编程语言里有着不同的表现形态。比如在 Go 里它是 `type ... interface`,而在 Python 中它可以是抽象类、普通类或者函数,甚至某个只在你大脑里存在的一套协议。
|
||||
|
||||
I 原则认为:**“客户(client)应该不依赖于它不使用的方法”**
|
||||
|
||||
> The interface-segregation principle (ISP) states that no client should be forced to depend on methods it does not use.
|
||||
|
||||
这里说的“客户(Client)”指的是接口的使用方 *(客户程序)*,也就是调用接口方法的高层模块。拿上一个统计 HN 页面条目的例子来说:
|
||||
|
||||
- `使用方(客户程序)`:SiteSourceGrouper
|
||||
- `接口(其实是抽象类)`:HNWebPage
|
||||
- `依赖关系`:调用接口方法:`get_text()` 获取页面文本
|
||||
|
||||
在 I 原则看来,**一个接口所提供的方法,应该就是使用方所需要的方法,不多不少刚刚好。** 所以,在上个例子里,我们设计的接口 `HNWebPage` 是符合接口隔离原则的。因为它没有向使用方提供任何后者不需要的方法 。
|
||||
|
||||
> 你需要 get_text()!我提供 get_text()!刚刚好!
|
||||
|
||||
所以,这条原则看上去似乎很容易遵守。既然如此,让我们试试来违反它吧!
|
||||
|
||||
### 例子:开发页面归档功能
|
||||
|
||||
让我们接着上一个例子开始。在实现了上个需求后,我现在有一个代表 Hacker News 站点页面的抽象类 `HNWebPage`,它只提供了一种行为,就是获取当前页面的文本内容。
|
||||
|
||||
```python
|
||||
class HNWebPage(metaclass=ABCMeta):
|
||||
|
||||
@abstractmethod
|
||||
def get_text(self) -> str:
|
||||
"""获取页面文本内容"""
|
||||
```
|
||||
|
||||
现在,假设我要开发一个和 HN 页面有关的新功能: **我想在不同时间点对 HN 首页内容进行归档,观察热点新闻在不同时间点发生的变化。** 所以除了页面文本内容外,我还需要拿到页面的大小、生成时间这些额外信息,然后将它们都保存到数据库中。
|
||||
|
||||
为了做到这一点,现在的 `HNWebPage` 类需要被扩展一下:
|
||||
|
||||
```python
|
||||
class HNWebPage(metaclass=ABCMeta):
|
||||
|
||||
@abstractmethod
|
||||
def get_text(self) -> str:
|
||||
"""获取页面文本内容"""
|
||||
|
||||
# 新增 get_size 与 get_generated_at
|
||||
|
||||
@abstractmethod
|
||||
def get_size(self) -> int:
|
||||
"""获取页面大小"""
|
||||
|
||||
@abstractmethod
|
||||
def get_generated_at(self) -> datetime.datetime:
|
||||
"""获取页面生成时间"""
|
||||
```
|
||||
|
||||
我在原来的类上增加了两个新的抽象方法:`get_size` 和 `get_generated_at`。这样归档程序就能通过它们拿到页面大小和生成时间了。
|
||||
|
||||
改完抽象类后,紧接着的任务就是修改依赖它的实体类。
|
||||
|
||||
### 问题:实体类不符合 HNWebPage 接口规范
|
||||
|
||||
在修改抽象类前,我们有两个实现了它协议的实体类:`RemoteHNWebPage` 和 `LocalHNWebPage`。如今,`HNWebPage` 增加了两个新方法 `get_size` 和 `get_generated_at`。我们自然需要把这两个实体类也加上这两个方法。
|
||||
|
||||
`RemoteHNWebPage` 类的修改很好做,我们只要让 `get_size` 放回页面长度,让 `get_generated_at` 返回当前时间就行了。
|
||||
|
||||
```python
|
||||
# class RemoteHNWebPage:
|
||||
#
|
||||
def get_generated_at(self) -> datetime.datetime:
|
||||
# 页面生成时间等同于通过 requests 请求的时间
|
||||
return datetime.datetime.now()
|
||||
```
|
||||
|
||||
但是,在给 `LocalHNWebPage` 添加 `get_generated_at` 方法时,我碰到了一个问题。`LocalHNWebPage` 是一个完全基于本地页面文件作为数据来源的类,仅仅通过 “static_hn.html” 这么一个本地文件,我根本就没法知道它的内容是什么时候生成的。
|
||||
|
||||
这时我只能选择让它的 `get_generated_at` 方法返回一个错误的结果*(比如文件的修改时间)*,或者直接抛出异常。无论是哪种做法,我都可能违反 [里式替换原则](https://www.zlovezl.cn/articles/write-solid-python-codes-part-2/)。
|
||||
|
||||
> Hint:里式替换原则认为子类(派生类)对象应该可以在程序中替代父类(基类)对象使用,而不破坏程序原本的功能。让方法抛出异常显然破坏了这一点。
|
||||
|
||||
```python
|
||||
# class LocalHNWebPage:
|
||||
#
|
||||
def get_generated_at(self) -> datetime.datetime:
|
||||
raise NotImplementedError("local web page can not provide generate_at info")
|
||||
```
|
||||
|
||||
所以,对现有接口的盲目扩展暴露出来一个问题:**更多的接口方法意味着更高的实现成本,给实现方带来麻烦的概率也变高了。**
|
||||
|
||||
不过现在让我们暂且把这个问题放到一边,继续写一个 `SiteAchiever` 类完成归档任务:
|
||||
|
||||
```python
|
||||
class SiteAchiever:
|
||||
"""将不同时间点的 HN 页面归档"""
|
||||
|
||||
def save_page(self, page: HNWebPage):
|
||||
"""将页面保存到后端数据库
|
||||
"""
|
||||
data = {
|
||||
"content": page.get_text(),
|
||||
"generated_at": page.get_generated_at(),
|
||||
"size": page.get_size(),
|
||||
}
|
||||
# 将 data 保存到数据库中
|
||||
```
|
||||
|
||||
### 成功违反 I 协议
|
||||
|
||||
代码写到这,让我们回头看看上个例子里的 *条目来源分组类 `SiteSourceGrouper`* 。
|
||||
|
||||
<div style="text-align: center; color: #999; margin: 14px 0 14px;font-size: 12px;">
|
||||
<img src="https://www.zlovezl.cn/static/uploaded/2020/02/SOLID_I_before.png" width="100%" />
|
||||
图:成功违反了 I 协议
|
||||
</div>
|
||||
|
||||
当我修改完抽象类后,虽然 `SiteSourceGrouper` 仍然依赖着 `HNWebPage`,但它其实只使用了 `get_text` 这一个方法而已,其他 `get_size`、`get_generated` 这些它 **不使用的方法也成为了它的依赖。**
|
||||
|
||||
很明显,现在的设计违反了接口隔离原则。为了修复这一点,我们需要将 `HNWebPage` 拆成更小的接口。
|
||||
|
||||
### 如何分拆接口
|
||||
|
||||
设计接口有一个技巧:**让客户(调用方)来驱动协议设计**。让我们来看看,`HNWebPage` 到底有哪些客户:
|
||||
|
||||
- `SiteSourceGrouper`:域名来源统计,依赖 `get_text()`
|
||||
- `SiteAchiever`:HN 页面归档程序,依赖 `get_text()`、`get_size()`、`get_generated_at()`
|
||||
|
||||
按照上面的方式,我们可以把 `HNWebPage` 分离成两个独立的抽象类:
|
||||
|
||||
```python
|
||||
class ContentOnlyHNWebPage(metaclass=ABCMeta):
|
||||
"""抽象类:Hacker New 站点页面(仅提供内容)
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def get_text(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class HNWebPage(ContentOnlyHNWebPage):
|
||||
"""抽象类:Hacker New 站点页面(含元数据)
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def get_size(self) -> int:
|
||||
"""获取页面大小"""
|
||||
|
||||
@abstractmethod
|
||||
def get_generated_at(self) -> datetime.datetime:
|
||||
"""获取页面生成时间"""
|
||||
```
|
||||
|
||||
将旧类拆分成两个不同的抽象类后,`SiteSourceGrouper` 和 `SiteAchiever` 就可以分别依赖不同的抽象类了。
|
||||
|
||||
同时,对于 `LocalHNWebPage` 类来说,它也只需要实现那个只返回的文本的 `ContentOnlyHNWebPage` 就行。
|
||||
|
||||
</div>
|
||||
|
||||
当我修改完抽象类后,虽然 `SiteSourceGrouper` 仍然依赖着 `HNWebPage`,但它其实只使用了 `get_text` 这一个方法而已,其他 `get_size`、`get_generated` 这些它 **不使用的方法也成为了它的依赖。**
|
||||
|
||||
很明显,现在的设计违反了接口隔离原则。为了修复这一点,我们需要将 `HNWebPage` 拆成更小的接口。
|
||||
|
||||
### 如何分拆接口
|
||||
|
||||
设计接口有一个技巧:**让客户(调用方)来驱动协议设计**。让我们来看看,`HNWebPage` 到底有哪些客户:
|
||||
|
||||
- `SiteSourceGrouper`:域名来源统计,依赖 `get_text()`
|
||||
- `SiteAchiever`:HN 页面归档程序,依赖 `get_text()`、`get_size()`、`get_generated_at()`
|
||||
|
||||
按照上面的方式,我们可以把 `HNWebPage` 分离成两个独立的抽象类:
|
||||
|
||||
```python
|
||||
class ContentOnlyHNWebPage(metaclass=ABCMeta):
|
||||
"""抽象类:Hacker New 站点页面(仅提供内容)
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def get_text(self) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class HNWebPage(ContentOnlyHNWebPage):
|
||||
"""抽象类:Hacker New 站点页面(含元数据)
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def get_size(self) -> int:
|
||||
"""获取页面大小"""
|
||||
|
||||
@abstractmethod
|
||||
def get_generated_at(self) -> datetime.datetime:
|
||||
"""获取页面生成时间"""
|
||||
```
|
||||
|
||||
将旧类拆分成两个不同的抽象类后,`SiteSourceGrouper` 和 `SiteAchiever` 就可以分别依赖不同的抽象类了。
|
||||
|
||||
同时,对于 `LocalHNWebPage` 类来说,它也只需要实现那个只返回的文本的 `ContentOnlyHNWebPage` 就行。
|
||||
|
||||
<div style="text-align: center; color: #999; margin: 14px 0 14px;font-size: 12px;">
|
||||
<img src="https://www.zlovezl.cn/static/uploaded/2020/02/SOLID_I_after.png" width="100%" />
|
||||
图:实施接口隔离后的结果
|
||||
</div>
|
||||
|
||||
### 一些不容易发现的违反情况
|
||||
|
||||
虽然我花了很长的篇幅,用了好几个抽象类才把接口隔离原则讲明白,但其实在我们的日常编码中,对这条原则的违反经常会出现在一些更容易被忽视的地方。
|
||||
|
||||
举个例子,当我们在 web 站点里判断用户请求的 Cookies 或头信息是否包含某个标记值时,我们经常直接写一个依赖整个 `request` 对象的函数:
|
||||
|
||||
```python
|
||||
def is_new_visitor(request: HttpRequest) -> bool:
|
||||
"""从 Cookies 判断是否新访客
|
||||
"""
|
||||
return request.COOKIES.get('is_new_visitor') == 'y'
|
||||
```
|
||||
|
||||
但事实上,除了 `.COOKIES` 以外,`is_new_visitor` 根本就不需要 `request` 对象里面的任何其他内容。“用户请求对象(request)”是一个比“Cookie 字典(request.COOKIES)”复杂得多的抽象。我们完全可以把函数改成只接收 cookies 字典。
|
||||
|
||||
```python
|
||||
def is_new_visitor(cookies: Dict) -> bool:
|
||||
"""从 Cookies 判断是否新访客
|
||||
"""
|
||||
return cookies.get('is_new_visitor') == 'y'
|
||||
```
|
||||
|
||||
类似的情况还有很多,比如一个发短信的函数本身只需要两个参数 `电话号码` 和 `用户姓名`,但是函数却依赖了整个用户对象 `User`,里面包含着几十个用不上的其他字段和方法。
|
||||
|
||||
对于这类函数,我们都可以重新考虑一下它们的抽象是否合理,是否需要应用接口隔离原则。
|
||||
|
||||
</div>
|
||||
|
||||
### 一些不容易发现的违反情况
|
||||
|
||||
虽然我花了很长的篇幅,用了好几个抽象类才把接口隔离原则讲明白,但其实在我们的日常编码中,对这条原则的违反经常会出现在一些更容易被忽视的地方。
|
||||
|
||||
举个例子,当我们在 web 站点里判断用户请求的 Cookies 或头信息是否包含某个标记值时,我们经常直接写一个依赖整个 `request` 对象的函数:
|
||||
|
||||
```python
|
||||
def is_new_visitor(request: HttpRequest) -> bool:
|
||||
"""从 Cookies 判断是否新访客
|
||||
"""
|
||||
return request.COOKIES.get('is_new_visitor') == 'y'
|
||||
```
|
||||
|
||||
但事实上,除了 `.COOKIES` 以外,`is_new_visitor` 根本就不需要 `request` 对象里面的任何其他内容。“用户请求对象(request)”是一个比“Cookie 字典(request.COOKIES)”复杂得多的抽象。我们完全可以把函数改成只接收 cookies 字典。
|
||||
|
||||
```python
|
||||
def is_new_visitor(cookies: Dict) -> bool:
|
||||
"""从 Cookies 判断是否新访客
|
||||
"""
|
||||
return cookies.get('is_new_visitor') == 'y'
|
||||
```
|
||||
|
||||
类似的情况还有很多,比如一个发短信的函数本身只需要两个参数 `电话号码` 和 `用户姓名`,但是函数却依赖了整个用户对象 `User`,里面包含着几十个用不上的其他字段和方法。
|
||||
|
||||
对于这类函数,我们都可以重新考虑一下它们的抽象是否合理,是否需要应用接口隔离原则。
|
||||
|
||||
### 现实世界中的接口隔离
|
||||
|
||||
当你知道了接口隔离原则的种种好处后,你很自然就会养成写小类、小接口的习惯。在现实世界里,其实已经有很多小而精的接口设计可以供你参考。比如:
|
||||
|
||||
- Python 的 [collections.abc](https://docs.python.org/3/library/collections.abc.html) 模块里面有非常多的小接口
|
||||
- Go 里面的 [Reader 和 Writer](https://golang.org/pkg/io/#Reader) 也是非常好的例子
|
||||
|
||||
## 总结
|
||||
|
||||
在这篇文章里,我向你介绍了 SOLID 原则的最后两位成员:**“依赖倒置原则”** 与 **“接口隔离原则”**。
|
||||
|
||||
这两条原则之间有一个共同点,那就是它们都和 **“抽象”** 有着紧密的联系。前者告诉我们要面向抽象而非实现编程,后者则教导我们在设计抽象时应该做到精准。
|
||||
|
||||
最后再总结一下:
|
||||
|
||||
- **“D:依赖倒置原则”** 认为高层模块和低层模块都应该依赖于抽象
|
||||
- 依赖抽象,意味着我们可以完全修改低层实现,而不影响高层代码
|
||||
- 在 Python 中你可以使用 abc 模块来定义抽象类
|
||||
- 除 abc 外,你也可以使用其他技术来完成依赖倒置
|
||||
- **“I:接口隔离原则”** 认为客户不应该依赖任何它不使用的方法
|
||||
- 设计接口就是设计抽象
|
||||
- 违反接口隔离原则也可能会导致违反单一职责与里式替换原则
|
||||
- 写更小的类、写更小的接口在大多数情况下是个好主意
|
||||
|
||||
看完文章的你,有没有什么想吐槽的?请留言或者在 [项目 Github Issues](https://github.com/piglei/one-python-craftsman) 告诉我吧。
|
||||
|
||||
[>>>下一篇【15.在边界处思考】](15-thinking-in-edge-cases.md)
|
||||
|
||||
[<<<上一篇【13.写好面向对象代码的原则(中)】](13-write-solid-python-codes-part-2.md)
|
||||
|
||||
|
||||
## 附录
|
||||
|
||||
- 题图来源: Photo by Carolina Garcia Tavizon on Unsplash
|
||||
- 更多系列文章地址:https://github.com/piglei/one-python-craftsman
|
||||
|
||||
系列其他文章:
|
||||
|
||||
- [所有文章索引 [Github]](https://github.com/piglei/one-python-craftsman)
|
||||
- [Python 工匠:写好面向对象代码的原则(上)](https://www.zlovezl.cn/articles/write-solid-python-codes-part-1/)
|
||||
- [Python 工匠:写好面向对象代码的原则(中)](https://www.zlovezl.cn/articles/write-solid-python-codes-part-2/)
|
||||
- [Python 工匠:写好面向对象代码的原则(下)](https://www.zlovezl.cn/articles/write-solid-python-codes-part-3/)
|
||||
|
||||
|
||||
当你知道了接口隔离原则的种种好处后,你很自然就会养成写小类、小接口的习惯。在现实世界里,其实已经有很多小而精的接口设计可以供你参考。比如:
|
||||
|
||||
- Python 的 [collections.abc](https://docs.python.org/3/library/collections.abc.html) 模块里面有非常多的小接口
|
||||
- Go 里面的 [Reader 和 Writer](https://golang.org/pkg/io/#Reader) 也是非常好的例子
|
||||
|
||||
## 总结
|
||||
|
||||
在这篇文章里,我向你介绍了 SOLID 原则的最后两位成员:**“依赖倒置原则”** 与 **“接口隔离原则”**。
|
||||
|
||||
这两条原则之间有一个共同点,那就是它们都和 **“抽象”** 有着紧密的联系。前者告诉我们要面向抽象而非实现编程,后者则教导我们在设计抽象时应该做到精准。
|
||||
|
||||
最后再总结一下:
|
||||
|
||||
- **“D:依赖倒置原则”** 认为高层模块和低层模块都应该依赖于抽象
|
||||
- 依赖抽象,意味着我们可以完全修改低层实现,而不影响高层代码
|
||||
- 在 Python 中你可以使用 abc 模块来定义抽象类
|
||||
- 除 abc 外,你也可以使用其他技术来完成依赖倒置
|
||||
- **“I:接口隔离原则”** 认为客户不应该依赖任何它不使用的方法
|
||||
- 设计接口就是设计抽象
|
||||
- 违反接口隔离原则也可能会导致违反单一职责与里式替换原则
|
||||
- 写更小的类、写更小的接口在大多数情况下是个好主意
|
||||
|
||||
看完文章的你,有没有什么想吐槽的?请留言或者在 [项目 Github Issues](https://github.com/piglei/one-python-craftsman) 告诉我吧。
|
||||
|
||||
[>>>下一篇【15.在边界处思考】](15-thinking-in-edge-cases.md)
|
||||
|
||||
[<<<上一篇【13.写好面向对象代码的原则(中)】](13-write-solid-python-codes-part-2.md)
|
||||
|
||||
|
||||
## 附录
|
||||
|
||||
- 题图来源: Photo by Carolina Garcia Tavizon on Unsplash
|
||||
- 更多系列文章地址:https://github.com/piglei/one-python-craftsman
|
||||
|
||||
系列其他文章:
|
||||
|
||||
- [所有文章索引 [Github]](https://github.com/piglei/one-python-craftsman)
|
||||
- [Python 工匠:写好面向对象代码的原则(上)](https://www.zlovezl.cn/articles/write-solid-python-codes-part-1/)
|
||||
- [Python 工匠:写好面向对象代码的原则(中)](https://www.zlovezl.cn/articles/write-solid-python-codes-part-2/)
|
||||
- [Python 工匠:写好面向对象代码的原则(下)](https://www.zlovezl.cn/articles/write-solid-python-codes-part-3/)
|
||||
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@ remove_list_entry(entry) {
|
|||
|
||||
在新代码中,`remove_list_entry` 函数利用了 C 语言里的指针特性,把之前的 `if / else` 完全消除了。无论待删除的目标是在链表头部还是中间,函数都能一视同仁的完成删除操作。之前的边界情况消失了。
|
||||
|
||||
看到这你是不是在犯嘀咕:*Python 又没有指针,你跟我说这么多指针不指针的干啥?*虽然 Python 没有指针,但我觉得这个例子为我们提供了一个很有趣的主题。那就是 **如何充分利用语言特性,更好的处理编码时的边界情况。**
|
||||
看到这你是不是在犯嘀咕:*Python 又没有指针,你跟我说这么多指针不指针的干啥?* 虽然 Python 没有指针,但我觉得这个例子为我们提供了一个很有趣的主题。那就是 **如何充分利用语言特性,更好的处理编码时的边界情况。**
|
||||
|
||||
我认为,好代码在处理边界情况时应该是简洁的、“润物细无声”的。就像上面的例子一样,可以做到让边界情况消融在代码主流程中。在写 Python 时,有不少编码技巧和惯例可以帮我们做到这一点,一块来看看吧。
|
||||
|
||||
|
@ -255,7 +255,7 @@ def sum_list(l, limit):
|
|||
|
||||
利用这个特点,我们还可以简化一些特定的边界处理逻辑。比如安全删除列表的某个元素:
|
||||
|
||||
```
|
||||
```python
|
||||
# 使用异常捕获安全删除列表的第 5 个元素
|
||||
try:
|
||||
l.pop(5)
|
||||
|
@ -295,7 +295,7 @@ if extra_context:
|
|||
|
||||
如果使用 `or` 操作符,我们可以让上面的语句更简练:
|
||||
|
||||
```
|
||||
```python
|
||||
context.update(extra_context or {})
|
||||
```
|
||||
|
||||
|
@ -371,7 +371,7 @@ Your number is 65
|
|||
|
||||
这个函数一共有 14 行有效代码。其中有 3 段 if 共 9 行代码,都是用于校验的边界值检查代码。也许你觉得这样的检查很正常,但请想象一下,假如需要校验的输入不止一个、校验逻辑也比这个复杂怎么办?那样的话,**这些边界值检查代码就会变得又臭又长。**
|
||||
|
||||
如何改进这些代码呢?把它们抽离出去,作为一个校验函数和核心逻辑隔离开是个不错的办法。但更重要的在于,要把*“输入数据校验”*作为一个独立的职责与领域,用更恰当的模块来完成这项工作。
|
||||
如何改进这些代码呢?把它们抽离出去,作为一个校验函数和核心逻辑隔离开是个不错的办法。但更重要的在于,要把 *“输入数据校验”* 作为一个独立的职责与领域,用更恰当的模块来完成这项工作。
|
||||
|
||||
在数据校验这块,[pydantic](https://pydantic-docs.helpmanual.io/) 模块是一个不错的选择。如果用它来做校验,代码可以被简化成这样:
|
||||
|
||||
|
@ -409,7 +409,7 @@ def input_a_number_with_pydantic():
|
|||
|
||||
很多年前刚接触 Web 开发时,我想学着用 JavaScript 来实现一个简单的文字跑马灯动画。如果你不知道啥是“跑马灯”,我可以稍微解释一下。“跑马灯”就是让一段文字从页面左边往右边不断循环滚动,十几年前的网站特别流行这个。😬
|
||||
|
||||
我记得里面有一段逻辑是这样的:*控制文字不断往右边移动,当横坐标超过页面宽度时,重置坐标后继续。*我当时写出来的代码,翻译成 Python 大概是这样:
|
||||
我记得里面有一段逻辑是这样的:*控制文字不断往右边移动,当横坐标超过页面宽度时,重置坐标后继续*。我当时写出来的代码,翻译成 Python 大概是这样:
|
||||
|
||||
```python
|
||||
while True:
|
||||
|
@ -425,7 +425,7 @@ while True:
|
|||
|
||||
在上面的代码里,我需要在主循环里保证 “element.position_x 不会超过页面宽度 page_width”。所以我写了一个 if 来处理当 `position_x` 超过页面宽度的情况。
|
||||
|
||||
但如果是要保证某个累加的数字*(position_x)*不超过另一个数字*(page_width)*,直接用 `%` 做取模运算不就好了吗?
|
||||
但如果是要保证某个累加的数字 *(position_x)* 不超过另一个数字 *(page_width)*,直接用 `%` 做取模运算不就好了吗?
|
||||
|
||||
```python
|
||||
while True:
|
||||
|
|
|
@ -12,21 +12,25 @@
|
|||
|
||||
### 内容目录
|
||||
|
||||
* [最佳实践](#最佳实践)
|
||||
* [1. 避免多层分支嵌套](#1-避免多层分支嵌套)
|
||||
* [2. 封装那些过于复杂的逻辑判断](#2-封装那些过于复杂的逻辑判断)
|
||||
* [3. 留意不同分支下的重复代码](#3-留意不同分支下的重复代码)
|
||||
* [4. 谨慎使用三元表达式](#4-谨慎使用三元表达式)
|
||||
* [常见技巧](#常见技巧)
|
||||
* [1. 使用“德摩根定律”](#1-使用德摩根定律)
|
||||
* [2. 自定义对象的“布尔真假”](#2-自定义对象的布尔真假)
|
||||
* [3. 在条件判断中使用 all() / any()](#3-在条件判断中使用-all--any)
|
||||
* [4. 使用 try/while/for 中 else 分支](#4-使用-trywhilefor-中-else-分支)
|
||||
* [常见陷阱](#常见陷阱)
|
||||
* [1. 与 None 值的比较](#1-与-none-值的比较)
|
||||
* [2. 留意 and 和 or 的运算优先级](#2-留意-and-和-or-的运算优先级)
|
||||
* [结语](#结语)
|
||||
* [注解](#注解)
|
||||
- [Python 工匠:编写条件分支代码的技巧](#python-工匠编写条件分支代码的技巧)
|
||||
- [序言](#序言)
|
||||
- [内容目录](#内容目录)
|
||||
- [Python 里的分支代码](#python-里的分支代码)
|
||||
- [最佳实践](#最佳实践)
|
||||
- [1. 避免多层分支嵌套](#1-避免多层分支嵌套)
|
||||
- [2. 封装那些过于复杂的逻辑判断](#2-封装那些过于复杂的逻辑判断)
|
||||
- [3. 留意不同分支下的重复代码](#3-留意不同分支下的重复代码)
|
||||
- [4. 谨慎使用三元表达式](#4-谨慎使用三元表达式)
|
||||
- [常见技巧](#常见技巧)
|
||||
- [1. 使用“德摩根定律”](#1-使用德摩根定律)
|
||||
- [2. 自定义对象的“布尔真假”](#2-自定义对象的布尔真假)
|
||||
- [3. 在条件判断中使用 all() / any()](#3-在条件判断中使用-all--any)
|
||||
- [4. 使用 try/while/for 中 else 分支](#4-使用-trywhilefor-中-else-分支)
|
||||
- [常见陷阱](#常见陷阱)
|
||||
- [1. 与 None 值的比较](#1-与-none-值的比较)
|
||||
- [2. 留意 and 和 or 的运算优先级](#2-留意-and-和-or-的运算优先级)
|
||||
- [结语](#结语)
|
||||
- [注解](#注解)
|
||||
|
||||
### Python 里的分支代码
|
||||
|
||||
|
@ -42,7 +46,7 @@ Python 支持最为常见的 `if/else` 条件分支语句,不过它缺少在
|
|||
|
||||
如果这篇文章只能删减成一句话就结束,那么那句话一定是**“要竭尽所能的避免分支嵌套”**。
|
||||
|
||||
过深的分支嵌套是很多编程新手最容易犯的错误之一。假如有一位新手 JavaScript 程序员写了很多层分支嵌套,那么你可能会看到一层又一层的大括号:`if { if { if { ... }}}`。俗称*“嵌套 if 地狱(Nested If Statement Hell)”*。
|
||||
过深的分支嵌套是很多编程新手最容易犯的错误之一。假如有一位新手 JavaScript 程序员写了很多层分支嵌套,那么你可能会看到一层又一层的大括号:`if { if { if { ... }}}`。俗称 *“嵌套 if 地狱(Nested If Statement Hell)”*。
|
||||
|
||||
但是因为 Python 使用了缩进来代替 `{}`,所以过深的嵌套分支会产生比其他语言下更为严重的后果。比如过多的缩进层次很容易就会让代码超过 [PEP8](https://www.python.org/dev/peps/pep-0008/) 中规定的每行字数限制。让我们看看这段代码:
|
||||
|
||||
|
@ -88,7 +92,7 @@ def buy_fruit(nerd, store):
|
|||
return buy_fruit(nerd, store)
|
||||
```
|
||||
|
||||
“提前结束”指:**在函数内使用 `return` 或 `raise` 等语句提前在分支内结束函数。**比如,在新的 `buy_fruit` 函数里,当分支条件不满足时,我们直接抛出异常,结束这段代码分支。这样的代码没有嵌套分支,更直接也更易读。
|
||||
“提前结束”指:**在函数内使用 `return` 或 `raise` 等语句提前在分支内结束函数。** 比如,在新的 `buy_fruit` 函数里,当分支条件不满足时,我们直接抛出异常,结束这段代码分支。这样的代码没有嵌套分支,更直接也更易读。
|
||||
|
||||
### 2. 封装那些过于复杂的逻辑判断
|
||||
|
||||
|
@ -111,7 +115,7 @@ if activity.allow_new_user() and user.match_activity_condition():
|
|||
return
|
||||
```
|
||||
|
||||
事实上,将代码改写后,之前的注释文字其实也可以去掉了。**因为后面这段代码已经达到了自说明的目的。**至于具体的 *什么样的用户满足活动条件?* 这种问题,就应由具体的 `match_activity_condition()` 方法来回答了。
|
||||
事实上,将代码改写后,之前的注释文字其实也可以去掉了。**因为后面这段代码已经达到了自说明的目的。** 至于具体的 *什么样的用户满足活动条件?* 这种问题,就应由具体的 `match_activity_condition()` 方法来回答了。
|
||||
|
||||
> **Hint:** 恰当的封装不光直接改善了代码的可读性,事实上,如果上面的活动判断逻辑在代码中出现了不止一次的话,封装更是必须的。不然重复代码会极大的破坏这段逻辑的可维护性。
|
||||
|
||||
|
@ -391,4 +395,3 @@ True
|
|||
> - 2018.04.08:在与 @geishu 的讨论后,调整了“运算优先符”使用的代码样例
|
||||
> - 2018.04.10:根据 @dongweiming 的建议,添加注解说明 "x and y or c" 表达式的陷阱
|
||||
|
||||
|
||||
|
|
|
@ -9,27 +9,31 @@
|
|||
|
||||
整型在 Python 中比较让人省心,因为它不区分有无符号并且永不溢出。但浮点型仍和绝大多数其他编程语言一样,依然有着精度问题,经常让很多刚进入编程世界大门的新人们感到困惑:["Why Are Floating Point Numbers Inaccurate?"](https://stackoverflow.com/questions/21895756/why-are-floating-point-numbers-inaccurate)。
|
||||
|
||||
相比数字,Python 里的字符串要复杂的多。要掌握它,你得先弄清楚 bytes 和 str 的区别。如果更不巧,你还是位 Python2 用户的话,光 unicode 和字符编码问题就够你喝上好几壶了*(赶快迁移到 Python3 吧,就在今天!)*。
|
||||
相比数字,Python 里的字符串要复杂的多。要掌握它,你得先弄清楚 bytes 和 str 的区别。如果更不巧,你还是位 Python2 用户的话,光 unicode 和字符编码问题就够你喝上好几壶了 *(赶快迁移到 Python3 吧,就在今天!)*。
|
||||
|
||||
不过,上面提到的这些都不是这篇文章的主题,如果感兴趣,你可以在网上找到成堆的相关资料。在这篇文章里,我们将讨论一些 **更细微、更不常见** 的编程实践。来帮助你写出更好的 Python 代码。
|
||||
|
||||
### 内容目录
|
||||
|
||||
* [最佳实践](#最佳实践)
|
||||
* [1. 少写数字字面量](#1-少写数字字面量)
|
||||
* [使用 enum 枚举类型改善代码](#使用-enum-枚举类型改善代码)
|
||||
* [2. 别在裸字符串处理上走太远](#2-别在裸字符串处理上走太远)
|
||||
* [3. 不必预计算字面量表达式](#3-不必预计算字面量表达式)
|
||||
* [实用技巧](#实用技巧)
|
||||
* [1. 布尔值其实也是“数字”](#1-布尔值其实也是数字)
|
||||
* [2. 改善超长字符串的可读性](#2-改善超长字符串的可读性)
|
||||
* [当多级缩进里出现多行字符串时](#当多级缩进里出现多行字符串时)
|
||||
* [3. 别忘了那些 “r” 开头的内建字符串函数](#3-别忘了那些-r-开头的内建字符串函数)
|
||||
* [4. 使用“无穷大” float("inf")](#4-使用无穷大-floatinf)
|
||||
* [常见误区](#常见误区)
|
||||
* [1. “value = 1” 并非线程安全](#1-value--1-并非线程安全)
|
||||
* [2. 字符串拼接并不慢](#2-字符串拼接并不慢)
|
||||
* [结语](#结语)
|
||||
- [Python 工匠:使用数字与字符串的技巧](#python-工匠使用数字与字符串的技巧)
|
||||
- [序言](#序言)
|
||||
- [内容目录](#内容目录)
|
||||
- [最佳实践](#最佳实践)
|
||||
- [1. 少写数字字面量](#1-少写数字字面量)
|
||||
- [使用 enum 枚举类型改善代码](#使用-enum-枚举类型改善代码)
|
||||
- [2. 别在裸字符串处理上走太远](#2-别在裸字符串处理上走太远)
|
||||
- [3. 不必预计算字面量表达式](#3-不必预计算字面量表达式)
|
||||
- [实用技巧](#实用技巧)
|
||||
- [1. 布尔值其实也是“数字”](#1-布尔值其实也是数字)
|
||||
- [2. 改善超长字符串的可读性](#2-改善超长字符串的可读性)
|
||||
- [当多级缩进里出现多行字符串时](#当多级缩进里出现多行字符串时)
|
||||
- [大数字也可以变得更加可读](#大数字也可以变得更加可读)
|
||||
- [3. 别忘了那些 “r” 开头的内建字符串函数](#3-别忘了那些-r-开头的内建字符串函数)
|
||||
- [4. 使用“无穷大” float("inf")](#4-使用无穷大-floatinf)
|
||||
- [常见误区](#常见误区)
|
||||
- [1. “value += 1” 并非线程安全](#1-value--1-并非线程安全)
|
||||
- [2. 字符串拼接并不慢](#2-字符串拼接并不慢)
|
||||
- [结语](#结语)
|
||||
|
||||
## 最佳实践
|
||||
|
||||
|
|
|
@ -19,25 +19,29 @@ Python 语言自身的内部实现细节也与这些容器类型息息相关。
|
|||
|
||||
### 内容目录
|
||||
|
||||
* [底层看容器](#底层看容器)
|
||||
* [写更快的代码](#写更快的代码)
|
||||
* [1. 避免频繁扩充列表/创建新列表](#1-避免频繁扩充列表创建新列表)
|
||||
* [2. 在列表头部操作多的场景使用 deque 模块](#2-在列表头部操作多的场景使用-deque-模块)
|
||||
* [3. 使用集合/字典来判断成员是否存在](#3-使用集合字典来判断成员是否存在)
|
||||
* [高层看容器](#高层看容器)
|
||||
* [写扩展性更好的代码](#写扩展性更好的代码)
|
||||
* [面向容器接口编程](#面向容器接口编程)
|
||||
* [常用技巧](#常用技巧)
|
||||
* [1. 使用元组改善分支代码](#1-使用元组改善分支代码)
|
||||
* [2. 在更多地方使用动态解包](#2-在更多地方使用动态解包)
|
||||
* [3. 使用 next() 函数](#3-使用-next-函数)
|
||||
* [4. 使用有序字典来去重](#4-使用有序字典来去重)
|
||||
* [常见误区](#常见误区)
|
||||
* [1. 当心那些已经枯竭的迭代器](#1-当心那些已经枯竭的迭代器)
|
||||
* [2. 别在循环体内修改被迭代对象](#2-别在循环体内修改被迭代对象)
|
||||
* [总结](#总结)
|
||||
* [系列其他文章](#系列其他文章)
|
||||
* [注解](#注解)
|
||||
- [Python 工匠:容器的门道](#python-工匠容器的门道)
|
||||
- [序言](#序言)
|
||||
- [内容目录](#内容目录)
|
||||
- [当我们谈论容器时,我们在谈些什么?](#当我们谈论容器时我们在谈些什么)
|
||||
- [底层看容器](#底层看容器)
|
||||
- [写更快的代码](#写更快的代码)
|
||||
- [1. 避免频繁扩充列表/创建新列表](#1-避免频繁扩充列表创建新列表)
|
||||
- [2. 在列表头部操作多的场景使用 deque 模块](#2-在列表头部操作多的场景使用-deque-模块)
|
||||
- [3. 使用集合/字典来判断成员是否存在](#3-使用集合字典来判断成员是否存在)
|
||||
- [高层看容器](#高层看容器)
|
||||
- [写扩展性更好的代码](#写扩展性更好的代码)
|
||||
- [面向容器接口编程](#面向容器接口编程)
|
||||
- [常用技巧](#常用技巧)
|
||||
- [1. 使用元组改善分支代码](#1-使用元组改善分支代码)
|
||||
- [2. 在更多地方使用动态解包](#2-在更多地方使用动态解包)
|
||||
- [3. 使用 next() 函数](#3-使用-next-函数)
|
||||
- [4. 使用有序字典来去重](#4-使用有序字典来去重)
|
||||
- [常见误区](#常见误区)
|
||||
- [1. 当心那些已经枯竭的迭代器](#1-当心那些已经枯竭的迭代器)
|
||||
- [2. 别在循环体内修改被迭代对象](#2-别在循环体内修改被迭代对象)
|
||||
- [总结](#总结)
|
||||
- [系列其他文章](#系列其他文章)
|
||||
- [注解](#注解)
|
||||
|
||||
|
||||
### 当我们谈论容器时,我们在谈些什么?
|
||||
|
@ -317,7 +321,7 @@ user = {**{"name": "piglei"}, **{"movies": ["Fight Club"]}}
|
|||
|
||||
### 3. 使用 next() 函数
|
||||
|
||||
`next()` 是一个非常实用的内建函数,它接收一个迭代器作为参数,然后返回该迭代器的下一个元素。使用它配合生成器表达式,可以高效的实现*“从列表中查找第一个满足条件的成员”* 之类的需求。
|
||||
`next()` 是一个非常实用的内建函数,它接收一个迭代器作为参数,然后返回该迭代器的下一个元素。使用它配合生成器表达式,可以高效的实现 *“从列表中查找第一个满足条件的成员”* 之类的需求。
|
||||
|
||||
```python
|
||||
numbers = [3, 7, 8, 2, 21]
|
||||
|
|
|
@ -16,31 +16,35 @@
|
|||
|
||||
Python 函数通过调用 `return` 语句来返回结果。使用 `return value` 可以返回单个值,用 `return value1, value2` 则能让函数同时返回多个值。
|
||||
|
||||
如果一个函数体内没有任何 `return` 语句,那么这个函数的返回值默认为 `None`。除了通过 `return` 语句返回内容,在函数内还可以使用抛出异常*(raise Exception)*的方式来“返回结果”。
|
||||
如果一个函数体内没有任何 `return` 语句,那么这个函数的返回值默认为 `None`。除了通过 `return` 语句返回内容,在函数内还可以使用抛出异常 *(raise Exception)* 的方式来“返回结果”。
|
||||
|
||||
接下来,我将列举一些与函数返回相关的常用编程建议。
|
||||
|
||||
### 内容目录
|
||||
|
||||
* [编程建议](#编程建议)
|
||||
* [1. 单个函数不要返回多种类型](#1-单个函数不要返回多种类型)
|
||||
* [2. 使用 partial 构造新函数](#2-使用-partial-构造新函数)
|
||||
* [3. 抛出异常,而不是返回结果与错误](#3-抛出异常而不是返回结果与错误)
|
||||
* [4. 谨慎使用 None 返回值](#4-谨慎使用-none-返回值)
|
||||
* [1. 作为操作类函数的默认返回值](#1-作为操作类函数的默认返回值)
|
||||
* [2. 作为某些“意料之中”的可能没有的值](#2-作为某些意料之中的可能没有的值)
|
||||
* [3. 作为调用失败时代表“错误结果”的值](#3-作为调用失败时代表错误结果的值)
|
||||
* [5. 合理使用“空对象模式”](#5-合理使用空对象模式)
|
||||
* [6. 使用生成器函数代替返回列表](#6-使用生成器函数代替返回列表)
|
||||
* [7. 限制递归的使用](#7-限制递归的使用)
|
||||
* [总结](#总结)
|
||||
* [附录](#附录)
|
||||
- [Python 工匠:让函数返回结果的技巧](#python-工匠让函数返回结果的技巧)
|
||||
- [序言](#序言)
|
||||
- [Python 的函数返回方式](#python-的函数返回方式)
|
||||
- [内容目录](#内容目录)
|
||||
- [编程建议](#编程建议)
|
||||
- [1. 单个函数不要返回多种类型](#1-单个函数不要返回多种类型)
|
||||
- [2. 使用 partial 构造新函数](#2-使用-partial-构造新函数)
|
||||
- [3. 抛出异常,而不是返回结果与错误](#3-抛出异常而不是返回结果与错误)
|
||||
- [4. 谨慎使用 None 返回值](#4-谨慎使用-none-返回值)
|
||||
- [1. 作为操作类函数的默认返回值](#1-作为操作类函数的默认返回值)
|
||||
- [2. 作为某些“意料之中”的可能没有的值](#2-作为某些意料之中的可能没有的值)
|
||||
- [3. 作为调用失败时代表“错误结果”的值](#3-作为调用失败时代表错误结果的值)
|
||||
- [5. 合理使用“空对象模式”](#5-合理使用空对象模式)
|
||||
- [6. 使用生成器函数代替返回列表](#6-使用生成器函数代替返回列表)
|
||||
- [7. 限制递归的使用](#7-限制递归的使用)
|
||||
- [总结](#总结)
|
||||
- [附录](#附录)
|
||||
|
||||
## 编程建议
|
||||
|
||||
### 1. 单个函数不要返回多种类型
|
||||
|
||||
Python 语言非常灵活,我们能用它轻松完成一些在其他语言里很难做到的事情。比如:*让一个函数同时返回不同类型的结果。*从而实现一种看起来非常实用的“多功能函数”。
|
||||
Python 语言非常灵活,我们能用它轻松完成一些在其他语言里很难做到的事情。比如:*让一个函数同时返回不同类型的结果。* 从而实现一种看起来非常实用的“多功能函数”。
|
||||
|
||||
就像下面这样:
|
||||
|
||||
|
@ -60,9 +64,9 @@ get_users()
|
|||
|
||||
当我们需要获取单个用户时,就传递 `user_id` 参数,否则就不传参数拿到所有活跃用户列表。一切都由一个函数 `get_users` 来搞定。这样的设计似乎很合理。
|
||||
|
||||
然而在函数的世界里,以编写具备“多功能”的瑞士军刀型函数为荣不是一件好事。这是因为好的函数一定是 [“单一职责(Single responsibility)”](https://en.wikipedia.org/wiki/Single_responsibility_principle) 的。**单一职责意味着一个函数只做好一件事,目的明确。**这样的函数也更不容易在未来因为需求变更而被修改。
|
||||
然而在函数的世界里,以编写具备“多功能”的瑞士军刀型函数为荣不是一件好事。这是因为好的函数一定是 [“单一职责(Single responsibility)”](https://en.wikipedia.org/wiki/Single_responsibility_principle) 的。**单一职责意味着一个函数只做好一件事,目的明确。** 这样的函数也更不容易在未来因为需求变更而被修改。
|
||||
|
||||
而返回多种类型的函数一定是违反“单一职责”原则的,**好的函数应该总是提供稳定的返回值,把调用方的处理成本降到最低。**像上面的例子,我们应该编写两个独立的函数 `get_user_by_id(user_id)`、`get_active_users()` 来替代。
|
||||
而返回多种类型的函数一定是违反“单一职责”原则的,**好的函数应该总是提供稳定的返回值,把调用方的处理成本降到最低。** 像上面的例子,我们应该编写两个独立的函数 `get_user_by_id(user_id)`、`get_active_users()` 来替代。
|
||||
|
||||
### 2. 使用 partial 构造新函数
|
||||
|
||||
|
@ -165,7 +169,7 @@ def create_for_input():
|
|||
|
||||
### 4. 谨慎使用 None 返回值
|
||||
|
||||
`None` 值通常被用来表示**“某个应该存在但是缺失的东西”**,它在 Python 里是独一无二的存在。很多编程语言里都有与 None 类似的设计,比如 JavaScript 里的 `null`、Go 里的 `nil` 等。因为 None 所拥有的独特 *虚无* 气质,它经常被作为函数返回值使用。
|
||||
`None` 值通常被用来表示 **“某个应该存在但是缺失的东西”**,它在 Python 里是独一无二的存在。很多编程语言里都有与 None 类似的设计,比如 JavaScript 里的 `null`、Go 里的 `nil` 等。因为 None 所拥有的独特 *虚无* 气质,它经常被作为函数返回值使用。
|
||||
|
||||
当我们使用 None 作为函数返回值时,通常是下面 3 种情况。
|
||||
|
||||
|
@ -212,7 +216,7 @@ if user:
|
|||
|
||||
对于那些不能从函数名里读出 None 值暗示的函数来说,有两种修改方式。第一种,如果你坚持使用 None 返回值,那么请修改函数的名称。比如可以将函数 `create_user_from_name()` 改名为 `create_user_or_none()`。
|
||||
|
||||
第二种方式则更常见的多:用抛出异常*(raise Exception)*来代替 None 返回值。因为,如果返回不了正常结果并非函数意义里的一部分,这就代表着函数出现了*“意料以外的状况”*,而这正是 **Exceptions 异常** 所掌管的领域。
|
||||
第二种方式则更常见的多:用抛出异常 *(raise Exception)* 来代替 None 返回值。因为,如果返回不了正常结果并非函数意义里的一部分,这就代表着函数出现了 *“意料以外的状况”*,而这正是 **Exceptions 异常** 所掌管的领域。
|
||||
|
||||
使用异常改写后的例子:
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
|
||||
### 1. 只做最精确的异常捕获
|
||||
|
||||
假如你不够了解异常机制,就难免会对它有一种天然恐惧感。你可能会觉得:*异常是一种不好的东西,好的程序就应该捕获所有的异常,让一切都平平稳稳的运行。*而抱着这种想法写出的代码,里面通常会出现大段含糊的异常捕获逻辑。
|
||||
假如你不够了解异常机制,就难免会对它有一种天然恐惧感。你可能会觉得:*异常是一种不好的东西,好的程序就应该捕获所有的异常,让一切都平平稳稳的运行。* 而抱着这种想法写出的代码,里面通常会出现大段含糊的异常捕获逻辑。
|
||||
|
||||
让我们用一段可执行脚本作为样例:
|
||||
|
||||
|
@ -110,7 +110,7 @@ def save_website_title(url, filename):
|
|||
|
||||
### 2. 别让异常破坏抽象一致性
|
||||
|
||||
大约四五年前,当时的我正在开发某移动应用的后端 API 项目。如果你也有过开发后端 API 的经验,那么你一定知道,这样的系统都需要制定一套**“API 错误码规范”**,来为客户端处理调用错误时提供方便。
|
||||
大约四五年前,当时的我正在开发某移动应用的后端 API 项目。如果你也有过开发后端 API 的经验,那么你一定知道,这样的系统都需要制定一套 **“API 错误码规范”**,来为客户端处理调用错误时提供方便。
|
||||
|
||||
一个错误码返回大概长这个样子:
|
||||
|
||||
|
@ -163,7 +163,7 @@ def process_image(...):
|
|||
- 我必须引入 `APIErrorCode` 异常类作为依赖来捕获异常
|
||||
- **哪怕我的脚本和 Django API 根本没有任何关系**
|
||||
|
||||
**这就是异常类抽象层级不一致导致的结果。**APIErrorCode 异常类的意义,在于表达一种能够直接被终端用户(人)识别并消费的“错误代码”。**它在整个项目里,属于最高层的抽象之一。**但是出于方便,我们却在底层模块里引入并抛出了它。这打破了 `image.processor` 模块的抽象一致性,影响了它的可复用性和可维护性。
|
||||
**这就是异常类抽象层级不一致导致的结果**。`APIErrorCode` 异常类的意义,在于表达一种能够直接被终端用户(人)识别并消费的“错误代码”。**它在整个项目里,属于最高层的抽象之一**。但是出于方便,我们却在底层模块里引入并抛出了它。这打破了 `image.processor` 模块的抽象一致性,影响了它的可复用性和可维护性。
|
||||
|
||||
这类情况属于“模块抛出了**高于**所属抽象层级的异常”。避免这类错误需要注意以下几点:
|
||||
|
||||
|
|
|
@ -44,7 +44,7 @@ for i, name in enumerate(names):
|
|||
|
||||
不过,判断某段循环代码是否地道,并不仅仅是以知道或不知道某个内置方法作为标准。我们可以从上面的例子挖掘出更深层的东西。
|
||||
|
||||
如你所见,Python 的 `for` 循环只有 `for <item> in <iterable>` 这一种结构,而结构里的前半部分 - *赋值给 item* - 没有太多花样可玩。所以后半部分的 **可迭代对象** 是我们唯一能够大做文章的东西。而以 `enumerate()` 函数为代表的*“修饰函数”*,刚好提供了一种思路:**通过修饰可迭代对象来优化循环本身。**
|
||||
如你所见,Python 的 `for` 循环只有 `for <item> in <iterable>` 这一种结构,而结构里的前半部分 - *赋值给 item* - 没有太多花样可玩。所以后半部分的 **可迭代对象** 是我们唯一能够大做文章的东西。而以 `enumerate()` 函数为代表的 *“修饰函数”*,刚好提供了一种思路:**通过修饰可迭代对象来优化循环本身。**
|
||||
|
||||
这就引出了我的第一个建议。
|
||||
|
||||
|
@ -248,7 +248,7 @@ def award_active_users_in_last_30days():
|
|||
|
||||
在计算机的世界里,我们经常用**“耦合”**这个词来表示事物之间的关联关系。上面的例子中,*“挑选时间”*和*“发送积分”*这两件事情身处同一个循环体内,建立了非常强的耦合关系。
|
||||
|
||||
为了更好的进行代码复用,我们需要把函数里的*“挑选时间”*部分从循环体中解耦出来。而我们的老朋友,**“生成器函数”**是进行这项工作的不二之选。
|
||||
为了更好的进行代码复用,我们需要把函数里的*“挑选时间”*部分从循环体中解耦出来。而我们的老朋友,**“生成器函数”** 是进行这项工作的不二之选。
|
||||
|
||||
### 使用生成器函数解耦循环体
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<img src="https://www.zlovezl.cn/static/uploaded/2019/05/clem-onojeghuo-142120-unsplash_w1280.jpg" width="100%" />
|
||||
</div>
|
||||
|
||||
装饰器*(Decorator)* 是 Python 里的一种特殊工具,它为我们提供了一种在函数外部修改函数的灵活能力。它有点像一顶画着独一无二 `@` 符号的神奇帽子,只要将它戴在函数头顶上,就能悄无声息的改变函数本身的行为。
|
||||
装饰器 *(Decorator)* 是 Python 里的一种特殊工具,它为我们提供了一种在函数外部修改函数的灵活能力。它有点像一顶画着独一无二 `@` 符号的神奇帽子,只要将它戴在函数头顶上,就能悄无声息的改变函数本身的行为。
|
||||
|
||||
你可能已经和装饰器打过不少交道了。在做面向对象编程时,我们就经常会用到 `@staticmethod` 和 `@classmethod` 两个内置装饰器。此外,如果你接触过 [click](https://click.palletsprojects.com/en/7.x/) 模块,就更不会对装饰器感到陌生。click 最为人所称道的参数定义接口 `@click.option(...)` 就是利用装饰器实现的。
|
||||
|
||||
|
@ -32,7 +32,7 @@ True
|
|||
|
||||
函数自然是“可被调用”的对象。但除了函数外,我们也可以让任何一个类(class)变得“可被调用”(callable)。办法很简单,只要自定义类的 `__call__` 魔法方法即可。
|
||||
|
||||
```
|
||||
```python
|
||||
class Foo:
|
||||
def __call__(self):
|
||||
print("Hello, __call___")
|
||||
|
@ -80,7 +80,7 @@ def delay(duration):
|
|||
|
||||
如何使用装饰器的样例代码:
|
||||
|
||||
```
|
||||
```python
|
||||
@delay(duration=2)
|
||||
def add(a, b):
|
||||
return a + b
|
||||
|
@ -135,7 +135,7 @@ def print_random_number(num):
|
|||
print_random_number()
|
||||
```
|
||||
|
||||
`@provide_number` 装饰器功能看上去很不错,但它有着我在前面提到的两个问题:**嵌套层级深、无法在类方法上使用。**如果直接用它去装饰类方法,会出现下面的情况:
|
||||
`@provide_number` 装饰器功能看上去很不错,但它有着我在前面提到的两个问题:**嵌套层级深、无法在类方法上使用。** 如果直接用它去装饰类方法,会出现下面的情况:
|
||||
|
||||
```
|
||||
class Foo:
|
||||
|
@ -149,7 +149,7 @@ Foo().print_random_number()
|
|||
|
||||
`Foo` 类实例中的 `print_random_number` 方法将会输出类实例 `self` ,而不是我们期望的随机数 `num`。
|
||||
|
||||
之所以会出现这个结果,是因为类方法*(method)*和函数*(function)*二者在工作机制上有着细微不同。如果要修复这个问题,`provider_number` 装饰器在修改类方法的位置参数时,必须聪明的跳过藏在 `*args` 里面的类实例 `self` 变量,才能正确的将 `num` 作为第一个参数注入。
|
||||
之所以会出现这个结果,是因为类方法 *(method)* 和函数 *(function)* 二者在工作机制上有着细微不同。如果要修复这个问题,`provider_number` 装饰器在修改类方法的位置参数时,必须聪明的跳过藏在 `*args` 里面的类实例 `self` 变量,才能正确的将 `num` 作为第一个参数注入。
|
||||
|
||||
这时,就应该是 [wrapt](https://pypi.org/project/wrapt/) 模块闪亮登场的时候了。`wrapt` 模块是一个专门帮助你编写装饰器的工具库。利用它,我们可以非常方便的改造 `provide_number` 装饰器,完美解决*“嵌套层级深”*和*“无法通用”*两个问题,
|
||||
|
||||
|
@ -198,9 +198,9 @@ Foo().print_random_number()
|
|||
|
||||
不过 [*“装饰器模式(Decorator Pattern)”*](https://en.wikipedia.org/wiki/Decorator_pattern) 是个例外。因为 Python 的“装饰器”和“装饰器模式”有着一模一样的名字,我不止一次听到有人把它们俩当成一回事,认为使用“装饰器”就是在实践“装饰器模式”。但事实上,**它们是两个完全不同的东西。**
|
||||
|
||||
“装饰器模式”是一个完全基于“面向对象”衍生出的编程手法。它拥有几个关键组成:**一个统一的接口定义**、**若干个遵循该接口的类**、**类与类之间一层一层的包装**。最终由它们共同形成一种*“装饰”*的效果。
|
||||
“装饰器模式”是一个完全基于“面向对象”衍生出的编程手法。它拥有几个关键组成:**一个统一的接口定义**、**若干个遵循该接口的类**、**类与类之间一层一层的包装**。最终由它们共同形成一种 *“装饰”* 的效果。
|
||||
|
||||
而 Python 里的“装饰器”和“面向对象”没有任何直接联系,**它完全可以只是发生在函数和函数间的把戏。**事实上,“装饰器”并没有提供某种无法替代的功能,它仅仅就是一颗[“语法糖”](https://en.wikipedia.org/wiki/Syntactic_sugar)而已。下面这段使用了装饰器的代码:
|
||||
而 Python 里的“装饰器”和“面向对象”没有任何直接联系,**它完全可以只是发生在函数和函数间的把戏**。事实上,“装饰器”并没有提供某种无法替代的功能,它仅仅就是一颗[“语法糖”](https://en.wikipedia.org/wiki/Syntactic_sugar)而已。下面这段使用了装饰器的代码:
|
||||
|
||||
```python
|
||||
@log_time
|
||||
|
@ -210,7 +210,7 @@ def foo(): pass
|
|||
|
||||
基本完全等同于下面这样:
|
||||
|
||||
```
|
||||
```python
|
||||
def foo(): pass
|
||||
|
||||
foo = log_time(cache_result(foo))
|
||||
|
|
Loading…
Reference in New Issue