Post

《Effective Python》笔记-第8章 稳定与性能

第六十五条:合理利用try/except/else/finally结构中的每个代码块

try/finally形式的复合语句可以确保,无论try块是否抛出异常,finally快都会得到运行。

如果某段代码应该在前一段代码顺利执行之后加以运行,那么可以把它放到else块里面,而不要把这两段代码全都写到try快之中。这样可以让try块更加专注,同时也能够跟except块形成明确对照:except块写的是try块没有顺利执行时所要运行的代码。

如果你在某段代码执顺利执行之后多做一些处理,然后再清理资源,那么通常可以考虑把这三段代码写道try、else与finally块里。

第六十六条:考虑用contextlib和with语句来改写可复用的try/finally代码

可以把try/finally逻辑封装到情景管理器里面,这样能通过with结构反复运行这套逻辑,而不需要每次用到的时候,都再写一遍try/finally。

Python内置的contextlib模块提供里contextmanager修饰器,让我们可以很方便的修饰某个函数,从而制作相应的情景管理器,使得这个函数能够运用在with里面。

情景管理器通过yield语句所产生的值,可以由with语句之中位于as右侧的那个变量所接收,这样的话,就可以通过该变量与当前情景交互了。

1
2
3
4
5
6
7
8
9
10
11
12
13
@contextmanager
def log_level(level, name):
    logger = logging.getLogger(name)
    old_level = logger.getEffectiveLevel()
    logger.setLevel(level)
    try:
        yield logger
    finally:
        logger.setLevel(old_level)

with log_level(logging.DEBUG, 'my-log') as logger:
    logger.debug(f'This is a message for {logger.name}')
    logging.debug('This will not print')

第六十七条:用datetime模块处理本地时间,不要用time模块

协调世间时(Coordinated Universal Time, UTC)是标准的时间表示方法,不依赖特定时区。涉及时间的程序里,我们很有可能要再UTC与当地时间之间互相转换,这样才能给出用户容易理解的格式。

不要用time模块在不同时区之间转换。

把Python内置的datetime模块与开发者社区提供的pytz模块结合起来,可以在不同时区之间可靠地转换。

在操作时间数据的过程中,总是应该使用UTC时间,只要到了最后一步,才需要把它转回当地时间以便显示出来。

第六十八条:用copyreg实现可靠地pickle操作

Python内置的pickle模块可以把对象序列化成字节流,也可以把字节流反序列化(还原)成对象。

经过pickle处理的字节流只应该在彼此信任的双方之间传输,而不应该随意传给别人,或者随意接受别人发来的这种数据,因为这个模块所使用的这种序列化格式本身就没有考虑过安全问题。

如果对象所在的这个类发生了变化(例如增加或删除了某些错误属性),那么程序在还原旧版数据的时候,可能会出现错误。

把内置的copyreg模块与pickle模块搭配起来使用,可以让新版的程序兼容旧版的序列化数据

第六十九条:在需要准确计算的场合,用decimal表示相应的数值

每一种数值几乎都可以用Python内置的某个类型,或者内置模块之中的某个类表示出来。

在精度要求较高且需要控制舍入方式的场合(例如计算费用),可以考虑使用decimal类。

用小鼠构造Decimal的时候,如果想保证取值准确,那么一定要把这个数放在str字符串里面传递,而不要直接传过去,那样可能会有误差。

第七十条:先分析性能,然后再优化

Python内置了两种profiler模块,可以找到程序里面总执行时间比例最高的一部分。一种是由profile模块提供的纯Python版本,还有一种是由cProfile模块提供的C扩展版本。c扩展版本比纯Python版本要好,因为对受测程序影响较小,测评结果更加准确。

把需要接受性能测试的主函数传给Profile对象的runcall方法,就可以专门分析出这个体系下面所有函数的调用情况了。

可以通过Stats对象筛选出我们关心的那些分析结果,从而更为专注的思考如何优化程序性能。

1
2
3
4
5
6
7
8
9
10
11
from cProfile in Profile

profiler = Profile()
profiler.runcall(test)
# 用pstats模块与里面的Stats类来统计结果。
from pstats import Stars

stats = Stats(profiler)
stats.strip_dirs()
stats.sort_stats('cumulative')
stats.print_stats()

第七十一条:优先考虑用deque实现生产者-消费者队列

先进先出队列(first-in, first-out, FIFO)也叫做生产者-消费者队列(producer-consumer queue)或生产-消费队列。

list类型可以用来实现FIFO队列,生产者可以通过append方法向队列添加元素。这种方案有个问题,就是消费者在用pop(0)从队列中获取元素时,所花的时间会随着队列长度,呈平方式增长。

跟list不同,内置collections模块之中的deque类,无论是通过append添加元素,还是通过popleft获取元素,所花的时间都只跟队列长度呈线性关系,而非平方关系,这使得它更适合于FIFT队列。

第七十二条:考虑用bisect搜索已排序的序列

Python内置的bisect模块可以更好地搜索有序列表。其中的bisect_left函数,能够迅速地对任何一个有序的序列执行二分搜索。

1
2
3
4
5
6
from bisect import bisect_left

index = bisect_left(data, 91234)
assert index == 91234
index = bisect_left(data, 91234.56)
assert index == 91235

bisect最好的地方,是它不局限于list类型,而是可以用在任何一种行为类似序列的对象上面。

用index方法在已经排顺序的列表之中查找某个值,花费的时间与列表长度成正比,通过for循环单纯的做比较以寻找目标值,所花的时间也是如此。

而bisect模块里面的bisect_left函数,只需要花费对数级别的时间就可以在有序列表中搜寻某个值,比其他方法快好几个数量级。

第七十三条:学会使用heapq制作优先级队列

有时候,需要根据元素的重要程度来排序。这种情况下,应该使用优先级队列(priority queue)。Python内置的heapq模块可以高效地实现出优先级队列。

headq规定,添加到优先级队列中的元素必须是可比较的(comparable),并且需要具备自然顺序,可以通过对类套用@functools.total_ordering修饰器并定义__lt__方法来实现。

如果直接用相关的列表操作来模拟优先级队列,那么程序的性能会随着队列长度增大而大幅下降,因为这样做的复杂程度是平方级别,而不是线性级别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from heapq import heappush

def add_book(queue, book):
    heappush(queue, book)

# 使用@functools.total_ordering修饰Book类,实现__lt__方法。

import functools

@functools.total_ordering
class Book:
    def __init__(self, title, due_date):
        self.title = title
        self.due_date = due_date

    def __lt__(self, other):
        return self.due_date < other.due_date

第七十四条:考虑用memoryview与bytearray来实现无须拷贝的bytes操作

针对I/O密集型的任务,却很容易就能用各种方式写出吞吐量较大(也就是处理能力较强)的平行代码。

Python内置的memoryview类型提供了一套无须执行拷贝的(也就是零拷贝)操作接口,让我们可以对支持缓冲协议的Python对象制作切片,并通过这种切片高速地完成读取与写入。

Python内置的bytearray类型是一种与bytes相似但内容能改变的类型,我们可以通过socket.recv_from这样的函数,以无需拷贝的方式读取数据。

可以用memoryview来封装bytearray,从而用收到的数据覆盖底层缓冲里面的任意区段,同时又无需执行拷贝操作。

This post is licensed under CC BY 4.0 by the author.