分享Python 加速运行技巧

目录
  • 1.避免全局变量
  • 2.避免
    • 2.1 避免模块和函数属性访问
    • 2.2 避免类内属性访问
  • 3.避免不必要的抽象
  • 4,避免数据复制
    • 4.1 避免无意义的数据复制
    • 4.2 交换值时不使用中间变量
    • 4.3 字符串拼接用join而不是+
  • 5.利用 if 条件的短路特性
  • 6.循环优化
    • 6.1 用for循环代替while循环
    • 6.2 使用隐式for循环代替显式for循环
    • 6.3 减少内层for循环的计算
  • 7.使用 numba.jit
  • 8.选择合适的数据结构

前言:

Python 是一种脚本语言,相比 C/C++ 这样的编译语言,在效率和性能方面存在一些不足。但是,有很多时候,Python 的效率并没有想象中的那么夸张。本文对一些 Python 代码加速运行的技巧进行整理。

代码优化原则:

本文会介绍不少的 Python 代码加速运行的技巧。在深入代码优化细节之前,需要了解一些代码优化基本原则。

第一个基本原则:不要过早优化
很多人一开始写代码就奔着性能优化的目标,“让正确的程序更快要比让快速的程序正确容易得多”。因此,优化的前提是代码能正常工作。过早地进行优化可能会忽视对总体性能指标的把握,在得到全局结果前不要主次颠倒。

第二个基本原则:权衡优化的代价
优化是有代价的,想解决所有性能的问题是几乎不可能的。通常面临的选择是时间换空间或空间换时间。另外,开发代价也需要考虑。

第三个原则:不要优化那些无关紧要的部分
如果对代码的每一部分都去优化,这些修改会使代码难以阅读和理解。如果你的代码运行速度很慢,首先要找到代码运行慢的位置,通常是内部循环,专注于运行慢的地方进行优化。在其他地方,一点时间上的损失没有什么影响。

1.避免全局变量

# 不推荐写法。代码耗时:26.8秒
import math
 
size = 10000
for x in range(size):
    for y in range(size):
        z = math.sqrt(x) + math.sqrt(y)

许多程序员刚开始会用 Python 语言写一些简单的脚本,当编写脚本时,通常习惯了直接将其写为全局变量,例如上面的代码。但是,由于全局变量和局部变量实现方式不同,定义在全局范围内的代码运行速度会比定义在函数中的慢不少。通过将脚本语句放入到函数中,通常可带来 15% - 30% 的速度提升。

# 推荐写法。代码耗时:20.6秒
import math
 
def main():  # 定义到函数中,以减少全部变量使用
    size = 10000
    for x in range(size):
        for y in range(size):
            z = math.sqrt(x) + math.sqrt(y)
 
main()

2.避免

2.1 避免模块和函数属性访问

# 不推荐写法。代码耗时:14.5秒
import math
 
def computeSqrt(size: int):
    result = []
    for i in range(size):
        result.append(math.sqrt(i))
    return result
 
def main():
    size = 10000
    for _ in range(size):
        result = computeSqrt(size)
 
main()

每次使用.(属性访问操作符时)会触发特定的方法,如__getattribute__()__getattr__(),这些方法会进行字典操作,因此会带来额外的时间开销。通过from import语句,可以消除属性访问。

# 第一次优化写法。代码耗时:10.9秒
from math import sqrt
 
def computeSqrt(size: int):
    result = []
    for i in range(size):
        result.append(sqrt(i))  # 避免math.sqrt的使用
    return result
 
def main():
    size = 10000
    for _ in range(size):
        result = computeSqrt(size)
 
main()

在第 1 节中我们讲到,局部变量的查找会比全局变量更快,因此对于频繁访问的变量sqrt,通过将其改为局部变量可以加速运行。

# 第二次优化写法。代码耗时:9.9秒
import math
 
def computeSqrt(size: int):
    result = []
    sqrt = math.sqrt  # 赋值给局部变量
    for i in range(size):
        result.append(sqrt(i))  # 避免math.sqrt的使用
    return result
 
def main():
    size = 10000
    for _ in range(size):
        result = computeSqrt(size)
 
main()

除了math.sqrt外,computeSqrt函数中还有.的存在,那就是调用list的append方法。通过将该方法赋值给一个局部变量,可以彻底消除computeSqrt函数中for循环内部的.使用。

