# Python工匠：案例、技巧与工程实践
本书基于受欢迎的“Python工匠”系列开源文章。全书从工程实践角度出发，通过剖析核心知识、展示典型案例与总结实用技巧，帮助大家系统进阶Python，写好工程代码，做好实践项目。 本书共计13章，分为五大部分：变量与基础类型、语法结构、函数与装饰器、面向对象编程、总结与延伸，涵盖Python编程的方方面面。本书的写作方式别具一格，核心知识点都会通过三大板块来阐述：基础知识、案例故事、编程建议。其中基础知识帮助大家快速回顾Python基础；案例故事由作者经历的编程项目与案例改编而来，兼具实战性与趣味性；编程建议以大家喜闻乐见的条目式知识点呈现，短小精悍，可直接应用于自己的编程实践中。 ...

2024年10月14日30分钟阅读

- Personal/Book
- Read/计算机-编程设计

## 简介

- 书名：《Python工匠：案例、技巧与工程实践》
- 作者： 朱雷
- 分类： 计算机-编程设计
- ISBN：9787115584045
- 出版社：人民邮电出版社有限公司

## 概述

本书基于受欢迎的“Python工匠”系列开源文章。全书从工程实践角度出发，通过剖析核心知识、展示典型案例与总结实用技巧，帮助大家系统进阶Python，写好工程代码，做好实践项目。 本书共计13章，分为五大部分：变量与基础类型、语法结构、函数与装饰器、面向对象编程、总结与延伸，涵盖Python编程的方方面面。本书的写作方式别具一格，核心知识点都会通过三大板块来阐述：基础知识、案例故事、编程建议。其中基础知识帮助大家快速回顾Python基础；案例故事由作者经历的编程项目与案例改编而来，兼具实战性与趣味性；编程建议以大家喜闻乐见的条目式知识点呈现，短小精悍，可直接应用于自己的编程实践中。

## 划线

> 即便两个人实现同一个功能，最终效果看上去也一模一样，但代码质量却可能有着云泥之别。

> 我前前后后读过一些书——《代码大全》《重构》《设计模式》《代码整洁之道》——毫无疑问，它们都是领域内首屈一指的经典好书，我从中学到了许多知识，至今受益匪浅。

> 编程是一个通过代码来表达思想的过程。

> 变量与注释是作者表达思想的基础，是读者理解代码的第一道门，它们对代码质量的贡献毋庸置疑。

> 除了上面的普通解包外，Python还支持更灵活的动态解包语法。只要用星号表达式（*variables）作为变量名，它便会贪婪[插图]地捕获多个值对象，并将捕获到的内容作为列表赋值给variables。

> 在常用的诸多变量名中，单下划线_是比较特殊的一个。它常作为一个无意义的占位符出现在赋值语句中。_这个名字本身没什么特别之处，这算是大家约定俗成的一种用法。

> 相比编写Sphinx格式文档，我其实更推荐使用类型注解，因为它是Python的内置功能，而且正在变得越来越流行。

> 因此，我强烈建议在多人参与的中大型Python项目里，至少使用一种类型注解方案——Sphinx格式文档或官方类型注解都行。

> 计算机科学领域只有两件难事：缓存失效和命名。

> 描述代码为什么要这么做，而不是简单复述代码本身。

> 指引性注释。这种注释并不直接复述代码，而是简明扼要地概括代码功能，起到“代码导读”的作用。

> 这些变化让整段代码变得更易读，也让整个算法变得更好理解。所以，哪怕是一段不到10行代码的简单函数，对变量和注释的不同处理方式，也会让代码发生质的变化

> 喜欢把所有变量初始化定义写在一起，放在函数最前面

> 总是从代码的职责出发，而不是其他东西。

> 直接翻译业务逻辑的代码，大多不是好代码。优秀的程序设计需要在理解原需求的基础上，恰到好处地抽象，只有这样才能同时满足可读性和可扩展性方面的需求

