python-cookbook
  • Introduction
  • 第 1 章 数据结构和算法
    • 1.1 解压序列赋值给多个变量
    • 1.2 解压可迭代对象赋值给多个变量
    • 1.3 保留最后N个元素
    • 1.4 查找最大或最小的N个元素
    • 1.5 实现一个优先级队列
    • 1.6 字典中的键映射多个值
    • 1.7 字典排序
    • 1.8 字典的运算
    • 1.9 查找两字典的相同点
    • 1.10 删除序列相同元素并保持顺序
    • 1.11 命名切片
    • 1.12 序列中出现次数最多的元素
    • 1.13 通过某个关键字排序一个字典列表
    • 1.14 排序不支持原生比较的对象
    • 1.15 通过某个字段将记录分组
    • 1.16 过滤序列元素
    • 1.17 从字典中提取子集
    • 1.18 映射名称到序列元素
    • 1.19 转换并同时计算数据
    • 1.20 合并多个字典或映射
  • 第 2 章 字符串和文本
    • 2.1 使用多个界定符分割字符串
    • 2.2 字符串开头或结尾匹配
    • 2.3 用Shell通配符匹配字符串
    • 2.4 字符串匹配和搜索
    • 2.5 字符串搜索和替换
    • 2.6 字符串忽略大小写的搜索替换
    • 2.7 最短匹配模式
    • 2.8 多行匹配模式
    • 2.9 将Unicode文本标准化
    • 2.10 在正则式中使用Unicode
    • 2.11 删除字符串中不需要的字符
    • 2.12 审查清理文本字符串
    • 2.13 字符串对齐
    • 2.14 合并拼接字符串
    • 2.15 字符串中插入变量
    • 2.16 以指定列宽格式化字符串
    • 2.17 在字符串中处理html和xml
    • 2.18 字符串令牌解析
    • 2.19 实现一个简单的递归下降分析器
    • 2.20 字节字符串上的字符串操作
  • 第 3 章 数字日期和时间
    • 3.1 数字的四舍五入
    • 3.2 执行精确的浮点数运算
    • 3.3 数字的格式化输出
    • 3.4 二八十六进制整数
    • 3.5 字节到大整数的打包与解包
    • 3.6 复数的数学运算
    • 3.7 无穷大与NaN
    • 3.8 分数运算
    • 3.9 大型数组运算
    • 3.10 矩阵与线性代数运算
    • 3.11 随机选择
    • 3.12 基本的日期与时间转换
    • 3.13 计算最后一个周五的日期
    • 3.14 计算当前月份的日期范围
    • 3.15 字符串转换为日期
    • 3.16 结合时区的日期操作
  • 第 4 章 迭代器与生成器
    • 4.1 手动遍历迭代器
    • 4.2 代理迭代
    • 4.3 使用生成器创建新的迭代模式
    • 4.4 实现迭代器协议
    • 4.5 反向迭代
    • 4.6 带有外部状态的生成器函数
    • 4.7 迭代器切片
    • 4.8 跳过可迭代对象的开始部分
    • 4.9 排列组合的迭代
    • 4.10 序列上索引值迭代
    • 4.11 同时迭代多个序列
    • 4.12 不同集合上元素的迭代
    • 4.13 创建数据处理管道
    • 4.14 展开嵌套的序列
    • 4.15 顺序迭代合并后的排序迭代对象
    • 4.16 迭代器代替while无限循环
  • 第 5 章 文件与 IO
    • 5.1 读写文本数据
    • 5.2 打印输出至文件中
    • 5.3 使用其他分隔符或行终止符打印
    • 5.4 读写字节数据
    • 5.5 文件不存在才能写入
    • 5.6 字符串的I-O操作
    • 5.7 读写压缩文件
    • 5.8 固定大小记录的文件迭代
    • 5.9 读取二进制数据到可变缓冲区中
    • 5.10 内存映射的二进制文件
    • 5.11 文件路径名的操作
    • 5.12 测试文件是否存在
    • 5.13 获取文件夹中的文件列表
    • 5.14 忽略文件名编码
    • 5.15 打印不合法的文件名
    • 5.16 增加或改变已打开文件的编码
    • 5.17 将字节写入文本文件
    • 5.18 将文件描述符包装成文件对象
    • 5.19 创建临时文件和文件夹
    • 5.20 与串行端口的数据通信
    • 5.21 序列化Python对象
  • 第 6 章 数据编码和处理
    • 6.1 读写CSV数据
    • 6.2 读写JSON数据
    • 6.3 解析简单的XML数据
    • 6.4 增量式解析大型XML文件
    • 6.5 将字典转换为XML
    • 6.6 解析和修改XML
    • 6.7 利用命名空间解析XML文档
    • 6.8 与关系型数据库的交互
    • 6.9 编码和解码十六进制数
    • 6.10 编码解码Base64数据
    • 6.11 读写二进制数组数据
    • 6.12 读取嵌套和可变长二进制数据
    • 6.13 数据的累加与统计操作
  • 第 7 章 函数
    • 7.1 可接受任意数量参数的函数
    • 7.2 只接受关键字参数的函数
    • 7.3 给函数参数增加元信息
    • 7.4 返回多个值的函数
    • 7.5 定义有默认参数的函数
    • 7.6 定义匿名或内联函数
    • 7.7 匿名函数捕获变量值
    • 7.8 减少可调用对象的参数个数
    • 7.9 将单方法的类转换为函数
    • 7.10 带额外状态信息的回调函数
    • 7.11 内联回调函数
    • 7.12 访问闭包中定义的变量
  • 第 8 章 类与对象
    • 8.1 改变对象的字符串显示
    • 8.2 自定义字符串的格式化
    • 8.3 让对象支持上下文管理协议
    • 8.4 创建大量对象时节省内存方法
    • 8.5 在类中封装属性名
    • 8.6 创建可管理的属性
    • 8.7 调用父类方法
    • 8.8 子类中扩展property
    • 8.9 创建新的类或实例属性
    • 8.10 使用延迟计算属性
    • 8.11 简化数据结构的初始化
    • 8.12 定义接口或者抽象基类
    • 8.13 实现数据模型的类型约束
    • 8.14 实现自定义容器
    • 8.15 属性的代理访问
    • 8.16 在类中定义多个构造器
    • 8.17 创建不调用init方法的实例
    • 8.18 利用Mixins扩展类功能
    • 8.19 实现状态对象或者状态机
    • 8.20 通过字符串调用对象方法
    • 8.21 实现访问者模式
    • 8.22 不用递归实现访问者模式
    • 8.23 循环引用数据结构的内存管理
    • 8.24 让类支持比较操作
    • 8.25 创建缓存实例
  • 第 9 章 元编程
    • 9.1 在函数上添加包装器
    • 9.2 创建装饰器时保留函数元信息
    • 9.3 解除一个装饰器
    • 9.4 定义一个带参数的装饰器
    • 9.5 可自定义属性的装饰器
    • 9.6 带可选参数的装饰器
    • 9.7 利用装饰器强制函数上的类型检查
    • 9.8 将装饰器定义为类的一部分
    • 9.9 将装饰器定义为类
    • 9.10 为类和静态方法提供装饰器
    • 9.11 装饰器为被包装函数增加参数
    • 9.12 使用装饰器扩充类的功能
    • 9.13 使用元类控制实例的创建
    • 9.14 捕获类的属性定义顺序
    • 9.15 定义有可选参数的元类
    • 9.16 args和*kwargs的强制参数签名
    • 9.17 在类上强制使用编程规约
    • 9.18 以编程方式定义类
    • 9.19 在定义的时候初始化类的成员
    • 9.20 利用函数注解实现方法重载
    • 9.21 避免重复的属性方法
    • 9.22 定义上下文管理器的简单方法
    • 9.23 在局部变量域中执行代码
    • 9.24 解析与分析Python源码
    • 9.25 拆解Python字节码
  • 第 10 章 模块与包
    • 10.1 构建一个模块的层级包
    • 10.2 控制模块被全部导入的内容
    • 10.3 使用相对路径名导入包中子模块
    • 10.4 将模块分割成多个文件
    • 10.5 利用命名空间导入目录分散的代码
    • 10.6 重新加载模块
    • 10.7 运行目录或压缩文件
    • 10.8 读取位于包中的数据文件
    • 10.9 将文件夹加入到sys.path
    • 10.10 通过字符串名导入模块
    • 10.11 通过钩子远程加载模块
    • 10.12 导入模块的同时修改模块
    • 10.13 安装私有的包
    • 10.14 创建新的Python环境
    • 10.15 分发包
  • 第 11 章 网络与 Web 编程
    • 11.1 作为客户端与HTTP服务交互
    • 11.2 创建TCP服务器
    • 11.3 创建UDP服务器
    • 11.4 通过CIDR地址生成对应的IP地址集
    • 11.5 创建一个简单的REST接口
    • 11.6 通过XML-RPC实现简单的远程调用
    • 11.7 在不同的Python解释器之间交互
    • 11.8 实现远程方法调用
    • 11.9 简单的客户端认证
    • 11.10 在网络服务中加入SSL
    • 11.11 进程间传递Socket文件描述符
    • 11.12 理解事件驱动的IO
    • 11.13 发送与接收大型数组
  • 第 12 章 并发编程
    • 12.1 启动与停止线程
    • 12.2 判断线程是否已经启动
    • 12.3 线程间通信
    • 12.4 给关键部分加锁
    • 12.5 防止死锁的加锁机制
    • 12.6 保存线程的状态信息
    • 12.7 创建一个线程池
    • 12.8 简单的并行编程
    • 12.9 Python的全局锁问题
    • 12.10 定义一个Actor任务
    • 12.11 实现消息发布-订阅模型
    • 12.12 使用生成器代替线程
    • 12.13 多个线程队列轮询
    • 12.14 在Unix系统上面启动守护进程
  • 第 13 章 脚本编程与系统管理
    • 13.1 通过重定向-管道-文件接受输入
    • 13.2 终止程序并给出错误信息
    • 13.3 解析命令行选项
    • 13.4 运行时弹出密码输入提示
    • 13.5 获取终端的大小
    • 13.6 执行外部命令并获取它的输出
    • 13.7 复制或者移动文件和目录
    • 13.8 创建和解压归档文件
    • 13.9 通过文件名查找文件
    • 13.10 读取配置文件
    • 13.11 给简单脚本增加日志功能
    • 13.12 给函数库增加日志功能
    • 13.13 实现一个计时器
    • 13.14 限制内存和CPU的使用量
    • 13.15 启动一个WEB浏览器
  • 第 14 章 测试、调试和异常
    • 14.1 测试stdout输出
    • 14.2 在单元测试中给对象打补丁
    • 14.3 在单元测试中测试异常情况
    • 14.4 将测试输出用日志记录到文件中
    • 14.5 忽略或期望测试失败
    • 14.6 处理多个异常
    • 14.7 捕获所有异常
    • 14.8 创建自定义异常
    • 14.9 捕获异常后抛出另外的异常
    • 14.10 重新抛出被捕获的异常
    • 14.11 输出警告信息
    • 14.12 调试基本的程序崩溃错误
    • 14.13 给你的程序做性能测试
    • 14.14 加速程序运行
  • 第 15 章 C 语言扩展
    • 15.1 使用ctypes访问C代码
    • 15.2 简单的C扩展模块
    • 15.3 编写扩展函数操作数组
    • 15.4 在C扩展模块中操作隐形指针
    • 15.5 从扩张模块中定义和导出C的API
    • 15.6 从C语言中调用Python代码
    • 15.7 从C扩展中释放全局锁
    • 15.8 C和Python中的线程混用
    • 15.9 用WSIG包装C代码
    • 15.10 用Cython包装C代码
    • 15.11 用Cython写高性能的数组操作
    • 15.12 将函数指针转换为可调用对象
    • 15.13 传递NULL结尾的字符串给C函数库
    • 15.14 传递Unicode字符串给C函数库
    • 15.15 C字符串转换为Python字符串
    • 15.16 不确定编码格式的C字符串
    • 15.17 传递文件名给C扩展
    • 15.18 传递已打开的文件给C扩展
    • 15.19 从C语言中读取类文件对象
    • 15.20 处理C语言中的可迭代对象
    • 15.21 诊断分段错误
