Effective Python 读书笔记

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

文章目录

前言

不错的Python进阶书。写这个笔记的目的是把书中提到的编写高质量Python的59个方法的要点记录下来便于今后的工作中查阅

第1章用Pythonic方式来思考

1. 用Pythonic方式来思考

python之禅 —— import this

2. 遵循PEP8风格

PEP8 —— Python Enhancement Proposal #8

空白

  • 使用空格而不是tab表示缩进
  • 和语法相关每一层缩进用4个空格
  • 每行字符数不超过79
  • 占据多行表达式除首行外其余各行应在通常缩进级别上加4空格
  • 函数与类之间两个空行隔开
  • 同一个类中各方法一个空行隔开
  • 使用下标获取元素不要在两旁加空格
  • 为变量赋值赋值符号左侧和右侧各自写一个空格不要在两旁添加空格

命名

  • 函数、变量、属性用小写各单词以下划线连接
  • 受保护实例属性以单下划线开头
  • 私有实例属性以两个下划线开头
  • 类以异常应每个单词以大写形式命名如CapitalizedWord
  • 模块级别常量应全部采用大写各单词以下划线相连如ALL_CAPS
  • 类中实例方法应该把首个参数命名为self以表示对象自身
  • 类方法首个参数应该命名为cls以表示类自身

表达式和语句

  • 采用内联形式否定词而不是把否定词放在整个表达式前面例如写if a is not b 而不是 if not a is b
  • 不要通过测长度方法判断list是否为空应采用if not list空值会自动评估为False
  • import语句总应放在文件开头
  • 引入模块时总应使用绝对名称不应根据当前模块路径使用相对名称例如引入bar包中的foo模块应完整写出from bar import foo不应该简写import foo
  • 文件中import语句应按顺序划三部分分别表示标准库模块第三方模块自用模块各部分中的import语句应按模块的字母顺序排列

3. 了解bytes, str, unicode区别

  • Python3中bytes是一个包含8位置的序列str是一种包含unicode字符序列

4. 用辅助函数取代复杂表达式

开发者容易过度运用Python语法特性写出特别复杂难以理解的单行表达式。这种情况需要把复杂表达式移到辅助函数

5. 了解切割序列的方法

a = ['a','b','c','d','e','f','g','h']
>>> a[:4]
['a', 'b', 'c', 'd']
>>> a[5:]
['f', 'g', 'h']
>>> a[-2:]
['g', 'h']
>>> a[2:-3]
['c', 'd', 'e']
>>> a[:]
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
a[2:6] = ['c', 'a', 'o']
>>> ['a', 'b', 'c', 'a', 'o', 'g', 'h']

6. 单次切片操作内不要同时指定start, end, stride

除了基本切片操作Python还提供了somelist[startstride]形式写法以实现步进式切割就是从每n个元素里取1个出来

既有start, end又有stride的切割操作可能令人费解考虑拆解两条语句其中一条做范围切割另一条做步进切割如下

b = a[::2]    # ['a', 'c', 'e', 'g']
c = b[1:-1]   # ['c', 'e']

7. 用列表推导取代map, filter

a = [1, 2, 3]
squares = [ x**2 for x in a ]
>>> print(squares)
[1, 4, 9]
或者map, filter
squares = map(lambda x: x ** 2, a)
>>> list(squares)
[1, 4, 9]

8. 不要使用含有两个以上表达式的列表推导

my_lists = [ 
    [[1,2,3], [4,5,6]],
]
flat = [x for sublist1 in my_lists
		  for sublist2 in sublist1
		  for x in sublist2]

# 可以看出列表推导写法不够简洁改用普通循环语句实现相同效果
flat = []
for sublist1 in my_lists:
	for sublist2 in sublist1:
		flat.extend(sublist2)

9. 用生成器表达式改写数据量较大的列表推导

