Effective Python 读书笔记

1
import this

用 Python 的方式思考

bytes str 和 unicode

接受 str 或 bytes 返回 str 的方法

1
2
3
4
5
6
def to_str(bytes_or_str):
if isinstance(bytes_or_str, bytes):
value = bytes_or_str.decode('utf-8')
else:
value = bytes_or_str
return value

接受 str 或 bytes 返回 bytes 的方法

1
2
3
4
5
6
def to_bytes(bytes_or_str):
if isinstance(bytes_or_str, str):
value = bytes_or_str.encode('utf-8')
else:
value = bytes_or_str
return value

切片

如果从序列的开头获取切片,那就不要在 start 那里写上 0 ,而是应该把它留空,这样代码看起来会清爽一些.

1
assert s[:5] == s[0:5]

如果切片一直要取到列表末尾,那就应该把 end 留空,因为即使写了,也是多余.

1
assert s[5:] == s[5:len(s)]

切割列表时,如果指定了 stride,那么代码可能会变得相当费解.我们呢不应该把 stride 与 start 和 end 写在一起.如果非要用,那就尽量采用正值.同时省略 start 和 end 索引.如果一定要配合 start 或 end 索引来使用 stride,请考虑步进式切片,把切割结果赋给某个变量,然后二次切片.

尽量用 enumerate 取代 range

1
2
for i, flavor in enumerate(flavor_list, 1):
print('{} : {}'.format(i, flavor))

zip() 遍历两个列表

1
2
3
4
names = ['tom', 'anny', 'jake']
letters = [len(n) for n in names]
for name, count in zip(names, letters):
print('{} : {}'.format(name, count))

如果输入的迭代器长度不同, 受封装的那些迭代器中,只要有一个耗尽了,zip 就不再产生新的元组了

函数

尽量用异常来表示特殊情况,而不要返回 None

1
2
3
4
5
def divide(a, b):
try:
return a / b
except ZeroDivisionError as e:
raise ValueError('Invlid inputs') from e

类与继承

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

很容易就能用 Python 内置的字典与元组类型构建出分层的数据结构, 从而保存程序的内部状态. 但是, 当嵌套多于一层的时候, 就应该避免这种做法(不要使用包含字典的字典), 这种多层嵌套的代码, 其他人很难看懂, 而且自己维护起来也很麻烦.

用来保存程序状态的数据结构一旦变得过于复杂, 就应该将其拆解为类, 以便提供更为明确的接口, 也能够在接口与具体实现之间创建抽象层.

把嵌套结构重构为类

collections 模块着的 namedtuple(具名元组)类型非常适合这种需求, 使用它很容易定义出精简而又不可变的数据类.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import collections

Grade = collections.namedtuple('Grade', ('score', 'weight'))


# 科目类
class Subject():
def __init__(self):
self._grades = []

def report_grade(self, score, weight):
self._grades.append(Grade(score, weight))

def average_grade(self):
total, total_weight = 0, 0
for grade in self._grades:
total += grade.score * grade.weight
total_weight += grade.weight
return total / total_weight


# 学生类
class Student():
def __init__(self):
self._subjects = {}

def subject(self, name):
if name not in self._subjects:
self._subjects[name] = Subject()
return self._subjects[name]

def average_grade(self):
total, count = 0, 0
for subject in self._subjects.values():
total += subject.average_grade()
count += 1
return total / count


# 考试成绩容器类
class Gradebook():
def __init__(self):
self._students = {}

def student(self, name):
if name not in self._students:
self._students[name] = Student()
return self._students[name]


if __name__ == '__main__':
book = Gradebook()
albert = book.student('Albert Einstein')
math = albert.subject('Math')
math.report_grade(80, 0.10)
print(albert.average_grade())