Powered by GitBook
On this page
  • 问题
  • 解决方案
  • 使用函数
  • 尽可能去掉属性访问
  • 理解局部变量
  • 避免不必要的抽象
  • 使用内置的容器
  • 避免创建不必要的数据结构或复制
  • 讨论

Was this helpful?

  1. 第 14 章 测试、调试和异常

14.14 加速程序运行

问题

你的程序运行太慢,你想在不使用复杂技术比如 C 扩展或 JIT 编译器的情况下加快程序运行速度。

解决方案

关于程序优化的第一个准则是“不要优化”,第二个准则是“不要优化那些无关紧要的部分”。 如果你的程序运行缓慢,首先你得使用 14.13 小节的技术先对它进行性能测试找到问题所在。

通常来讲你会发现你的程序在少数几个热点地方花费了大量时间, 比如内存的数据处理循环。一旦你定位到这些点,你就可以使用下面这些实用技术来加速程序运行。

使用函数

很少有人知道,定义在全局范围的代码运行起来要比定义在函数中运行慢的多。这种速度差异是由于局部变量和全局变量的实现方式(使用局部变量要更快些)。 因此,如果你想让程序运行更快些,只需要将脚本语句放入函数中即可:

# somescript.py
import sys
import csv

def main(filename):
    with open(filename) as f:
         for row in csv.reader(f):
             # Some kind of processing
             pass

