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

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

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