> 定义一个临时变量”是诸多方式里不太起眼的一个，但用得恰当的话效果也很巧妙。

> 函数内变量的数量太多，通常意味着函数过于复杂，承担了太多职责。只有把复杂函数拆分为多个小函数，代码的整体复杂度才可能实现根本性的降低。

> 这样的代码就像删掉赘语的句子，变得更精练、更易读。

> 在编写了许多函数以后，我总结出了一个值得推广的好习惯：先写注释，后写代码。

> 在写出一句有说服力的接口注释前，别写任何函数代码。

> 在定义数值字面量时，如果数字特别长，可以通过插入_分隔符来让它变得更易读

> True和False这两个布尔值可以直接当作1和0来使用

> f-string格式化方式用起来最方便

> 首先创建一个空列表，然后把需要拼接的字符串都放进列表，最后调用str.join来获得大字符串

> 把数字字面量改成常量和枚举类型后，我们就能很好地规避输入错误问题。同样，把字符串字面量改写成枚举类型，也可以获得这种好处

> 如果需要验证某个“经验之谈”，dis和timeit两个优秀的工具可以帮到你：前者能让你直接查看编译后的字节码，后者则能让你方便地做性能测试。

> Python里的字典在底层使用了哈希表（hash table）数据结构。当你往字典里存放一对key: value时，Python会先通过哈希算法计算出key的哈希值——一个整型数字；然后根据这个哈希值，决定数据在表里的具体位置。

> 集合只能存放可哈希对象

> 当我们把某个对象放进集合或者作为字典的键使用时，解释器都需要对该对象进行一次哈希运算，得到哈希值，然后再进行后面的操作。

> 虽然都是返回结果，但yield和return的最大不同之处在于，return的返回是一次性的，使用它会直接中断整个函数执行，而yield可以逐步给调用方生成结果：

> 我们有时会过于喜欢用not关键字，反倒忘记了运算符本身就可以表达否定逻辑。最后，代码里会出现许多下面这种判断语句：

> 异常处理对于我来说，就是一些不想做却又不得不做的琐事

> 在Python世界里，EAFP指不做任何事前检查，直接执行操作，但在外层用try来捕获可能发生的异常。如果还用下雨举例，这种做法类似于“出门前不看天气预报，如果淋雨了，就回家后洗澡吃感冒药”。

> Python里的函数可以一次返回多个值（通过返回一个元组实现）。所以，当我们要表明函数执行出错时，可以让它同时返回结果与错误信息。

> 新函数拥有更稳定的返回值类型，它永远只会返回Item类型或是抛出异常。· 虽然我们鼓励使用异常，但异常总是会不可避免地让人“感到惊讶”，所以，最好在函数文档里说明可能抛出的异常类型。· 不同于返回值，异常在被捕获前会不断往调用栈上层汇报。因此create_item()的直接调用方也可以完全不处理CreateItemError，而交由更上层处理。异常的这个特点给了我们更多灵活性，但同时也带来了更大的风险。具体来说，假如程序缺少一个顶级的统一异常处理逻辑，那么某个被所有人忽视了的异常可能会层层上报，最终弄垮整个程序。

> 上下文管理器功能强大、用处很多，其中最常见的用处之一，就是简化异常处理工作。

> 除了应该避免抛出高于当前抽象级别的异常外，我们同样应该避免泄露低于当前抽象级别的异常

> 在数据校验这块，pydantic模块是一个不错的选择。

> 在编写代码时，我们应当尽量避免手动校验任何数据。因为数据校验任务独立性很强，所以应该引入合适的第三方校验模块（或者自己实现），让它们来处理这部分专业工作。

> 可迭代对象不一定是迭代器，但迭代器一定是可迭代对象；· 对可迭代对象使用iter()会返回迭代器，迭代器则会返回其自身；· 每个迭代器的被迭代过程是一次性的，可迭代对象则不一定；· 可迭代对象只需要实现__iter__方法，而迭代器要额外实现__next__方法。