# 推荐写法。代码耗时:7.9秒
import math
 
def computeSqrt(size: int):
    result = []
    append = result.append
    sqrt = math.sqrt    # 赋值给局部变量
    for i in range(size):
        append(sqrt(i))  # 避免 result.append 和 math.sqrt 的使用
    return result
 
def main():
    size = 10000
    for _ in range(size):
        result = computeSqrt(size)
 
main()

2.2 避免类内属性访问

# 不推荐写法。代码耗时:10.4秒
import math
from typing import List
 
class DemoClass:
    def __init__(self, value: int):
        self._value = value
    
    def computeSqrt(self, size: int) -> List[float]:
        result = []
        append = result.append
        sqrt = math.sqrt
        for _ in range(size):
            append(sqrt(self._value))
        return result
 
def main():
    size = 10000
    for _ in range(size):
        demo_instance = DemoClass(size)
        result = demo_instance.computeSqrt(size)
 
main()

避免.的原则也适用于类内属性,访问self._value的速度会比访问一个局部变量更慢一些。通过将需要频繁访问的类内属性赋值给一个局部变量,可以提升代码运行速度。

# 推荐写法。代码耗时:8.0秒
import math
from typing import List
 
class DemoClass:
    def __init__(self, value: int):
        self._value = value
    
    def computeSqrt(self, size: int) -> List[float]:
        result = []
        append = result.append
        sqrt = math.sqrt
        value = self._value
        for _ in range(size):
            append(sqrt(value))  # 避免 self._value 的使用
        return result
 
def main():
    size = 10000
    for _ in range(size):
        demo_instance = DemoClass(size)
        demo_instance.computeSqrt(size)
 
main()

3.避免不必要的抽象

# 不推荐写法,代码耗时:0.55秒
class DemoClass:
    def __init__(self, value: int):
        self.value = value
 
    @property
    def value(self) -> int:
        return self._value
 
    @value.setter
    def value(self, x: int):
        self._value = x
 
def main():
    size = 1000000
    for i in range(size):
        demo_instance = DemoClass(size)
        value = demo_instance.value
        demo_instance.value = i
 
main()

任何时候当你使用额外的处理层(比如装饰器、属性访问、描述器)去包装代码时,都会让代码变慢。大部分情况下,需要重新进行审视使用属性访问器的定义是否有必要,使用getter/setter函数对属性进行访问通常是 C/C++ 程序员遗留下来的代码风格。如果真的没有必要,就使用简单属性。

# 推荐写法,代码耗时:0.33秒
class DemoClass:
    def __init__(self, value: int):
        self.value = value  # 避免不必要的属性访问器
 
def main():
    size = 1000000
    for i in range(size):
        demo_instance = DemoClass(size)
        value = demo_instance.value
        demo_instance.value = i
 
main()

4,避免数据复制

4.1 避免无意义的数据复制

# 不推荐写法,代码耗时:6.5秒
def main():
    size = 10000
    for _ in range(size):
        value = range(size)
        value_list = [x for x in value]
        square_list = [x * x for x in value_list]
 
main()

上面的代码中value_list完全没有必要,这会创建不必要的数据结构或复制。

# 推荐写法,代码耗时:4.8秒
def main():
    size = 10000
    for _ in range(size):
        value = range(size)
        square_list = [x * x for x in value]  # 避免无意义的复制
 
main()

另外一种情况是对 Python 的数据共享机制过于偏执,并没有很好地理解或信任 Python 的内存模型,滥用 copy.deepcopy()之类的函数。通常在这些代码中是可以去掉复制操作的。

4.2 交换值时不使用中间变量

# 不推荐写法,代码耗时:0.07秒
def main():
    size = 1000000
    for _ in range(size):
        a = 3
        b = 5
        temp = a
        a = b
        b = temp
 
main()

上面的代码在交换值时创建了一个临时变量temp,如果不借助中间变量,代码更为简洁、且运行速度更快。

# 推荐写法,代码耗时:0.06秒
def main():
    size = 1000000
    for _ in range(size):
        a = 3
        b = 5
        a, b = b, a  # 不借助中间变量
 
main()

4.3 字符串拼接用join而不是+

