Book Review of Effective Python: 59 Specific Ways to Write Better Python

2019/08/26

Tags: review python

书名是《Effective Python:编写高质量 Python 代码的 59 个方法》这本书买了大概 2 年,一直没看完,前几天找了一些时间看完了。

这书是针对中级 Python 程序员的,里面没有对于语法什么的讲解,也没有对 c 代码实现的讲解,针对中级程序员。

用 Python 方式来思考

  1. Python 3 里面包含两种表示字符串的类型:bytes 和 str。前者是二进制形式,后者是 Unicode 编码(比如使用 utf8)之后的形式。想要把 bytes 表示为 str 形式,需要使用 decode ,反之是 encode

  2. 切割列表的时候,start 和 end 可以越界,这样可以用来限制列表最大长度。 a=[0,1,2]; a[:10] 可以限制最大是 10 个。

  3. 可以使用列表推导式 [x*2 for x in a] 类似这样的形式,代替 mapfilter

  4. 字典推导 {k: v for k, v in a.items()}

  5. 把列表推导式的表达式放到 () 里面就会产生生成器。

  6. enumerate 可以把各种迭代器包装成生成器。迭代的时候可以同时返回列表索引。

  7. zip 函数可以同时遍历多个迭代器,这样可以相对优雅一点同时遍历一些平行列表。

  8. try/except/else/finally 里面,try 如果出现异常会执行 except 里面的内容,如果没有异常会执行 else 的内容,finally 总是会执行。合理使用可以把逻辑处理的更好,比如 try 里面放需要处理异常的代码,else 里面放如果出现异常就往上传播的代码。

函数

  1. 函数在遇到特殊情况的时候,应该抛出异常,而不要返回 None。

  2. 闭包(closure)是一种定义在某个作用域中的函数,这种函数引用了那个作用域里面的变量。闭包和普通/匿名函数的区别?

  3. 闭包里面可以使用 nonlocal 语句来声明想要修改闭包外的作用域中的变量,类似 global

  4. 考虑使用生成器来改写直接返回列表的函数,不过要注意生成器只能迭代一次。

  5. for x in foo 这样的语句,实际会调用 iter(foo) ,内置的 iter 会调用 foo.__iter__ 这个方法,这个方法必须返回迭代器对象,那个迭代器会实现 __next__ 方法。for 循环会在迭代器上面反复调用 next 方法,直到耗尽产生 StopIteration 异常。

  6. 函数定义上面,使用 *args 这样的形式可以把所有的位置参数都吞掉。类似的 **kwargs 可以把所有命名/关键字参数都吞掉。

  7. 函数参数越加越多的时候,增加的时候可以使用关键字参数,要不光看一个函数调用也不知道各个参数都是干嘛的。尤其以后增加减少参数的时候很不方便。

  8. 函数参数的默认值只会在程序加载模块并读到本函数定义的时候评估一次,对于 {}, [] 这样的值,会总是使用同一个引用。

  9. 可是使用 * 做参数表示位置参数的终结。例如 func(a, b, *, c=1, d=2) 这样的函数,调用的时候 func(1,2,3) 这样会提示需要 2 个位置参数,给了 3 个这样的异常。