> 生成器（generator）利用其简单的语法，大大降低了迭代器的使用门槛，是优化循环代码时最得力的帮手。

> enumerate()是Python的一个内置函数，它接收一个可迭代对象作为参数，返回一个不断生成(当前下标,当前元素)的新可迭代对象。

> “修饰可迭代对象”是指用生成器（或普通的迭代器）在循环外部包装原本的循环主体，完成一些原本必须在循环内部执行的工作——比如过滤特定成员、提供额外结果等，以此简化循环代码。

> product()接收多个可迭代对象作为参数，然后根据它们的笛卡儿积不断生成结果：

> 用product()优化函数里的嵌套循环

> takewhile(predicate, iterable)会在迭代第二个参数iterable的过程中，不断使用当前值作为参数调用predicate()函数，并对返回结果进行真值测试，如果为True，则返回当前值并继续迭代，否则立即中断本次迭代。

> for循环（和while循环）后的else关键字，代表如果循环正常结束（没有碰到任何break），便执行该分支内的语句。因此，老式的“循环+标记变量”代码，就可以利用该特性简写为“循环+else分支”

> 因为Python语言不支持“带标签的break”语句[插图]，无法用一个break跳出多层循环。

> 为了规避这个问题，使用None来替代可变类型默认值是比较常见的做法：

> 当你要调用参数较多（超过3个）的函数时，使用关键字参数模式可以大大提高代码的可读性。

> 每个函数只返回一种类型，变得更简单易用。

> 适合返回None的函数需要满足以下两个特点：（1）函数的名称和参数必须表达“结果可能缺失”的意思；（2）如果函数执行无法产生结果，调用方也不关心具体原因。

> 除了“搜索”“查询”几个场景外，对绝大部分函数而言，返回None并不是一个好的做法。

> 在编写函数时，请不要纠结函数是不是应该只有一个return，只要尽早返回结果可以提升代码可读性，那就多多返回吧。

> 为了简化函数调用，让代码更简洁，我们其实可以定义一个接收单个参数的double()函数，让它通过multiply()完成计算：def double(value): # 返回 multiply 函数调用结果 return multiply(2, value)# 调用代码变得更简单result = double(value)val = double(number)

> 原来，在使用re.sub(pattern, repl, string)函数时，第二个参数repl不光可以是普通字符串，还可以是一个可调用的函数对象。

> 截至上一个问题，小R所写的mosaic_matchobj()函数只是一个无状态函数。但为了满足新需求，小R需要调整mosaic_matchobj()函数，把它从一个无状态函数改为有状态函数。

> 闭包是一种非常有用的工具，非常适合用来实现简单的有状态函数。

> 权衡了这三种方案的利弊后

> 别写太复杂的函数

> Python里的递归因为缺少语言层面的优化，局限性较大。当你想用递归来实现某个算法时，请先琢磨琢磨是否能用循环来改写。如果答案是肯定的，那就改成循环吧。

> 装饰器并不提供任何独特的功能，它所做的，只是让我们可以在函数定义语句上方，直接添加用来修改函数行为的装饰器函数

> 装饰器是一种通过包装目标函数来修改其行为的特殊高阶函数，绝大多数装饰器是利用函数的闭包原理实现的。

> 添加@wraps(wrapped)来装饰decorated函数后，wraps()首先会基于原函数func来更新包装函数decorated的名称、文档等内置属性，之后会将func的所有额外属性赋值到decorated上

> 所以，装饰器的优势并不在于它提供了动态修改函数的能力，而在于它把影响函数的装饰行为移到了函数头部，降低了代码的阅读与理解成本。