_要点_

  • 不要使用包含其它字典的字典, 也不要使用过长的元组
  • 如果容器中包含间的而又不可变的数据, 那么可以先使用 nametuple 来表示, 再修改为完整的类
  • 保存内部状态的字典如果变得比较复杂, 那就应该把这些代码拆解为多个辅助类.

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

    待续

    多用 public 属性, 少用 private 属性

    元类及属性

    用纯属性取代 getset 方法

    使用 @property 装饰器在设置属性的时候实现特殊行为.
    1
    2
    3
    4
    5
    class Resistor():
    def __init__(self, ohms):
    self.ohms = ohms
    self.voltage = 0
    self.current = 0

下面这个子类继承自 Resistor, 它在给 voltage(_电压)属性赋值的时候,还会同时修改 current(电流_)属性.

settergetter 方法的名称必须与相关属性相符.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class VoltageResistance(Resistor):
def __init__(self, ohms):
super().__init__(ohms)
self._voltage = 0

@property
def voltage(self):
return self._voltage

@voltage.setter
def voltage(self, voltage):
self._voltage = voltage
self.current = self._voltage / self.ohms

if __name__ == '__main__':
r2 = VoltageResistance(3)
print('Before: {} amps'.format(r2.current))
r2.voltage = 10
print('after: {} amps'.format(r2.curren

>>>

1
2
Before: 0 amps
after: 3.3333333333333335 amps

@property 来代替属性重构

带有配额的漏桶.
代码略

漏桶算法是一种具备传输, 调度和统计等用途的算法. 它把容器比作底部有漏洞的桶(leakybucket), 把配额(quota)比作桶底漏出的水.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Grade():
def __get__(*args, **kwargs):
#...
def __set__(*args, **kwargs):
# ...

class Exam():
#class attributes
math_grade = Grade()
writing_grade = Grade()
science_grade = Grade()

if __name__ == '__main__':
exam = Exam()
exam.writing_grade = 40

为属性赋值时, Python 会将其转译:

Exam.__dict__['writing_grade'].__set__(exam, 40)

在获取属性时
print(exam.writing_grade)
Python 也会将其转译
print(Exam.__dict__['writing_grade'].__get__(exam, Exam))

__getattr__ __getattribute__ 和 __setattr__ 实现按需生成的属性

__getattr__

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class LazyDB():
def __init__(self):
self.exists = 5

def __getattr__(self, name):
value = 'value for {}'.format(name)
setattr(self, name, value)
return value

if __name__ == '__main__':
data = LazyDB()
print('before: {}'.format(data.__dict__))
print('foo: {}'.format(data.foo))
print('after: {}'.format(data.__dict__))

>>>

1
2
3
before: {'exists': 5}
foo: value for foo
after: {'exists': 5, 'foo': 'value for foo'}

然后给 LazyDB 添加记录功能, 把程序对 __getattr__ 的调用行为记录下来. 为了避免无限递归, 需要在 LoggingLazyDB 子类里面通过 super().__getattr__() 来获取真正的属性值.

1
2
3
4
5
6
7
8
9
10
11
class LoggingLazyDB(LazyDB):
def __getattr__(self, name):
print('Called __getattr__{}'.format(name))
return super().__getattr__(name)


if __name__ == '__main__':
data = LoggingLazyDB()
print('exists: {}'.format(data.exists))
print('foo: {}'.format(data.foo))
print('foo: {}'.format(data.foo))

>>>

1
2
3
4
exists:  5
Called __getattr__foo
foo: value for foo
foo: value for foo

因为 exists 属性本身就在实例字典里面, 所以访问它的时候不会触发 __getattr__. foo 属性初始时并不在实例字典里, 所以初次访问的时候会触发 __getattr__. __getattr__ 调用 setattr 方法, 把 foo 放在实例字典中, 所以第二次访问 foo 的时候不会触发 __getattr__.

__getattribute__

程序每次访问对象的属性时, Python 会调用这个特殊方法, 即使属性字典里面已经有了该属性, 也依然会触发 __getattribute__ 方法.

ValidatingDB 会在 __getattribute__ 方法里面记录每次调用的时间.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class ValidatingDB():
def __init__(self):
self.exists = 5

def __getattribute__(self, name):
print('Called __getattrbute__ {}'.format(name))
try:
return super().__getattribute__(name)
except AttributeError:
value = 'value for {}'.format(name)
setattr(self, name, value)
return value

if __name__ == '__main__':
data = ValidatingDB()
print('exists: {}'.format(data.exists))
print('foo: {}'.format(data.foo))
print('foo: {}'.format(data.foo))

>>>

1
2
3
4
5
6
Called __getattrbute__ exists
exists: 5
Called __getattrbute__ foo
foo: value for foo
Called __getattrbute__ foo
foo: value for foo

按照 Python 处理缺失属性的标准流程, 如果程序动态地访问了一个不应该有的属性, 可以在 __getattr____getattrbute__ 里面抛出 AttributeError 异常.

1
2
3
4
5
6
7
8
class MissingPropertyDB():
def __getattr__(self, name):
if name == 'bad_name':
raise AttributeError('{} is missing'.format(name))

if __name__ == '__main__':
data = MissingPropertyDB()
data.bad_name

>>>

1
2
3
4
5
6
Traceback (most recent call last):
File "C:/Users/wter/OneDrive/pythonpj/half_a_wheel/half/test.py", line 54, in <module>
data.bad_name
File "C:/Users/wter/OneDrive/pythonpj/half_a_wheel/half/test.py", line 49, in __getattr__
raise AttributeError('{} is missing'.format(name))
AttributeError: bad_name is missing

实现通用的功能时, 会在 Python 中使用内置的 hasattr 函数来判断对象是否已经拥有了相关的属性, 并用内置的 getattr 函数来获取属性值. 这些函数会在实例字典中搜索待查询的属性,然后再调用 __getattr__.

1
2
3
4
5
data = LoggingLazyDB()
print('before: {}'.format(data.__dict__))
print('foo exists: {}'.format(hasattr(data, 'foo')))
print('after: {}'.format(data.__dict__))
print('foo exists: {}'.format(hasattr(data, 'foo')))

>>>

1
2
3
4
5
before:  {'exists': 5}
Called __getattr__foo
foo exists: True
after: {'exists': 5, 'foo': 'value for foo'}
foo exists: True

用元类验证子类

内容赞略

用元类来注册子类

内容赞略

用元类来注解类的属性

内容赞略

并发和并行

subprocess 模块来管理子进程

1
2
3
4
5
6
7
8
9
10
11
12
13
def run_sleep(period):
proc = subprocess.Popen(['sleep', str(period)])
return proc

start = time.time()
procs = []
for _ in range(0, 20):
proc = run_sleep(0.1)
procs.append(proc)
for proc in procs:
proc.communicate()
end = time.time()
print('Finished in {} s'.format(end - start))

用协程来并发的运行多个函数

_暂略_

内置模块

function.wraps 定义函数修饰器

datetima 模块来处理本地时间

time 模块

datetime 模块

使用内置算法与数据结构

双向队列

collection 模块中的 deque 类, 是一种双向队列. 从头部或者尾部插入或移除一个元素, 之需要消耗常数级别的时间.非常适合用来表示先进先出的队列.

1
2
3
fifo = deque()
fifo.append(1)
x = fifo.popleft()

list 从尾部插入或者移除元素, 需要O(1), 但是从头部插入或移除元素会消耗线性级别的时间.

有序字典

collection 模块中的 OrderedDict 类, 能够按照键的插入顺序, 来保留键值对在字典中的次序.

带有默认值的字典

1
2
stats = defaultdict(int)
stats[my_counter'] += 1

堆队列

heapd 模块提供了 heappush heappopnsmallest 等函数, 能在标准的list类型中创建堆结构.

二分查找

list 使用 index 方法来搜索某个元素, 所耗的时间会与列表的长度呈线性比例.
bisect 模块中的 bisect_left 等函数, 提供了高效的二分析半搜索算法, 可以在一系列排好顺序的元素之中搜寻某个值.

迭代器有关的工具

在重视精确度的场景, 应该使用 decimal

协作开发

文档

测试

Tips

使用大写的变量名称表示常量

习惯用下划线表示无用的变量