前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Python在生物信息学中的应用:让你的程序运行得更快

Python在生物信息学中的应用:让你的程序运行得更快

作者头像
简说基因
发布2024-02-21 16:35:17
830
发布2024-02-21 16:35:17
举报
文章被收录于专栏:简说基因简说基因

程序运行太慢,想要提速,但不使用复杂的技术如 C 扩展或 JIT 编译器。

解决方案

程序优化的第一准则是“不要优化”第二准则是“不要优化那些不重要的部分”。基于这两个原则,如果你的程序运行得很慢,你得先找出影响性能的问题所在。

多数时候我们发现程序把大量的时间花在几个热点位置,比如处理数据的内层循环。一旦确认了这些热点,就可以使用以下各小节中介绍的技术让程序运行得更快。

使用函数

很多人开始使用 Python 时都是用它来编写一些简单的脚本。最开始时,很容易陷入只管编写代码而不重视程序结构的怪圈。例如:

代码语言:javascript
复制
# somescript.py

import sys
import csv

with open(sys.argv[1]) as f:
    for row in csv.reader(f):

    # Some kind of processing
    pass

一个鲜为人知的事实是,像上面这样定义在全局范围内的代码比定义在函数中的代码要慢。速度的差异与局部变量与全局变量的实现机制有关(涉及局部变量的操作要更快)。因此,如果想让程序运行得更快,可以将脚本中的语句放入函数中即可:

代码语言:javascript
复制
# 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% 的情况很常见。

消除属性访问

每次使用句点操作符(.)来访问对象的属性都会带来开销。在底层,这会触发调用特殊的方法。

通常可以用 from module import name 的导入形式以及选择性地使用绑定方法(bound method)来避免出现属性查询操作。我们用下面的代码片段来加以说明:

代码语言:javascript
复制
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)

当在我们的机器上测试时,这个程序运行了大约 40 秒。现在将 compute_roots() 函数修改为如下形式:

代码语言:javascript
复制
from math import sqrt

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

修改后的版本运行时间大约是 29 秒。唯一不同之处就是消除了属性访问。用 sqrt() 代替了 math.sqrt()。result.append() 方法被赋给一个局部变量 result_append,然后在内部循环中使用它。

但是,必须强调的是,只有在频繁执行的代码中做这些修改才有意义,比如在循环中。因此,这种优化技术适用的场景需要经过精心挑选。

理解变量所处的位置

前述提及,访问局部变量比全局变量要快。对于需要频繁访问的名称,想提高运行速度,可以通过尽量让这些变量尽可能成为局部变量来实现。例如:

代码语言:javascript
复制
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 方法已经从 math 模块中提取出来并放置在一个局部变量中。如果运行这份代码,执行时间大约是 25 秒,这比上一个版本的 29 秒又有所提升。根本原因就是查找局部变量比全局变量要快。

当使用类时,局部参数同样能起到提速的效果。一般来说,查找像 self.name 这样的值会比访问一个局部变量要慢很多。在内层循环中将需要经常访问的属性移到局部变量中来会很划算。例如:

代码语言:javascript
复制
# 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)

避免不必要的抽象

装饰器(decorator)、属性(property)或者描述符(descriptor)包装过的代码,运行速度通常会变慢。参考以下代码:

代码语言:javascript
复制
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

测试一下:

代码语言:javascript
复制
>>> 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
>>>

使用内建的容器

内建的数据类型比如字符串、元组、列表、集合以及字典都是用 C 语言实现的,速度非常快。如果需要构建自己的数据结构作为替代(例如链表、二叉树等),想在性能上达到内建的速度几乎不可能,因此还是尽量使用内建的数据结构吧。

避免产生不必要的数据结构或者拷贝动作

有时候程序员可能会创建一些不必要的数据结构,比如下面的代码:

代码语言:javascript
复制
values = [x for x in sequence]
squares = [x*x for x in values]

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

代码语言:javascript
复制
squares = [x*x for x in sequence]

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

讨论

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

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

还要注意一些小的优化的结果。比如下面创建字典的两种方式:

代码语言:javascript
复制
a = {
    'name' : 'AAPL',
    'shares' : 100,
    'price' : 534.22
}

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

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

如果你的优化要求比较高,本节的这些简单技术满足不了,那么你可以研究下基于即时编译(JIT)技术的一些工具。例如,PyPy 工程是 Python 解释器的另外一种实现,它会分析你的程序运行并对那些频繁执行的部分生成本机机器码。它有时候能极大的提升性能,通常可以接近 C 代码的速度。不过可惜的是,到写这本书为止,PyPy 还不能完全支持 Python3。因此,这个是你将来需要去研究的。你还可以考虑下 Numba 工程, Numba 是一个在你使用装饰器来选择 Python 函数进行优化时的动态编译器。这些函数会使用LLVM被编译成本地机器码。它同样可以极大的提升性能。但是,跟 PyPy 一样,它对于 Python 3 的支持现在还停留在实验阶段。

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

参考

  • 《Python Cookbook》第三版
  • http://python3-cookbook.readthedocs.org/zh_CN/latest/
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2024-02-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 简说基因 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档