> （1） 运行时校验：在执行阶段进行特定校验，当校验通不过时终止执行。· 适合原因：装饰器可以方便地在函数执行前介入，并且可以读取所有参数辅助校验。· 代表样例：Django框架中的用户登录态校验装饰器@login_required。（2） 注入额外参数：在函数被调用时自动注入额外的调用参数。· 适合原因：装饰器的位置在函数头部，非常靠近参数被定义的位置，关联性强。· 代表样例：unittest.mock模块的装饰器@patch。（3） 缓存执行结果：通过调用参数等输入信息，直接缓存函数执行结果。· 适合原因：添加缓存不需要侵入函数内部逻辑，并且功能非常独立和通用。· 代表样例：functools模块的缓存装饰器@lru_cache。（4） 注册函数：将被装饰函数注册为某个外部流程的一部分。· 适合原因：在定义函数时可以直接完成注册，关联性强。· 代表样例：Flask框架的路由注册装饰器@app.route。（5）替换为复杂对象：将原函数（方法）替换为更复杂的对象，比如类实例或特殊的描述符对象（见12.1.3节）。· 适合原因：在执行替换操作时，装饰器语法天然比foo = staticmethod(foo)的写法要直观得多。· 代表样例：静态类方法装饰器@staticmethod。在设计新的装饰器时，你可以先参考上面的常见装饰器功能列表，琢磨琢磨自己的设计是否能很好地发挥装饰器的优势。切勿滥用装饰器技术，设计出一些天马行空但难以理解的API。吸取前人经验，同时在设计上保持克制，才能写出更好用的装饰器。

> 私有属性是“君子协定”

> 在Python里，所有的类属性和方法默认都是公开的，不过你可以通过添加双下划线前缀__的方式把它们标示为私有。

> 和普通方法相比，静态方法不需要访问实例的任何状态，是一种与状态无关的方法，因此静态方法其实可以改写成脱离于类的外部普通函数。

> 使用@property装饰器，你可以把上面的get_basename()方法变成一个虚拟属性，然后像使用普通属性一样使用它

> @property是个非常有用的装饰器，它让我们可以基于方法定义类属性，精确地控制属性的读取、赋值和删除行为，灵活地实现动态属性等功能。

> 在超过90%的情况下，你能找到的合理的Python代码就如上所示：没有任何类型检查，想做什么就直接做。你肯定想问，假如调用方提供的fp参数不是文件对象怎么办？答案是：不怎么办，直接报错就好。示例如下。

> 总结一下，抽象类通过__subclasshook__钩子和.register()方法，实现了一种比继承更灵活、更松散的子类化机制，并以此改变了isinstance()的行为。

> super()使用的其实不是当前类的父类，而是它在MRO链条里的上一个类。

> 大多数情况下，你需要的并不是多重继承，而也许只是一个更准确的抽象模型，在该模型下，最普通的继承关系就能完美解决问题。

> 元类控制着类的创建行为，就像普通类控制着实例的创建行为一样。

> 但继承是一种类与类之间紧密的耦合关系。让子类继承父类，虽然看上去毫无成本地获取了父类的全部能力，但同时也意味着，从此以后父类的所有改动都可能影响子类。继承关系越复杂，这种影响就越容易超出人们的控制范围。

> 针对事物的行为建模，而不是对事物本身建模。

> 在多数情况下，基于事物的行为来建模，可以孵化出更好、更灵活的模型设计。

> 即使B和A是同类，那它们真的需要用继承来表明类型关系吗？要知道，Python是鸭子类型的，你不用继承也能实现多态。

> 多态（polymorphism）是面向对象编程的基本概念之一。它表示同一个方法调用，在运行时会因为对象类型的不同，产生不同效果。

> SOLID单词里的5个字母，分别代表5条设计原则。· S：single responsibility principle（单一职责原则，SRP）。· O：open-closed principle（开放–关闭原则，OCP）。· L：Liskov substitution principle（里式替换原则，LSP）。· I：interface segregation principle（接口隔离原则，ISP）。· D：dependency inversion principle（依赖倒置原则，DIP）。

> 单一职责是面向对象领域的设计原则，通常用来形容类。而在Python中，单一职责的适用范围不限于类——通过定义函数，我们同样能让上面的代码符合单一职责原则。

> 这世间唯一不变的，只有变化本身。