# 不推荐写法,代码耗时:2.6秒
import string
from typing import List
 
def concatString(string_list: List[str]) -> str:
    result = ''
    for str_i in string_list:
        result += str_i
    return result
 
def main():
    string_list = list(string.ascii_letters * 100)
    for _ in range(10000):
        result = concatString(string_list)
 
main()

当使用a + b拼接字符串时,由于 Python 中字符串是不可变对象,其会申请一块内存空间,将a和b分别复制到该新申请的内存空间中。因此,如果要拼接n个字符串,会产生 n-1个中间结果,每产生一个中间结果都需要申请和复制一次内存,严重影响运行效率。而使用join()拼接字符串时,会首先计算出需要申请的总的内存空间,然后一次性地申请所需内存,并将每个字符串元素复制到该内存中去。

# 推荐写法,代码耗时:0.3秒
import string
from typing import List
 
def concatString(string_list: List[str]) -> str:
    return ''.join(string_list)  # 使用 join 而不是 +
 
def main():
    string_list = list(string.ascii_letters * 100)
    for _ in range(10000):
        result = concatString(string_list)
 
main()

5.利用 if 条件的短路特性

# 不推荐写法,代码耗时:0.05秒
from typing import List
 
def concatString(string_list: List[str]) -> str:
    abbreviations = {'cf.', 'e.g.', 'ex.', 'etc.', 'flg.', 'i.e.', 'Mr.', 'vs.'}
    abbr_count = 0
    result = ''
    for str_i in string_list:
        if str_i in abbreviations:
            result += str_i
    return result
 
def main():
    for _ in range(10000):
        string_list = ['Mr.', 'Hat', 'is', 'Chasing', 'the', 'black', 'cat', '.']
        result = concatString(string_list)
 
main()

if 条件的短路特性是指对if a and b这样的语句, 当a为False时将直接返回,不再计算b;对于if a or b这样的语句,当a为True时将直接返回,不再计算b。因此, 为了节约运行时间,对于or语句,应该将值为True可能性比较高的变量写在or前,而and应该推后。

# 推荐写法,代码耗时:0.03秒
from typing import List
 
def concatString(string_list: List[str]) -> str:
    abbreviations = {'cf.', 'e.g.', 'ex.', 'etc.', 'flg.', 'i.e.', 'Mr.', 'vs.'}
    abbr_count = 0
    result = ''
    for str_i in string_list:
        if str_i[-1] == '.' and str_i in abbreviations:  # 利用 if 条件的短路特性
            result += str_i
    return result
 
def main():
    for _ in range(10000):
        string_list = ['Mr.', 'Hat', 'is', 'Chasing', 'the', 'black', 'cat', '.']
        result = concatString(string_list)
 
main()

6.循环优化

6.1 用for循环代替while循环

# 不推荐写法。代码耗时:6.7秒
def computeSum(size: int) -> int:
    sum_ = 0
    i = 0
    while i < size:
        sum_ += i
        i += 1
    return sum_
 
def main():
    size = 10000
    for _ in range(size):
        sum_ = computeSum(size)
 
main()

Python 的for循环比while循环快不少。

# 推荐写法。代码耗时:4.3秒
def computeSum(size: int) -> int:
    sum_ = 0
    for i in range(size):  # for 循环代替 while 循环
        sum_ += i
    return sum_
 
def main():
    size = 10000
    for _ in range(size):
        sum_ = computeSum(size)
 
main()

6.2 使用隐式for循环代替显式for循环

针对上面的例子,更进一步可以用隐式for循环来替代显式for循环

# 推荐写法。代码耗时:1.7秒
def computeSum(size: int) -> int:
    return sum(range(size))  # 隐式 for 循环代替显式 for 循环
 
def main():
    size = 10000
    for _ in range(size):
        sum = computeSum(size)
 
main()

6.3 减少内层for循环的计算

# 不推荐写法。代码耗时:12.8秒
import math
 
def main():
    size = 10000
    sqrt = math.sqrt
    for x in range(size):
        for y in range(size):
            z = sqrt(x) + sqrt(y)
 
main()

上面的代码中sqrt(x)位于内侧for循环, 每次训练过程中都会重新计算一次,增加了时间开销。

# 推荐写法。代码耗时:7.0秒
import math
 