类与继承

  1. Collections 模块里面提供了 namedtuple 可以给每个数据的属性定义名字,比单纯的通过数字索引的列表和元组好。

  2. 保存内部状态的字典如果变得比较复杂,那就应该把这些代码拆解成辅助类。

  3. 可以使用 Collections 模块的 defaultdict 给字典设置默认值。比如我们想要统计一下字母出现的次数 count={} ,那在循环的时候想要给一个字典添加新的 count['a']+=1 ,这个时候必须要先判断下这个 count['a'] 是不是已经有了,而不能直接增加。使用 defaultdict 可以这样 count=defaultdict(func, {}) 这样。 func 可以是一个用来产生默认值的函数,比如这里可是用 int

  4. 我们可以在一个类的 @classmethod 方法里面初始化一个类,使用 cls(args) 来实例化这个对象。

  5. 总是使用内置的 super 来初始化父类。 object 这个父类实际上也应该需要初始化。 super(__class__, self).__init__(*args, **kwargs) or super().__init__(*args, **kwargs)

  6. 能使用 mix-in 组件实现的效果,就不要用多重继承。把功能实现为可拔插的 mix-in 组件,然后令相关的类继承自己需要的那些组件,即可定制该类实例所应具备的行为。

  7. 多用 public 属性。单个下划线开头的字段,应该视为 protected 字段,双下划线开头的字段是 private 字段。多用 protected 字段,并在文档里面说清这些字段的合理用法,不要使用 private 属性来限制子类访问。只有当子类不受控制的时候,才考虑使用 private 属性来避免名称冲突。

  8. 编写自己的容器类型的时候,可以继承 collections.abc 模块中的基类。

元类及属性

  1. 可以使用 @property 来定义 getter, @xxxx.setter 来定义 setter。

  2. 程序每次访问对象的属性的时候 __getattribute__ 都会被调用,即使属性字典里面已经有了这个属性。 __getattr__ 只在属性字典里面没有的时候才调用。

  3. 如果要在 __getattribute____setattr__ 方法里面访问实例属性,应该通过 super() 来做,避免无限递归。

  4. 元类定义 def __new__(meta, name, bases, clas_dict) 可以获取类的几个属性:meta 元类本身,name 类的名字,bases 类继承的所有父类,class_dict 是类的属性字典。

  5. python 把子类的整个 class 语句体处理完毕之后,就会调用元类的 __new__ 方法。这个阶段还没有执行任何的子类的代码,也没有创建实例,这个时候可以对子类做一些修改,或者记录工作。

并发并行

  1. 可以给 subprocess 模块的 communicate 方法传入 timetout 参数,避免程序长时间未相应。

  2. Python 采用 GIL(global interpreter lock) 机制保证同一时刻只有一条线程可以向前执行。GIL 实际就是一把互斥锁(mutual-exclusion lock, mutex),用以防止 CPython 受到占先式多线程切换(preemptive multithreading) 操作的干扰。GIL 会使得 Python 代码无法并行,但是对于系统调用却不影响,执行系统调用的时候会释放 GIL,直到执行完毕。这样可以在执行 I/O 操作的同时,执行一些运算操作。

  3. GIL 并不会处理程序逻辑上面的数据安全,如果需要避免线程竞争导致的数据破坏,那需要自己加锁,可是使用 Lock 类。

  4. Queue 类具备阻塞式的队列操作,能够指定缓冲区大小,还支持 join 方法,可以方便的实现队列。

  5. 线程启动时的开销比较大,可以使用协程(coroutine) 。使用协程可以让程序看似并行执行,而又避免了多线程需要处理锁的麻烦。(描述可能有误)

  6. 对于密集计算型程序,可以使用多进程来跑多个 CPU。使用 ProcessPoolExecutor 替代 ThreadPoolExecutor ,multiprocessing 模块会使用 pickle 把数据库序列化,通过 locl socket 发送到子进程。还可以使用 multiprocessing 提供的高级机制,例如共享内存(shared memory),跨进程锁(cross-process lock),队列(queue),代理(proxy)等实现自己的逻辑。

内置模块

  1. 使用 functools.wraps 装饰自己写的装饰器。会保留 __doc__, __name__, __module__ 等属性。

  2. 内置的 contextlib 模块实现了 contextmanager 的装饰器,可是使用他来实现支持 with 语句的函数。 try: yield sth; finally: expr; sth 可以赋值给 with xxx as yyy 的 yyy。

  3. 使用 copyreg.pickle 可以定义 pickle 执行反序列化的时候执行的函数。

  4. 使用内置的算法和数据结构:collections 模块的 deque 类,实现双向队列。使用 OrderedDict 实现有序字典。使用 defaultdict 实现带有默认值的字典。使用 heapq 把普通列表变成堆结构。使用 bisect 模块做二分查找。 itertools 模块提供了很多迭代器相关的函数,比如可以在不迭代的情况下,切割迭代器之类(实际我理解就是包了一个其他函数返回新的迭代器,不过总是比自己包一个省点事情)。

  5. 重视精度的情况下,应该使用 decimal

