Merge pull request #79 from wgzhao/fix_md_syntax_error

Fix some markdown syntax errors
This commit is contained in:
piglei 2021-12-05 20:40:20 +08:00 committed by GitHub
commit df664c3bc2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 412 additions and 393 deletions

View File

@ -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-定义临时变量提升可读性)
- [结语](#结语)
## 如何为变量起名

View File

@ -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

View File

@ -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) 对象就是其中之一。它是一种基于内存的特殊对象,拥有和文件对象几乎一致的接口设计。

View File

@ -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 种设计模式,你知道是哪些吗?)*
让我们最后再总结一下吧:

View File

@ -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*),通常不是最佳解决方案
- 违反里氏替换原则,通常也会导致违反“开放-关闭”原则
- 考虑什么是类的核心特征,然后为父类增加新的方法和属性可以帮到你

View File

@ -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 又依赖了 WerkzeugWerkzeug 又由更多个低层模块组成。
依赖倒置原则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/)

View File

@ -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:

View File

@ -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" 表达式的陷阱

View File

@ -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-字符串拼接并不慢)
- [结语](#结语)
## 最佳实践

View File

@ -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]

View File

@ -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 异常** 所掌管的领域。
使用异常改写后的例子:

View File

@ -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` 模块的抽象一致性,影响了它的可复用性和可维护性。
这类情况属于“模块抛出了**高于**所属抽象层级的异常”。避免这类错误需要注意以下几点:

View File

@ -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():
在计算机的世界里,我们经常用**“耦合”**这个词来表示事物之间的关联关系。上面的例子中,*“挑选时间”*和*“发送积分”*这两件事情身处同一个循环体内,建立了非常强的耦合关系。
为了更好的进行代码复用,我们需要把函数里的*“挑选时间”*部分从循环体中解耦出来。而我们的老朋友,**“生成器函数”**是进行这项工作的不二之选。
为了更好的进行代码复用,我们需要把函数里的*“挑选时间”*部分从循环体中解耦出来。而我们的老朋友,**“生成器函数”** 是进行这项工作的不二之选。
### 使用生成器函数解耦循环体

View File

@ -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))