def main():
    size = 10000
    sqrt = math.sqrt
    for x in range(size):
        sqrt_x = sqrt(x)  # 减少内层 for 循环的计算
        for y in range(size):
            z = sqrt_x + sqrt(y)
 
main()

7.使用 numba.jit

我们沿用上面介绍过的例子,在此基础上使用numba.jit。numba可以将 Python 函数 JIT 编译为机器码执行,大大提高代码运行速度。

# 推荐写法。代码耗时:0.62秒
import numba
 
@numba.jit
def computeSum(size: float) -> int:
    sum = 0
    for i in range(size):
        sum += i
    return sum
 
def main():
    size = 10000
    for _ in range(size):
        sum = computeSum(size)
 
main()

8.选择合适的数据结构

Python 内置的数据结构如str, tuple, list, set, dict底层都是 C 实现的,速度非常快,自己实现新的数据结构想在性能上达到内置的速度几乎是不可能的。

list类似于 C++ 中的std::vector,是一种动态数组。其会预分配一定内存空间,当预分配的内存空间用完,又继续向其中添加元素时,会申请一块更大的内存空间,然后将原有的所有元素都复制过去,之后销毁之前的内存空间,再插入新元素。删除元素时操作类似,当已使用内存空间比预分配内存空间的一半还少时,会另外申请一块小内存,做一次元素复制,之后销毁原有大内存空间。

因此,如果有频繁的新增、删除操作,新增、删除的元素数量又很多时,list的效率不高。此时,应该考虑使用collections.dequecollections.deque是双端队列,同时具备栈和队列的特性,能够在两端进行 O(1)复杂度的插入和删除操作。

list的查找操作也非常耗时。当需要在list频繁查找某些元素,或频繁有序访问这些元素时,可以使用bisect维护list对象有序并在其中进行二分查找,提升查找的效率。

另外一个常见需求是查找极小值或极大值,此时可以使用heapq模块将list转化为一个堆,使得获取最小值的时间复杂度是O(1)。