协作开发

  1. 应该为模块,类,函数各自编写文档,可是使用内置的 doctest 模块运行 docstring 中的范例代码。

  2. 用包来安排模块,并提供稳固的 API。把对外接可见的名称放到一个 __all__ 属性里面,然后把一些比如 utils.py 里面的函数也放到包的 __init__.py 里面统一对外提供,这样以后即使包内部目录结构变化什么的也不会对对外的 API 发生影响。

  3. 为自己编写的模块定义根异常,让这个模块抛出的其他异常都继承这个。有几个好处。

    1. 调用者能清楚的知道调用代码是否正确,因为如果有错误会抛出异常。

    2. 调用者如果遇到了根异常之外的异常,那么可能是因为 API 作者的程序 bug 导致没有处理好这些异常,如果 API 作者处理好了这些异常,那么就不应该抛出本模块之外的异常。

    3. 有了根异常,可以在未来基于根异常来添加更加详细的异常,而对于那些之前捕获根异常的用户来说没有影响,但是新的用户可以更加精细的处理异常。

部署

  1. 考虑在模块级别的代码里面适配不同的部署环境。

  2. 通过 repr 字符串来输出调试信息。

  3. 使用 pdb 调试,一些 pdb 命令需要掌握:

    1. bt: 打印当前调试点的调用栈。

    2. up: 把调试范围沿着调用栈往上一层。

    3. down: 把调试范围沿着调用栈往下一层。

    4. step: 执行当前这条代码后,移动到下一条可执行语句暂停。如果是函数调用,会进入那个函数。

    5. next: 执行当前这行代码后,移动到下一条可执行语句暂停。如果是函数调用,会调用那个函数。

    6. return: 继续运行程序,直到当前函数执行完毕。

    7. continue: 继续运行程序,直到下一个断点。

  4. 使用 profile 和 cProfile 来分析程序性能,尽量使用 cProfile。

  5. 使用 tracemalloc 来掌握内存的使用和泄漏情况。

其他整理

Python 类的特殊属性:

  1. __doc__: 文档字符串。

  2. __name__: 对象名字。

  3. __module__: 对象模块信息。

  4. __class__: 类名称。

  5. __dict__: 返回实例的属性字典。

  6. __bases__: 父类列表。

  7. __qualname__: 带类定义'路径'的名称。

  8. __slots__:

Python 类的特殊方法:

  1. __iter__: 返回一个生成器,可以允许一个对象的实例支持迭代。

  2. __call__: 直接调用一个对象的实例的时候会执行这个。

  3. __len__: 支持 len(obj) 函数。

  4. __getitem__: 支持 bar[0] 这样的方式索引。

  5. __set__: 描述符协议,支持赋值语句。

  6. __get__: 描述符协议,支持读取。

  7. __getattr__: 读取某个对象的属性的时候,属性字典里面没有的时候调用。 setattr 可以给对象增加属性。 hasattr 可以判断是否有这个属性。

  8. __setattr__: 对属性赋值的时候会调用,无论是直接赋值还是通过 setattr

  9. __new__: 初始化一个对象的时候,在 __init__ 之前执行。

  10. __init__: 初始化一个对象的时候,在 __new__ 之后执行。

  11. __enter__:

  12. __exit__:

  13. __repr__: 返回对象的可打印字符串,对应格式化字符串里面的 %r

  14. __str__: 返回对象的字符串表示,对应格式化字符串里面的 %s

  15. __contains__:

  16. __reversed__:

发现全列出来太天真了,太多了,这里文档里面有列一些,不过也不是全部,就不多写了。

Comments