it = (len(x) for x in open('file.txt')
print(it)
>>>
generator obj at XXX
print(next(it))

10. 尽量用enumerate取代range

flavor_list = ['vanilla', 'chocolate', 'pecan', 'strawberry']
for i, flavor in enumerate(flavor_list):
	print('%d: %s' % (i + 1, flavor))

还可以直接指定enumerate函数开始计数时所用值这样能把代码写的更短

for i, flavor in enumerate(flavor_list, 1):
	print('%d: %s' % (i, flavor))

11. 用zip函数同时遍历两个迭代器

使用range或enumerate方法的代码不够简洁

names = ['Cecilia', 'Lise', 'Marie']
letters = [ len(s) for s in names]

longest_name = None
max_letters = 0
for i, name in enumerate(names):
    if letters[i] > max_letters:
        max_letters = letters[i]
        longest_name = name

print('longest_name: ', longest_name)

改用内置zip方法使代码更清晰

names = ['Cecilia', 'Lise', 'Marie']
letters = [ len(s) for s in names]

longest_name = None
max_letters = 0

for name, count in zip(names, letters):
    if count > max_letters:
        longest_name = name
        max_letters = count

print('longest_name: ', longest_name)

注意点

  • Python2中zip不是生成器而是会把开发者提供那些迭代器都平行遍历一次可能占用大量内存导致程序崩溃
  • 如果输入迭代器长度不同zip会提前终止将导致意外结果

12. 不要在for和while循环后写else块

for i in range(3):
	print('Loop %d' % i)
else:
	print('Else block!')
  • else块会在整个循环执行完之后立刻运行
for i in range(3):
    print('Loop %d' % i)
else:
    print('Else block!')

$ python test.py
Loop 0
Loop 1
Loop 2
Else block!
  • 循环里用break提前跳出会导致else块不执行
for i in range(3):
	print('Loop %d' % i)
	if i == 1:
		break
else:
	print('Else block!')
>>>
Loop 0
Loop 1
  • 如果for循环遍历序列是空的就会立刻执行else块
for x in []:
	print('Never runs')
else:
	print('For Else block!')
>>>
For Else block!

要点不要在循环后使用else块这种写法即不直观又容易让人误解

13. 合理利用try/except/else/finally结构中每个代码块

  • 无论try是否异常都可利用try/finally复合语句中finally块执行清理工作
  • else块可用来缩减try代码量并把没有发生异常所要执行的语句与try/except分开
  • 顺利运行try块后若想使某些操作能在finaly块的清理代码前执行可将这些操作写到else
UNDEFINED = object()
def divide_json(path):
	handle = open(path, 'r+')
	try:
		data = handle.read()
		op = json.loads(data)
		value = (op['numerator']/op['denominator'])
	except ZeroDivisionError es e:
		return UNDEFINED
    else:
	    op['result'] = value
	    result = json.dumps(op)
	    handle.seek(0)
	    handle.write(result)
	    return value
    finally:
	    handle.close()

第2章 函数

14. 尽量用异常来表示特殊情况而不返回Nones

例令函数返回None可能会使调用该函数人犯错

def divide(a, b):
	try:
		return a / b
	except ZeroDivisionError:
		return None

x,y = 0, 5
result = divide(x, y)
if not result:
	print('Invaid inputs') # This is WRONG !

改进方法一 把返回值拆两部分例

def divide(a, b):
	try:
		return True, a / b
	except ZeroDivisionError:
		return False, None

改进方法二给上一级抛异常

def divide(a, b):
	try:
		return a / b:
	except ZeroDivisionError as e:
		raise ValueError('Invalid inputs') from e

15. 了解如何在闭包里使用外围作用域中的变量

使用nonlocal获取闭包内的数据

def sort_priority3(numbers, group):
	found = False
	def helper(x):
		nonlocal found
		if x in group:
			found = True
			return (0, x)
		return (1, x)
	numbers.sort(key=helper)
	return found

16. 用生成器改写直接返回列表函数

这种写法的问题是返回前要把所有结果放在列表里如果输入量非常大程序可能耗尽内存崩溃

def index_words(text):
	result = []
	if text:
		result.append(0)
	for index, letter in enumerate(text):
		if letter == ' ':
			result.append(index + 1)
	return result

address = 'Four score and seven years ago...'
result = index_words(address)
print(result[:3])
>>>
[0, 5, 11]

用生成器改写会更好生成器用yield表达式函数调用生成器函数时不会真的运行而是返回迭代器

def index_word_iter(text):
	if text:
		yield 0
	for index, letter in enumerate(text):
		if letter == ' ':
			yield index + 1

address = 'Four score and seven years ago...'
result = list(index_words_iter(address))
[0, 5, 11, 15, 21, 27]

17. 在参数上迭代时多加小心

函数在输入参数上多次迭代时要当心如果参数是迭代器可能导致奇怪的行为并错失某些值

18. 用数量可变位置参数减少视觉杂讯 (visual noise)

def log(message, *values):
	if not values:
		print(message)
	else:
		values_str = ', '.join(str(x) for x in values)
		print('%s: %s' % (message, values_str))

log('My numbers are', 1, 2)
log('Hi there')

要点

  • def语句中使用*args, 即可令函数接收数量可变的位置参数
  • 在已经接收*args参数的函数上继续添加位置参数可能产生难以排查的BUG

19. 用关键字参数表达可选的行为

def remainder(number, divisor):
	return number % divisor

>>> 下面这些调用都是等效的
remainder(20, 7)
remainder(20, divisor=7)
remainder(number=20, divisor=7)
remainder(ivisor=7, number=20)

灵活使用关键字参数提供如下好处

  • 更容易理解含义
  • 在函数定义中提供默认值使代码简洁

20. 用None和文档字符串描述含有动态默认值的参数

参数默认值只会在程序加载模块并读到本函数的定义时评估一次对于{}或[]等动态的值这可能会导致奇怪的行为

def decode(data, default={}):
	try:
		return json.loads(data)
	except ValueError:
		return default

foo = decode('bad data')
foo['stuff'] = 5
bar = decode('also bad')
bar['meep'] = 1
>>>
Foo: {'stuff': 5, 'meep': 1}
Bar: {'stuff': 5, 'meep': 1} # Wrong 

解决方法是把关键字参数默认设置为None并在文档字符串中描述它的行为

def decode(data, default=None):
	""" Load JSON data from a string

	Args:
		data: JSON data to decode
		default: Value to return if decoding fails, Defaults to an empty dictionary
	"""
    if default is None:
        default = {}
    try:
        return json.loads(data)
    except ValueError:
        return default

21. 用只能以关键字形式指定的参数来确保代码明晰

第3章 类与继承

22. 尽量用辅助类维护程序状态而不要用字典和元组

  • 不要使用包含其他字典的字典也不要使用过长的元组
  • 如果容器中包含简单而又不可变的数据可先使用namedtuple表示后有需要再修改为类
  • 保存内部状态的字典如果比较复杂应该把代码拆解为辅助类

23. 简单的接口应该接受函数而不是类的实例

Python有许多内置API允许调用者传入函数以定制其行为例

>>> names = ['Socrates', 'Archimedes', 'Plato', 'Aristotle']
>>> names.sort(key=lambda x: len(x))
>>> print(names)
['Plato', 'Socrates', 'Aristotle', 'Archimedes']

其他编程语言可能用抽象类定义挂钩然而在Python中很多挂钩只是无状态函数这些函数有明确参数和返回值。用函数做挂钩比定义一个类要简单

24. 以@classmethod形式的多态去通用地构建对象

通过@classmethod机制可以用一种与构造器相仿的方式构造类的对象

25. 用super初始化父类

  • 直接调用父类的__init__方法在多重继承影响下可能产生无法预知行为
  • 总是应该使用内置的super函数初始化父类

26. 只在使用Mix-in组件制作工具类时进行多重继承

27. 多用public属性少用private属性

  • Python编译器无法严格保证private字段私密性用经典的格言来说“we are all consenting adults here”大家都认为开放比封闭更好
  • 只有子类不受自己控制时才可以考虑用private属性来避免名称冲突

28. 继承collections.abc以实现自定义的容器类型

编写自制容器类型可以从内置collections.abc模块的抽象基类中继承。从这样的基类继承了子类后如果忘记实现某个方法那么collections.abc模块会指出这个错误

from collections.abc import Sequence
class BadType(Sequence):
	pass
foo = BadType()
>>>
TypeError: Can't instantiate abstract class BadType with ...

第4章 元素与属性

29. 用纯属性取代get和set方法

从其他语言转入Python的开发者可能会在类中明确实现getter, setter但这种做法不像Python编程风格

class OldResistor(object):
    def __init__(self, ohms):
        self._ohms = ohms
    def get_ohms(self):
        return self._ohms
    def set_ohms(self, ohms):
        self._ohms = ohms


obj = OldResistor(50e3)
print(obj.get_ohms())

但是对Python来说不需要手工实现setter和getter而是直接用public属性这样原地自增操作变得清晰自然

class Resistor(object):
	def __init(self, ohms):
		self.ohms = ohms
r1 = Resistor(50e3)
r1.ohms += 5e3

30. 考虑用@property代替属性重构

Python内置的@property装饰器负责把一个方法变成属性调用

例使用@property既能检查参数又可以用类似属性这样简单方法访问类的变量

class Student(object):
    @property
    def score(self):
        return self.score

    @score.setter
    def score(self, value):
        if not isinstance(value, int):
            raise ValueError('score must be an int!')
        if value < 0 or value > 100:
            raise ValueError('score must between 0 ~ 100')
        self._score = value

s = Student()
s.score = 60
s.score = 10000
>>> python test.py
ValueError: score must between 0 ~ 100

31. 用描述符来改写需要复用的@property方法

32. 用getattr, getattribute, setattr实现按需生成操作 (惰性方式加载并保存对象属性

  • 如果某个类定义了__getattr__同时系统在该类对象实例字典中找不到待查属性那么系统就会调用这个方法
  • 程序每次访问对象属性Python系统会调用__getattribute__

33. 用元类验证子类

34. 用元类注册子类

第5章 并发及并行

36. 用subprocess模块管理子进程

import subprocess

proc = subprocess.Popen(['echo', 'hello'], stdout=subprocess.PIPE)
out, err = proc.communicate()
print(out.decode('utf-8'))

把子进程从父进程中剥离

import subprocess
import time


def run_sleep(period):
    proc = subprocess.Popen(['sleep', str(period)])
    return proc

start = time.time()
procs = []
for _ in range(10):
    proc = run_sleep(0.1)
    procs.append(proc)

for proc in procs:
    proc.communicate()
end = time.time()
print('Finished in %.3f seconds' % (end - start))

37. 可以用线程执行阻塞式IO但不要用它做并行计算

  • 受到全局解释器锁GIL限制多条Python线程不能在多个CPU核心上平行地执行字节码

38. 在线程中使用Lock来防止数据竞争

  • GIL不会保护开发者自己代码 同一时刻固然只能有一个Python线程运行但这个线程正在操作某个数据结构时其他线程可能会打断它也就是说Python解释器来执行两个连续字节码指令时其他线程可能会中途插进来。

threading.Lock类实现了互斥锁

class LockingCounter(object):
	def __init__(self):
		self.lock = Lock()
		self.count = 0
	def increment(self, offset):
		with self.lock:
			self.count += offset

39. 用Queue协调各线程之间的工作

import Queue
import threading


q = Queue.Queue(10=

def producer():
    for i in range(10):
        q.put(i)

def consumer():
    for i in range(10):
        print(q.get())

threads = []

t1 = threading.Thread(target=consumer)
t2 = threading.Thread(target=producer)

40.考虑用协程并发地运行多个函数

  • 对于生成器的yield表达式来说外部代码通过send方法传给生成器的那个值就是该表达式所具备的值

41. 考虑用concurrent.futures实现平行计算

  • multiprocessing模块可用于实现平行计算利用multiprocessing模块最恰当做法是通过内置concurrent.futures模块及其ProcessPoolExecutor类使用它

第6章 内置模块

42. 用functools.wraps定义函数修饰器

def trace(func):
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        print('%s(%r, %r) -> %r' % (func.__name__, args, kwargs, result))
        return result
    return wrapper


@trace
def fibonacci(n):
    if n in (0, 1):
        return n
    return fibonacci(n - 2) + fibonacci(n - 1)


print(fibonacci(3))
  • Python为修饰器提供专门语法使得程序运行时能用一个函数修改另一个函数

43. 考虑以contextlib和with语句改写可复用的try/finally代码

用with语句代替try… finally例如

with open('/path/to/file', 'r') as f:
	f.read()

44. 用copyreg实现可靠pickle操作

内置的pickle模块能够将Python对象字节序列转化为字节流也能把字节反序列化成Python对象

45. 使用datatime模块处理本地时间而不是time

  • 不要用time模块在不同时区进行转换
  • 开发者总是应该先把时间表示成UTC然后对其转换最后再转回本地时间

46. 使用内置算法和数据结构

  • 双向队列 deque
  • 有序字典 OrderedDict (按照键插入顺序保留键值对在字典中次序
  • 优先级队列 heapq
  • 二分查找 bisect
  • 迭代器有关 itertools

47.重视精确度场合应该使用decimal

Decimal类适合用在对精度要求高对舍入行为要求严格的场合例如涉及货币计算的场合

48. 学会安装由Python开发者社区构建的模块

  • PyPI (Python Package Index) 包含许多常用软件包 pip命令行工具可以从PyPI中安装软件包

第7章 协作开发

49. 为每个函数、类和模块编写文档字符串

为模块编写文档

  • 每个模块应该有顶级的docstring
  • 头一行文字用一句话描述本模块用途接着一段话包含一些细节信息把与本模块操作相关内容告诉模块使用者。
  • 还可以在模块的dockstring中强调本模块里比较重要类和函数使开发者据此了解该模块用法

举例

# words.py
#!/usr/bin/env python3
""" Library for XXX...

Testing how words XXX...

Available functions:
 - palindrome: Determine if a word ...
...
"""

为类编写文档

每一个类应该有docstring写法与模块级的docstring大致相同如下

class Player(object):
	"""Represents a player of the game.

	Subclassses may override ...

	Public attributes:
	- power: ...
	- coins: ...
	"""

为函数编写文档

函数的docstring第一行用一句话描述函数功能接下来用一段话描述具体行为和函数参数。 若函数由返回值应该在docstring中写明

    def find_anagrams(word, dictionary):
    	"""Find all anagrams for a word.

    	This function only runs as fast as the test for membership in the dictionary container...

    	Args:
    		word: String of the target word.
    		dictionary: Container with all strings that are known to be actual words.

    	Returns:
    		List of anagrams that were found. Empty if none were found.

作者的建议

  • 如果函数没有参数仅有一个简单返回值那么只需一句话描述该函数就够了
  • 如果函数没有返回值不要在docstring里提及不要出现return None这种说法
  • 如果正常使用中不会抛异常就不要在docstring提到异常
  • 如果函数接收可变参数应该在文档中描述*args, **kwargs用途
  • 如果函数参数有默认值应该指出这些默认值

50. 用包安排模块并提供稳固API

在目录中放入名为__init__.py的空文件就可以采用相对于该目录的路径引入目录中其他Python文件例如

main.py
mypackage/__init__.py
mypackage/utils.py

在main.py中引入utils模块

# main.py
from mypackage import utils

as子句可以给引入当前作用域的属性重新起名解决冲突

from analysis.utils import inspect as analysis_inspect
from frontend.utils import inspect as frontend_inspect

尽量不要使用import *语句而是应该用from x import y明确指出自己想要引入的名称

51. 为自编的模块定义根异常以便将调用者与API隔离

52. 用适当方式打破循环依赖关系

  • 如果两个模块必须相互调用对方才能完成引入操作就会出现循环依赖现象这可能导致程序启动时崩溃
  • 打破循环依赖关系最佳方案是把导致两个模块互相依赖的那部分代码重构为单独模块并把它放在依赖树底部

53. 用虚拟环境隔离项目并重建其依赖关系

第8章 部署

54. 考虑用模块级别代码配置不同部署环境

55. 通过repr字符串输出调试信息

简单的print不能打印出值的类型信息可以用repr获取类型信息

56. 用unittest测试全部代码

57. 用pdb实现交互调试

https://docs.python.org/zh-cn/3/library/pdb.html

58. 先分析性能然后再优化

59. 用tracemalloc掌握内存使用和泄漏情况

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: python