到此这篇关于分享Python 加速运行技巧的文章就介绍到这了,更多相关Python 加速运行技巧内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Python加速程序运行的方法

    问题 你的程序运行太慢,你想在不使用复杂技术比如C扩展或JIT编译器的情况下加快程序运行速度. 解决方案 关于程序优化的第一个准则是"不要优化",第二个准则是"不要优化那些无关紧要的部分". 如果你的程序运行缓慢,首先你得使用14.13小节的技术先对它进行性能测试找到问题所在. 通常来讲你会发现你得程序在少数几个热点位置花费了大量时间, 比如内存的数据处理循环.一旦你定位到这些点,你就可以使用下面这些实用技术来加速程序运行. 使用函数 很多程序员刚开始会使用Pyth

  • python运行加速的几种方式

    目录 一.总结 二.全面加速(pypy) 二.减少文件的打开即with的调用 三.if判断靠前 一.总结 1.使用pypy 2.减少函数化调用 3.减少文件的打开即with的调用,将这一调用放在for循环前面,然后传递至后面需要用到的地方 4.if函数判断条件多的尽量在前面 全面加速(pypy) 二.全面加速(pypy) 将python换为pypy,在纯python代码下,pypy的兼容性就不影响使用了,因为一些纯python的代码常常会用pypy进行一下加速 测试代码,for循环1000000

  • 分享Python 加速运行技巧

    目录 1.避免全局变量 2.避免 2.1 避免模块和函数属性访问 2.2 避免类内属性访问 3.避免不必要的抽象 4,避免数据复制 4.1 避免无意义的数据复制 4.2 交换值时不使用中间变量 4.3 字符串拼接用join而不是+ 5.利用 if 条件的短路特性 6.循环优化 6.1 用for循环代替while循环 6.2 使用隐式for循环代替显式for循环 6.3 减少内层for循环的计算 7.使用 numba.jit 8.选择合适的数据结构 前言: Python 是一种脚本语言,相比 C/

  • 分享3个简单的Python代码高效运行技巧

    目录 1. 引言 2. 获取字典的值 3. 循环中使用enumerate 4. 使用f-strings来拼接和打印字符串 5. 总结 1. 引言 小伙伴们日常工作中都必不可少地使用Python实现一些简单的功能,但是不同的人所编写的代码执行效率往往是不同的.本文重点介绍大家经常遇到的场景下,三个有效的,方便理解的,执行高效的实用技巧. 闲话少说,我们直接开始吧!!! 2. 获取字典的值 不妨假设我们有以下字典: my_dict = {'first_name': 'Michaela',      

  • python语言使用技巧分享

    一 在写之前 最好指定python的路径: #!/usr/bin/python python 在linux中需要添加编码方式:以免出现中文乱码 # -*- coding: UTF-8 –*- 二 在各类语言中,python应该是最会利用识缩进的语言 ,他的for语句即使有多行也不需要想java,C++.c一样使用{} ,可以像js.swift一样同换行符代表一句话,而不是使用: 号.有学过语言背景的同学请注意: 在python看来: 如果改变了缩进的方式,例如在第二个for上缩进,会导致错误:

  • 分享Python 的24个编程超好用技巧

    目录 1.ALLORANY 2.BASHPLOTIB 3.COLLECTIONS 4.DIR 5.EMOJI 6.FROM_FUTURE_IMPORT 7.GEOPY 8.HOWDOI 9.INSPECT 10.JEDI 11.**KWARGS 12.LISTCOMPREHENSIONS 13.MAP 14.NEWSPAPER3K 15.OPERATOROVERLOADING(操作符重载) 16.PPRINT 17.QUEUE(队列) 18.sh 19.TYPEHINT(类型提示) 20.UUI

  • python使用建议技巧分享(三)

    这是一个系列文章,主要分享python的使用建议和技巧,每次分享3点,希望你能有所收获. 1 如何去掉list中重复元素 my_list = [3, 2, 1, 1, 2, 3] print my_list # [3, 2, 1, 1, 2, 3] unique_list = list(set(my_list)) print unique_list # [1, 2, 3] 或者 from collections import OrderedDict my_list = [3, 2, 1, 1,

  • Python 高效编程技巧分享

    一.根据条件在序列中筛选数据 假设有一个数字列表 data, 过滤列表中的负数 data = [1, 2, 3, 4, -5] # 使用列表推导式 result = [i for i in data if i >= 0] # 使用 fliter 过滤函数 result = filter(lambda x: x >= 0, data) 学生的数学分数以字典形式存储,筛选其中分数大于 80 分的同学 from random import randint d = {x: randint(50, 10

  • 分享18 个 Python 高效编程技巧

    目录 01 交换变量 02 字典推导(Dictionary comprehensions)和集合推导(Set comprehensions) 03 计数时使用Counter计数对象. 04 漂亮的打印出JSON 05 解决FizzBuzz 06 if 语句在行内 07 连接 08 数值比较 09 同时迭代两个列表 10 带索引的列表迭代 11 列表推导式 12 字典推导 13 初始化列表的值 14 列表转换为字符串 15 从字典中获取元素 16 获取列表的子集 17 迭代工具 18 False

  • 分享Python中四个不常见的小技巧

    目录 1. 引言 2. 获取 n 个最大数字 3. 获取 n 个最小数字 4. 删除字符串的特定部分 5. 从列表中删除重复元素 6. 总结 1. 引言 在编程界,每个人都希望自己可以写出世界上最好的代码,其实最好的代码往往需要具备最好的代码质量.勤能补拙,善于总结往往可以快速提升大家的编程技巧. 本文重点对日常中不常使用的四个Python技巧进行简明阐述,希望可以提升大家编码时的工作效率. 闲话少说,我们直接开始吧! 2. 获取 n 个最大数字 我们知道,要获得列表中的最大数字,我们往往使用​

  • Python实现运行其他程序的四种方式实例分析

    本文实例讲述了Python实现运行其他程序的四种方式.分享给大家供大家参考,具体如下: 在Python中,可以方便地使用os模块来运行其他脚本或者程序,这样就可以在脚本中直接使用其他脚本或程序提供的功能,而不必再次编写实现该功能的代码.为了更好地控制运行的进程,可以使用win32process模块中的函数,如果想进一步控制进程,则可以使用ctype模块,直接调用kernel32.dll中的函数. [方式一]使用os.system()函数运行其他程序 os模块中的system()函数可以方便地运行

随机推荐