> 虽然继承功能强大，但它并非通往OCP的唯一途径。除了继承外，我们还可以采用另一种思路：组合（composition）。更具体地说，使用基于组合思想的依赖注入（dependency injection）技术

> 但是，如果少了PostFilter抽象类，当编写HNTopPostsSpider类的__init__方式时，我就无法给post_filter增加类型注解了——post_filter: Optional[这里写什么？]，因为我根本找不到一个具体的类型。

> 但数据驱动也有一个缺点：它的可定制性不如其他两种方式。举个例子，假如我想以“链接是否以某个字符串结尾”来进行过滤，现在的数据驱动代码就做不到。影响每种方案可定制性的根本原因在于，各方案所处的抽象级别不一样。比如，在依赖注入方案下，我选择抽象的内容是“条目过滤行为”；而在数据驱动方案下，抽象内容则是“条目过滤行为的有效站点地址”。很明显，后者的抽象级别更低，关注的内容更具体，所以灵活性不如前者。

> LSP认为，所有子类（派生类）对象应该可以任意替代父类（基类）对象使用，且不会破坏程序原本的功能。

> 在Python 3.8版本里，类型注解typing模块增加了一个名为“协议”（Protocol）的类型。从各种意义上来说，Protocol都比抽象类更接近传统的“接口”。

> 更丰富的接口协议，意味着更高的实现成本，也更容易给实现方带来麻烦。

> 描述符（descriptor）是Python对象模型里的一种特殊协议，它主要和4个魔法方法有关： get、set、delete__和__set_name。从定义上来说，除了最后一个方法__set_name__以外，任何一个实现了__get__、__set__或__delete__的类，都可以称为描述符类，它的实例则叫作描述符对象。

> 现在你应该明白了，一个对象的__del__方法，并非在使用del语句时被触发，而是在它被作为垃圾回收时触发。del语句无法直接回收任何东西，它只是简单地删掉了指向当前对象的一个引用（变量名）而已

> TDD（test-driven development，测试驱动开发）是由Kent Beck提出的一种软件开发方式。在TDD工作流下，要对软件做一个改动，你不会直接修改代码，而会先写出这个改动所需要的测试用例。TDD的工作流大致如下：（1）写测试用例（哪怕测试用例引用的模块根本不存在）；（2）执行测试用例，让其失败；（3）编写最简单的代码（此时只关心实现功能，不关心代码整洁度）；（4）执行测试用例，让测试通过；（5）重构代码，删除重复内容，让代码变得更整洁；（6）执行测试用例，验证重构；（7）重复整个过程。

> 你应该了解这些理论，越多越好，但是千万不要陷入教条主义。因为在现实世界里，每个人参与的项目千差万别，别人的理论不一定适用于你，如果盲目遵从，反而会给自己增加麻烦。

## 笔记

> 编程最初带给我们的快乐已悄然远去，写代码这件事现在变得有些痛苦。更有甚者，一想到项目里的烂代码，每天起床后最想干的一件事就是辞职。

💭 哈哈哈真实

> 如果某个函数的圈复杂度超过10，就代表它已经太复杂了，代码编写者应该想办法简化。优化写法或者拆分成子函数都是不错的选择。

💭 代码复杂度

> 所以简单来说，抽象就是一种选择特征、简化认知的手段。接下来，我们看看抽象与软件开发的关系。

💭 抽离出来普遍具有的特征现象

> 当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子，那么这只鸟就可以称为鸭子。 ——来自“鸭子类型”的维基百科词条

💭 鸭子类型

> 所有与数据模型有关的方法，基本都以双下划线开头和结尾，它们通常被称为魔法方法（magic method）。

💭 魔法方法

> 所以，写单元测试不是浪费时间，也不会降低开发效率。你在单元测试上花费的那点儿时间，会在未来的日子里为项目的所有参与者节约不计其数的时间。

💭 重要的单元测试

> 不要掉进完美主义的陷阱。

💭 不要让完美主义成为你的绊脚石！

## 书评

## 点评