main(sys.argv[1])

速度的差异取决于实际运行的程序,不过根据经验,使用函数带来 15-30% 的性能提升是很常见的。

尽可能去掉属性访问

每一次使用点 (.) 操作符来访问属性的时候会带来额外的开销。 它会触发特定的方法,比如 __getattribute__() 和 __getattr__() ,这些方法会进行字典操作操作。

通常你可以使用 from module import name 这样的导入形式,以及使用绑定的方法。

假设你有如下的代码片段:

import math

def compute_roots(nums):
    result = []
    for n in nums:
        result.append(math.sqrt(n))
    return result

# Test
nums = range(1000000)
for n in range(100):
    r = compute_roots(nums)

现在我们修改 compute_roots() 函数如下,这个版本快一些:

from math import sqrt

def compute_roots(nums):

    result = []
    result_append = result.append
    for n in nums:
        result_append(sqrt(n))
    return result

唯一不同之处就是消除了属性访问。 用 sqrt() 代替了 math.sqrt() 。 The result.append() 方法被赋给一个局部变量 result_append ,然后在内部循环中使用它。

不过,这些改变只有在大量重复代码中才有意义,比如循环。 因此,这些优化也只是在某些特定地方才应该被使用。

理解局部变量

局部变量会比全局变量运行速度快。 对于频繁访问的名称,通过将这些名称变成局部变量可以加速程序运行。

