【读书笔记】Effective Python
目前工作Python依然是主要语言,业务实际上线也是基于Python来完成,所以编写出高质量的Python代码就很重要。
快速的刷完这本Effective Python,确实学到了很多新的用法,常阅读,常实践,常总结。
1.培养Pythonic思维
- 模块级别的变量,所有字母都大写,各个单词之间用下划线连接;例如:
DATA_DIR
- 使用插值格式字符串(f-string)来格式化字符串。
f{var:.2f}
通过:
可以后面跟格式化的具体要求 - 不直接通过下标来访问元组,而是赋值到多个变量中
- 遍历变量用
enumerate
而不是range(len(.))
的形式,enumerate
的第二个参数可以指定开始序号 zip
进行多个变量的迭代的时候,是按照最短的进行;如果需要最长的改用itertools.zip_longest()
- 通过赋值表达式(:=)可以减少代码,例如在
if else
语句中:if count:=get_count()
2.列表与字典
- 列表切片的时候,下标可以越界;例如获取前面10个
a[:10]
,这时候a
的长度不一定大于等于10 - 列表的切片是一次拷贝操作,修改切片后新列表的值,不影响原先的列表
- 如果针对多个指标进行排序,并且拆分成多个sort,最重要的指标写在最后面
- Python3.6开始,遍历dict时候key的顺序,就是key插入的顺序
- 优先考虑使用defaultdict
3.函数
- Python中函数可以以元组的形式返回多个变量,但是不要返回超过3个变量,如果超过应该通过
namedtuple
构建一个类进行返回 - 函数的参数中,多个值可以通过
*args
表示,**kwargs
表示可选参数的值 - 通过
nonlocal
可以让修改外围作用域的变量 - 函数参数的默认值如果是可变的,应该设为
None
,并通过docstring表明这个参数到底是什么。例如对于列表,函数开头判断如果是None
,再赋一个空列表 - 函数参数列表中
/
前面的参数只能通过位置传递,*
后面的参数只能通过关键词传递,例如division(num1,num2,/,*,ignore_zero_division=False)
。处于这两个符号中间的参数既可以通过位置传递也可以通过关键词传递 - Python可以通过装饰器实现在某个函数执行前后者执行后执行一些其他的逻辑。推荐wrapper函数上加上
functool.wraps(func)
避免改变原始函数的函数属性(name,docstring等)
4.推导与生成
- 列表推导式中最多只能写两个子表达式(两个if,两个for,或者1个if和1个for),否则应该拆分成正常的循环
- 迭代器只能迭代一次,如果需要多次迭代,则需要封装成一个对象并实现
__iter__()
方法,每次调用返回一个新的迭代器 - 大数据量场景,可以用生成器改写列表推导式 例如
it = (len(x) for x in open('large.txt'))
itertools
基础库包含很多迭代相关的基础函数,比如chain,repeat,cycle,product,permutation,combinations
5.类与接口
- 不要在字典中嵌套字典或者长的元组,可以考虑采用
namedtuple
这种轻型的类来实现。例如:User = namedtuple('User', ['name', 'sex', 'age'])
- 通过实现一个类的
__call__
方法,可以让这个类的实例像一个函数一样使用,可以很方便的实现一个有状态的闭包 - Python中类只有一个构造方法,如果想在父类中统一的方式构造子类的实例,可以定义
@classmethod
方法,并用cls(...)
的形式构造具体实例 - Python的子类中通过
super().__init__()
可以完成超类的初始化,一般不需要手动传递参数指定初始化的顺序 - Python中尽量不使用双下划线
__
的私有属性,而是单下划线_
的受保护的数量以及docstring
来进行提醒 - 自定义的容器类,例如
List
,可从collections.abc
中找到类似的容器进行继承,并实现对应方法
6.元类与属性
- 如果在访问/设置属性的同时还需要进行额外的校验/操作,可以通过
@property
方法来实现,而非getter/setter - 通过
__getattr__
和__setattr__
可以实现惰性加载保存对象,__getattribute__
则是每次访问属性都会调用 - 通过
__init_subclass__
可以实现子类在定义阶段就被拦截,实现对子类的类属性的校验,类的注册等操作 - 类修饰器本质也是一个函数,从而重建/调整被修饰的类,例如给类的每个方法/属性都施加一套逻辑
7.并发与并行
- Python中并发可以通过多线程/协程来实现,而并行则需要通过多进程/C语言扩展等方式来实现,两者的本质区别在于能不能提速
- Python中实现子进程最佳的方法是
subprocess
, 并且可以实现Linux中的管道操作 - CPU密集型的任务不应该使用Python的多线程,否则会比单线程还要慢,因为需要抢占CPU
- 通过
threading.Lock
在多线程场景下加锁,保护共享的数据 - 通过
Queue
可以在协调多个进程之间的工作节奏,实现生产者消费者模式 - 使用协程来实现更高效的I/O,协程相比线程开销更小,区别是每次遇到
await
就会暂停一次 - 通过
concurrent.futures
的ThreadPoolExecutor
和ProcessPoolExecutor
可以快速实现多线程和多进程
8.稳定与性能
try/except/else/finally
是完成的异常捕获结构,如果希望在try
正确执行之后再执行一段逻辑,可以放到else
里面- 可以将重复的
try/except
结构用@contextmanager
封装,后续就可以用with ... as ..
的方式调用 - 需要精确计算的场合,使用
decimal
来表示数值 - 通过
bisect
模块可以快速实现有序列表中的二分查找,相比list
的index
要快很多 - 通过
heapq
可实现一个高性能的优先队列 - Python内置的
memoryview
实现了无需进行拷贝的接口,结合bytearray
可以实现接收到的数据覆盖底层缓冲里面的任意区段
9.测试与调试
- 通过
repr
可以获得一个表示该值能打印的字符串,可以区分print(repe(5))和print(repr('5'))
, 在f-string中可添加!r
后缀 - 对于一个自定义的类,可以打印
__dict__
进行调试观察 - 通过
TestCase
子类来进行单元测试,子类中每个以test
开头的方法都是一个测试用例 - 通过
setUp
和tearDown
可以实现每个测试用例在开始前和后都执行一次,保证测试环境;setUpModule
和tearDownModule
则是模块级别的,只会执行一次 - 采用mock来模拟测试函数依赖的接口
10.协作与开发
- 给模块编写文档:第一行是一句话表明这个模块的用途,接下来一段说明这个模块所需要知道的一切,最后这个模块重要的类和函数需要强调
- 给类编写文档:第一行是一句话表明这个类的用途,接下来说明本类的行为,最后是这个类重要的属性和方法
- 给函数编写文档:第一行是一句话表明这个函数的用途,接下来一段说明这个函数的具体行为,再分别用两段描述这个函数的参数和返回值,如果有异常也需要说明
- 通过
python -m pydoc -p <PORT>
可以搭建一个Python文档查看的服务,从web界面看文档,包括自己写的模块 - 对用重要的接口,复杂的函数可以通过typing指定函数参数的数据类型,实现静态分析
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!