Python工匠 | 全书要点汇总(1):https://developer.aliyun.com/article/1407215
7 函数
1、函数参数与返回相关基础知识
- 不要使用可变类型作为参数默认值,用
None
来代替 - 使用标记对象,可以严格区分函数调用时是否提供了某个参数
- 定义仅限关键字参数,可以强制要求调用方提供参数名,提升可读性
- 函数应该拥有稳定的返回类型,不要返回多类型
- 使用返回
None
的情况——操作类函数、查询类函数表示意料之中的缺失值 - 在执行失败时,相比返回
None
,抛出异常更为合适如果提前返回结果可以提升可读性,就提前返回,不必追求“单一出口”
2、代码可维护性技巧
- 不要编写太长的函数,但长度没有标准,65行算是一个危险信号
- 圈复杂度是评估函数复杂程度的常用指标,圈复杂度超过10的函数需要重构
- 抽象与分层思想可以帮我们更好地构建与管理复杂的系统
- 同一个函数内的代码应该处在同一抽象级别
3、函数与状态
- 没有副作用的无状态纯函数易于理解,容易维护,但大多是时候“状态”不可避免
- 避免使用全局变量给函数增加状态
- 当函数状态较简单时,可以使用闭包技巧
- 当函数需要较为复杂的状态管理时,建议定义类来管理状态
4、语言机制对函数的影响
functools.partial()
可以用来快速构建偏函数functools.lru_cache()
可以用来给函数添加缓存- 比起
map
和filter
,列表推导式的可读性更强,更应该使用
lambda
函数只是一种语法糖,你可以使用operator
模块等方式来替代它- Python语言里的递归限制较多,可能的话,请尽量使用循环来替代
8 装饰器
1、基础与技巧
- 装饰器最常见的实现方式,是利用闭包原理通过多层嵌套函数实现
- 在实现装饰器时,请记得使用
wraps()
更新包装函数的元数据 wraps()
不光有保留元数据,还能保留包装函数的额外属性- 利用仅限关键字参数,可以很方便地实现可选参数的装饰器
2、使用类来实现装饰器
- 只要是可调用对象,都可以使用装饰器
- 实现了
__call__
方法的类实例可调用 - 基于类的装饰器分为两种:“函数替换”与“实例替换”
- “函数替换”装饰器与普通装饰器没什么区别,只是嵌套层数更少
- 通过类来实现“实例替换”装饰器,在管理状态和追加行为上有天然的优势
- 混合使用类和函数来实现装饰器,可以灵活满足各种场景
3、使用wrapt模块
- 使用wrapt模块可以方便地让装饰器同时兼容函数和类方法
- 使用wrapt模块可以帮你写出结构更扁平的装饰器代码
4、装饰器设计技巧
- 装饰器将包装调用提前到了函数被定义的位置,它的大部分优点也源于此
- 在编写装饰器时,请考虑你的设计是否能够很好地发挥装饰器的优势
- 在某些场景下,类装饰器可以替代元类,并且代码更简单
- 装饰器和装饰器模式截然不同,不要弄混它们
- 装饰器里应该只有一层浅浅的包装代码,要把核心逻辑放在其它函数与类中
9 面向对象编程
1、语言基础知识
- 类与实例的数据,都保存在一个名为
__dict__
的字典属性中 - 灵活利用
__dict__
属性,能帮你做到常规做法难以完成的一些事情 - 使用
@classmethod
可以定义类方法,类方法常用作工厂方法 - 类与实例的数据,都保存在一个名为
__dict__
的字典属性中 - 灵活利用
__dict__
属性,能帮你做到常规做法难以完成的一些事情 - 使用
@classmethod
可以定义类方法,类方法常用作工厂方法super()
函数获取的并不是当前类的父类,而是当前MRO链条里的下一个类 - Mixin是一种基于多重继承的有效编程模式,用好Mixin需要精心设计
- 元类的功能相当强大,但同时也相当复杂,除非开发一些框架类工具,否则你极少需要使用元类
- 通过定义
__init_subclass__
钩子方法,你可以在某个类被继承时执行自定义逻辑
3、鸭子类型与抽象类
- “鸭子类型”是Python语言最为鲜明的特点之一,在该风格下,一般不做任何严格的类型检查
- 虽然“鸭子类型”非常实用,但是它有两个明显的缺点——缺乏标准和过于隐式
- 抽象类提供了一种更灵活的子类化机制,我们可以通过定义抽象类来改变
isinstance()
的行为 - 通过
@abstractmethod
装饰器,你可以要求抽象类的子类必须实现某个方法
4、面向对象设计
- 继承提供了相当强大的代码复用机制,但同时也带来了非常紧密的耦合关系
- 错误使用继承容易导致代码失控
- 对事物的行为而不是事物本身建模,更容易孵化出好的面向对象设计
- 在创建继承关系时应当谨慎。用组合来替代继承有时是更好的做法
5、函数与面向对象的配合
- Python里的面向对象不必特别纯粹,加入用函数打一点儿配合,你可以设计出更好的代码
- 可以想
requests
模块一样,用函数为自己的面向对象模块实现一些更易用的API
- 在Python中,我们极少会应用真正的“单例模式”,大多数情况下,一个简单的模块级全局对象就够了
- 使用“预绑定方法模式”,你可以快速为普通实例包装出类似普通函数的API
6、代码编写细节
- Python的成员私有协议并不严格,如果你想标示某个属性为私有,使用单下划线前缀就够了
- 编写类时,类方法排序应该遵循某种特殊规则,把读者最关心的内容摆在最前面
- 多态是面向对象编程里的基本概念,同时也是最想大的思维工具之一
- 多态可能的介入时机:许多类似的条件分支判断、许多针对类型的
isinstance()
判断
10 面向对象设计原则(上)
1、SRP
- 一个类只应该有一种被修改的可能
- 编写更小的类通常更不容易违反SRP
- SRP同样适用于函数,你可以让函数和类协同工作
2、OCP
- 类应该对修改关闭,对扩展开放
- 通过分析需求,找到代码中易变的部分,是让类符合OCP的关键
- 使用子类继承的方式可以让类符合OCP
- 通过算法类与依赖注入,也可以让类符合OCP
- 将数据与逻辑分离,使用数据驱动的方式也是时间OCP的好办法
11 面向对象设计原则(下)
1、LSP
- LSP认为子类应该可以任意替代父类使用
- 子类不应该抛出符类不认识的异常
- 子类方法应该返回与父类一致的类型,或者返回父类返回值的子类型对象
- 子类的方法参数应该和父类方法完全一致,或者要求更为宽松(注意与上一点区别 )
- 某些类可能会存在隐式合约,违反这些合约也会导致LSP
2、DIP
- DIP认为高层模块和底层模块都应该依赖于抽象
- 编写单元测试有一个原则:测试行为,而不是测试实现
- 单元测试不宜使用太多
mock
,否则需要调整设计 - 依赖抽象的好处是,修改底层模块实现不会影响高层代码
- 在Python中,你可以用
abc
模块来定义抽象类 - 除
abc
外,你也可以用Protocol
等技术来完成依赖倒置
3、ISP
- ISP认为客户依赖的接口不应该包含任何它不需要的方法
- 设计接口就是设计抽象
- 写更小的类、更小的接口在大多是情况下是个好主意
12 数据模型与描述符
1、字符串相关协议
- 使用
__str__
方法,可以定义对象的字符串值(被str()
触发) - 使用
__repr__
方法,可以定义对象爱对调试友好的详细字符串值(被repr()
触发) - 如果对象只定义了
__repr__
方法,它同时会用于替代__str__
- 使用
__format__
方法,可以在对象被用于字符串模板渲染时,提供多种字符串值(被.format()
触发)
2、比较运算符重载
- 通过重载与比较运算符有关的6个魔法方法,你可以让对象支持
==
、>=
等比较运算
- 使用
functools.total_ordering
可以极大地减少重载比较运算符的工作量
3、描述符协议
- 使用描述符协议,你可以轻松实现可复用的属性对象
- 实现了
__get__
、__set__
、__delete__
其中任何一个方法的类都是描述符类要在描述符里保存实例级别的数据,你需要将其存放在instance.dict
里,而不是直接放在描述符对象上
- 使用
__set_name__
方法能让描述符对象知道自己被绑定了什么名字
4、数据类与自定义哈希运算
- 要让自定义类支持集合运算,你需要实现
__eq__
与__hash__
两个方法 - 如果两个对象相等,它们的哈希值也必须相等,否则会破坏哈希表的正确性
- 不同对象的哈希值可以一样,哈希冲突并不会破坏程序正确性,单会影响效率
- 使用
dataclasses
模块,你可以快速创建一个支持哈希操作的数据类 - 要让数据类支持哈希操作,你必须指定
frozen=True
参数将其声明为不可变类型 - 一个对象的哈希值必须在它的声明周期里保持不变
5、其他建议
- 虽然数据模型能帮我们写出更Pythonic的代码,单切勿过度推崇
__del__
方法不是在执行del
语句时被触发,而是在对象被作为垃圾回收时触发- 不要使用
__del__
来做任何“自动化”的资源回收工作
13 开发大型项目
一些项目开发工具介绍
- flake8:包含pycodestyle模块(检查代码风格规范)和pyflakes模块(检查代码的正确性)
- isort:格式化
import
语句
- black:严格的代码风格工具,使用
black {filename}
命令整理代码 - mypy:静态类型检查工具
单元测试工具:TDD(测试驱动开发)是一种行之有效的工作方式
- unittest:标准库模块。
- pytest:一个开源的第三方单元测试框架,更符合python风格。
用@pytest.mark.parametrize
可以编写参数化测试。