例如,看下之前对于 compute_roots() 函数进行修改后的版本:

import math

def compute_roots(nums):
    sqrt = math.sqrt
    result = []
    result_append = result.append
    for n in nums:
        result_append(sqrt(n))
    return result

在这个版本中,sqrt 从 match 模块被拿出并放入了一个局部变量中。 如果你运行这个代码,大概花费25秒(对于之前29秒又是一个改进)。 这个额外的加速原因是因为对于局部变量 sqrt 的查找要快于全局变量 sqrt。

对于类中的属性访问也同样适用于这个原理。 通常来讲,查找某个值比如 self.name 会比访问一个局部变量要慢一些。 在内部循环中,可以将某个需要频繁访问的属性放入到一个局部变量中。例如:

# Slower
class SomeClass:
    # ...
    def method(self):
         for x in s:
             op(self.value)

# Faster
class SomeClass:
    # ...
    def method(self):
         value = self.value
         for x in s:
             op(value)

避免不必要的抽象

任何时候当你使用额外的处理层(比如装饰器、属性访问、描述器)去包装你的代码时,都会让程序运行变慢。

比如看下如下的这个类:

class A:
    def __init__(self, x, y):
        self.x = x
        self.y = y
    @property
    def y(self):
        return self._y
    @y.setter
    def y(self, value):
        self._y = value

现在进行一个简单测试:

>>> from timeit import timeit
>>> a = A(1,2)
>>> timeit('a.x', 'from __main__ import a')
0.07817923510447145
>>> timeit('a.y', 'from __main__ import a')
0.35766440676525235

可以看到,访问属性 y 相比属性 x 而言慢的不止一点点,大概慢了 4.5 倍。 如果你在意性能的话,那么就需要重新审视下对于 y 的属性访问器的定义是否真的有必要了。 如果没有必要,就使用简单属性吧。 如果仅仅是因为其他编程语言需要使用 getter/setter 函数就去修改代码风格,这个真的没有必要。

使用内置的容器

内置的数据类型比如字符串、元组、列表、集合和字典都是使用 C 来实现的,运行起来非常快。 如果你想自己实现新的数据结构(比如链接列表、平衡树等), 那么要想在性能上达到内置的速度几乎不可能,因此,还是乖乖的使用内置的吧。

避免创建不必要的数据结构或复制

有时候程序员想显摆下,构造一些并没有必要的数据结构。例如,有人可能会像下面这样写:

values = [x for x in sequence]
squares = [x*x for x in values]

也许这里的想法是首先将一些值收集到一个列表中,然后使用列表推导来执行操作。 不过,第一个列表完全没有必要。

与此相关,还要注意下那些对 Python 的共享数据机制过于偏执的程序所写的代码。 有些人并没有很好的理解或信任 Python 的内存模型,滥用 copy.deepcopy() 之类的函数。 通常在这些代码中是可以去掉复制操作的。

讨论

在优化之前,有必要先研究下使用的算法。 选择一个复杂度为 O(n log n) 的算法要比你去调整一个复杂度为 O(n**2) 的算法所带来的性能提升要大得多。

如果你觉得你还是得进行优化,那么请从整体考虑。 作为一般准则,不要对程序的每一个部分都去优化,因为这些修改会导致代码难以阅读和理解。 你应该专注于优化产生性能瓶颈的地方,比如内部循环。

你还要注意微小优化的结果。例如考虑下面创建一个字典的两种方式:

a = {
    'name' : 'AAPL',
    'shares' : 100,
    'price' : 534.22
}

b = dict(name='AAPL', shares=100, price=534.22)

后面一种写法更简洁一些(你不需要在关键字上输入引号)。 不过,如果你将这两个代码片段进行性能测试对比时,会发现使用 dict() 的方式会慢了 3 倍。 看到这个,你是不是有冲动把所有使用 dict() 的代码都替换成第一种。 不过,聪明的程序员只会关注他应该关注的地方,比如内部循环。在其他地方,这点性能损失没有什么影响。

如果你的优化要求比较高,本节的这些简单技术满足不了,那么你可以研究下基于即时编译(JIT)技术的一些工具。 例如,PyPy 工程是 Python 解释器的另外一种实现,它会分析你的程序运行并对那些频繁执行的部分生成本机机器码。 它有时候能极大的提升性能,通常可以接近 C 代码的速度。

你还可以考虑下 Numba 工程, Numba 是一个在你使用装饰器来选择 Python 函数进行优化时的动态编译器。 这些函数会使用 LLVM 被编译成本地机器码。它同样可以极大的提升性能。 但是,跟 PyPy 一样,它对于 Python 3 的支持现在还停留在实验阶段。【这是 2015 年的情况】

引用 John Ousterhout 说过的话作为结尾:“最好的性能优化是从不工作到工作状态的迁移”。 直到你真的需要优化的时候再去考虑它。确保你程序正确的运行通常比让它运行更快要更重要一些(至少开始是这样的)。

Previous14.13 给你的程序做性能测试Next第 15 章 C 语言扩展

Last updated 5 years ago

Was this helpful?