!这15个技巧直接让你的Python性能起飞

这是我参与2022首次更文挑战的第8天,活动详情查看:2022首次更文挑战

前言

Python 一直以来被大家所诟病的一点就是执行速度慢,但不可否认的是 Python 依然是我们学习和工作中的一大利器。因此,我们对 Python 呢是“又爱又恨”。

本文总结了一些小 tips 有助于提升 Python 执行速度、优化性能。以下所有技巧都经过我的验证,可放心食用💖。

先上结论:

  1. 使用map()进行函数映射
  2. 使用set()求交集
  3. 使用sort()sorted()排序
  4. 使用collections.Counter()计数
  5. 使用列表推导
  6. 使用join()连接字符串
  7. 使用x, y = y, x交换变量
  8. 使用while 1取代while True
  9. 使用装饰器缓存
  10. 减少点运算符(.)的使用
  11. 使用for循环取代while循环
  12. 使用Numba.jit加速计算
  13. 使用Numpy矢量化数组
  14. 使用in检查列表成员
  15. 使用itertools库迭代

如何测量程序的执行时间❓

关于 Python 如何精确地测量程序的执行时间,这个问题看起来简单其实很复杂,因为程序的执行时间受到很多因素的影响,例如操作系统、Python 版本以及相关硬件(CPU 性能、内存读写速度)等。在同一台电脑上运行相同版本的语言时,上述因素就是确定的了,但是程序的睡眠时间依然是变化的,且电脑上正在运行的其他程序也会对实验有干扰,因此严格来说这就是《实验不可重复》。

我了解到的关于计时比较有代表性的两个库就是timetimeit

其中,time库中有time()perf_counter()以及process_time()三个函数可用来计时⏲(以秒为单位),加后缀_ns表示以纳秒计时(自 Python3.7 始)。在此之前还有clock()函数,但是在 Python3.3 之后被移除了。上述三者的区别如下:

  • time()精度上相对没有那么高,而且受系统的影响,适合表示日期时间或者大程序的计时。
  • perf_counter()适合小一点的程序测试,会计算sleep()时间。
  • process_time()适合小一点的程序测试,不计算sleep()时间。

time库相比,timeit 有两个优点:

  • timeit 会根据您的操作系统和 Python 版本选择最佳计时器。
  • timeit 在计时期间会暂时禁用垃圾回收。

timeit.timeit(stmt='pass', setup='pass', timer=<default timer>, number=1000000, globals=None) 参数说明:

  • stmt='pass':需要计时的语句或者函数。
  • setup='pass':执行stmt之前要运行的代码。通常,它用于导入一些模块或声明一些必要的变量。
  • timer=<default timer>:计时器函数,默认为time.perf_counter()
  • number=1000000:执行计时语句的次数,默认为一百万次。
  • globals=None:指定执行代码的命名空间。

本文所有的计时均采用timeit方法,且采用默认的执行次数一百万次

为什么要执行一百万次呢?因为我们的测试程序很短,如果不执行这么多次的话,根本看不出差距。

正文

1.使用map()进行函数映射

✅ Exp1:将字符串数组中的小写字母转为大写字母。

测试数组为 oldlist = ['life', 'is', 'short', 'i', 'choose', 'python']。

  • 方法一
newlist = []
for word in oldlist:
    newlist.append(word.upper())
复制代码
  • 方法二
list(map(str.upper, oldlist))
复制代码

方法一耗时 0.5267724000000005s,方法二耗时 0.41462569999999843s,性能提升 21.29% 🚀

2.使用set()求交集

✅ Exp2:求两个list的交集。

测试数组:a = [1,2,3,4,5],b = [2,4,6,8,10]。

  • 方法一
overlaps = []
for x in a:
    for y in b:
        if x == y:
            overlaps.append(x)
复制代码
  • 方法二
list(set(a) & set(b))
复制代码

方法一耗时 0.9507264000000006s,方法二耗时 0.6148200999999993s,性能提升 35.33% 🚀

关于set()的语法:|&-分别表示求并集、交集、差集。

3.使用sort()sorted()排序

我们可以通过多种方式对序列进行排序,但其实自己编写排序算法的方法有些得不偿失。因为内置的 sort()或 sorted() 方法已经足够优秀了,且利用参数key可以实现不同的功能,非常灵活。二者的区别是sort()方法仅被定义在list中,而sorted()是全局方法对所有的可迭代序列都有效。

✅ Exp3:分别使用快排和sort()方法对同一列表排序。

测试数组:lists = [2,1,4,3,0]。

  • 方法一
def quick_sort(lists,i,j):
    if i >= j:
        return list
    pivot = lists[i]
    low = i
    high = j
    while i < j:
        while i < j and lists[j] >= pivot:
            j -= 1
        lists[i]=lists[j]
        while i < j and lists[i] <=pivot:
            i += 1
        lists[j]=lists[i]
    lists[j] = pivot
    quick_sort(lists,low,i-1)
    quick_sort(lists,i+1,high)
    return lists
复制代码
  • 方法二
lists.sort()
复制代码

方法一耗时 2.4796975000000003s,方法二耗时 0.05551999999999424s,性能提升 97.76% 🚀

顺带一提,sorted()方法耗时 0.1339823999987857s

可以看出,sort()作为list专属的排序方法还是很强的,sorted()虽然比前者慢一点,但是胜在它“不挑食”,它对所有的可迭代序列都有效。

扩展:如何定义sort()sorted()方法的key

  1. 通过lambda定义
#学生:(姓名,成绩,年龄)
students = [('john', 'A', 15),('jane', 'B', 12),('dave', 'B', 10)]
students.sort(key = lambda student: student[0]) #根据姓名排序
sorted(students, key = lambda student: student[0])
复制代码
  1. 通过operator定义
import operator

students = [('john', 'A', 15),('jane', 'B', 12),('dave', 'B', 10)]
students.sort(key=operator.itemgetter(0))
sorted(students, key = operator.itemgetter(1, 0)) #先对成绩排序,再对姓名排序
复制代码

operatoritemgetter()适用于普通数组排序,attrgetter()适用于对象数组排序

  1. 通过cmp_to_key()定义,最为灵活
import functools

def cmp(a,b):
    if a[1] != b[1]:
        return -1 if a[1] < b[1] else 1 #先按照成绩升序排序
    elif a[0] != b[0]:
        return -1 if a[0] < b[0] else 1 #成绩相同,按照姓名升序排序
    else:
        return -1 if a[2] > b[2] else 1 #成绩姓名都相同,按照年龄降序排序 

students = [('john', 'A', 15),('john', 'A', 14),('jane', 'B', 12),('dave', 'B', 10)]
sorted(students, key = functools.cmp_to_key(cmp))
复制代码

4.使用collections.Counter()计数

✅ Exp4:统计字符串中每个字符出现的次数。

测试数组:sentence='life is short, i choose python'。

  • 方法一
counts = {}
for char in sentence:
    counts[char] = counts.get(char, 0) + 1
复制代码
  • 方法二
from collections import Counter
Counter(sentence)
复制代码

方法一耗时 2.8105250000000055s,方法二耗时 1.6317423000000062s,性能提升 41.94% 🚀

5.使用列表推导

列表推导(list comprehension)短小精悍。在小代码片段中,可能没有太大的区别。但是在大型开发中,它可以节省一些时间。

✅ Exp5:对列表中的奇数求平方,偶数不变。

测试数组:oldlist = range(10)。

  • 方法一
newlist = []
for x in oldlist:
    if x % 2 == 1:
        newlist.append(x**2)
复制代码
  • 方法二
[x**2 for x in oldlist if x%2 == 1]
复制代码

方法一耗时 1.5342976000000021s,方法二耗时 1.4181957999999923s,性能提升 7.57% 🚀

6.使用 join() 连接字符串

大多数人都习惯使用+来连接字符串。但其实,这种方法非常低效。因为,+操作在每一步中都会创建一个新字符串并复制旧字符串。更好的方法是用 join() 来连接字符串。关于字符串的其他操作,也尽量使用内置函数,如isalpha()isdigit()startswith()endswith()等。

✅ Exp6:将字符串列表中的元素连接起来。

测试数组:oldlist = ['life', 'is', 'short', 'i', 'choose', 'python']。

  • 方法一
sentence = ""
for word in oldlist:
    sentence += word
复制代码
  • 方法二
"".join(oldlist)
复制代码

方法一耗时 0.27489080000000854s,方法二耗时 0.08166570000000206s,性能提升 70.29% 🚀

join还有一个非常舒服的点,就是它可以指定连接的分隔符,举个例子👇

oldlist = ['life', 'is', 'short', 'i', 'choose', 'python']
sentence = "//".join(oldlist)
print(sentence)
复制代码

life//is//short//i//choose//python

7.使用x, y = y, x交换变量

✅ Exp6:交换x,y的值。

测试数据:x, y = 100, 200。

  • 方法一
temp = x
x = y
y = temp
复制代码
  • 方法二
x, y = y, x
复制代码

方法一耗时 0.027853900000010867s,方法二耗时 0.02398730000000171s,性能提升 13.88% 🚀

8.使用while 1取代while True

在不知道确切的循环次数时,常规方法是使用while True进行无限循环,在代码块中判断是否满足循环终止条件。虽然这样做没有任何问题,但while 1的执行速度比while True更快。因为它是一种数值转换,可以更快地生成输出。

✅ Exp8:分别用while 1while True循环 100 次。

  • 方法一
i = 0
while True:
    i += 1
    if i > 100:
        break
复制代码
  • 方法二
i = 0
while 1:
    i += 1
    if i > 100:
        break
复制代码

方法一耗时 3.679268300000004s,方法二耗时 3.607847499999991s,性能提升1.94% 🚀

9.使用装饰器缓存

将文件存储在高速缓存中有助于快速恢复功能。Python 支持装饰器缓存,该缓存在内存中维护特定类型的缓存,以实现最佳软件驱动速度。我们使用lru_cache装饰器来为斐波那契函数提供缓存功能,在使用fibonacci递归函数时,存在大量的重复计算,例如fibonacci(1)fibonacci(2)就运行了很多次。而在使用了lru_cache后,所有的重复计算只会执行一次,从而大大提高程序的执行效率。

✅ Exp9:求斐波那契数列。

测试数据:fibonacci(7)。

  • 方法一
def fibonacci(n):
    if n == 0:
        return 0
    elif n == 1:
        return 1
    return fibonacci(n - 1) + fibonacci(n-2)
复制代码
  • 方法二
import functools

@functools.lru_cache(maxsize=128)
def fibonacci(n):
    if n == 0:
        return 0
    elif n == 1:
        return 1
    return fibonacci(n - 1) + fibonacci(n-2)
复制代码

方法一耗时 3.955014900000009s,方法二耗时 0.05077979999998661s,性能提升 98.72% 🚀

❗注意事项:

  • 缓存是按照参数作为键,也就说在参数不变时,被lru_cache装饰的函数只会执行一次。
  • 所有参数必须可哈希,例如list不能作为被lru_cache装饰的函数的参数。
import functools
 
@functools.lru_cache(maxsize=100)
def demo(a, b):
    print('我被执行了')
    return a + b
if __name__ == '__main__':
    demo(1, 2)
    demo(1, 2)
复制代码

我被执行了(执行了两次demo(1, 2),却只输出一次

from functools import lru_cache
 
@lru_cache(maxsize=100)
def list_sum(nums: list):
    return sum(nums)

if __name__ == '__main__':
    list_sum([1, 2, 3, 4, 5])
复制代码

TypeError: unhashable type: 'list'

functools.lru_cache(maxsize=128, typed=False)的两个可选参数:

  • maxsize代表缓存的内存占用值,超过这个值之后,就的结果就会被释放,然后将新的计算结果进行缓存,其值应当设为 2 的幂。
  • typed若为True,则会把不同的参数类型得到的结果分开保存。

10.减少点运算符(.)的使用

点运算符(.)用来访问对象的属性或方法,这会引起程序使用__getattribute__()__getattr__()进行字典查找,从而带来不必要的开销。尤其注意,在循环当中,更要减少点运算符的使用,应该将它移到循环外处理。

这启发我们应该尽量使用from ... import ...这种方式来导包,而不是在需要使用某方法时通过点运算符来获取。其实不光是点运算符,其他很多不必要的运算我们都尽量移到循环外处理。

✅ Exp10:将字符串数组中的小写字母转为大写字母。

测试数组为 oldlist = ['life', 'is', 'short', 'i', 'choose', 'python']。

  • 方法一
newlist = []
for word in oldlist:
    newlist.append(str.upper(word))
复制代码
  • 方法二
newlist = []
upper = str.upper
for word in oldlist:
    newlist.append(upper(word))
复制代码

方法一耗时 0.7235491999999795s,方法二耗时 0.5475435999999831s,性能提升 24.33% 🚀

11.使用for循环取代while循环

当我们知道具体要循环多少次时,使用for循环比使用while循环更好。

✅ Exp12:使用forwhile分别循环 100 次。

  • 方法一
i = 0
while i < 100:
    i += 1
复制代码
  • 方法二
for _ in range(100):
    pass
复制代码

方法一耗时 3.894683299999997s,方法二耗时 1.0198077999999953s,性能提升73.82% 🚀

12.使用Numba.jit加速计算

Numba 可以将 Python 函数编译码为机器码执行,大大提高代码执行速度,甚至可以接近 C 或 FORTRAN 的速度。它能和 Numpy 配合使用,在 for 循环中或存在大量计算时能显著地提高执行效率。

✅ Exp12:求从 1 加到 100 的和。

  • 方法一
def my_sum(n):
    x = 0
    for i in range(1, n+1):
        x += i
    return x
复制代码
  • 方法二
from numba import jit

@jit(nopython=True) 
def numba_sum(n):
    x = 0
    for i in range(1, n+1):
        x += i
    return x
复制代码

方法一耗时 3.7199997000000167s,方法二耗时 0.23769430000001535s,性能提升 93.61% 🚀

13.使用Numpy矢量化数组

矢量化是 NumPy 中的一种强大功能,可以将操作表达为在整个数组上而不是在各个元素上发生。这种用数组表达式替换显式循环的做法通常称为矢量化。

在 Python 中循环数组或任何数据结构时,会涉及很多开销。NumPy 中的向量化操作将内部循环委托给高度优化的 C 和 Fortran 函数,从而使 Python 代码更加快速。

✅ Exp13:两个长度相同的序列逐元素相乘。

测试数组:a = [1,2,3,4,5], b = [2,4,6,8,10]

  • 方法一
[a[i]*b[i] for i in range(len(a))]
复制代码
  • 方法二
import numpy as np
a = np.array([1,2,3,4,5])
b = np.array([2,4,6,8,10])
a*b
复制代码

方法一耗时 0.6706845000000214s,方法二耗时 0.3070132000000001s,性能提升 54.22% 🚀

14.使用in检查列表成员

若要检查列表中是否包含某成员,通常使用in关键字更快。

✅ Exp14:检查列表中是否包含某成员。

测试数组:lists = ['life', 'is', 'short', 'i', 'choose', 'python']

  • 方法一
def check_member(target, lists):
    for member in lists:
        if member == target:
            return True
    return False
复制代码
  • 方法二
if target in lists:
    pass
复制代码

方法一耗时 0.16038449999999216s,方法二耗时 0.04139250000000061s,性能提升 74.19% 🚀

15.使用itertools库迭代

itertools是用来操作迭代器的一个模块,其函数主要可以分为三类:无限迭代器、有限迭代器、组合迭代器。

✅ Exp15:返回列表的全排列。

测试数组:["Alice", "Bob", "Carol"]

  • 方法一
def permutations(lst):
    if len(lst) == 1 or len(lst) == 0:
        return [lst]
    result = []
    for i in lst:
        temp_lst = lst[:]
        temp_lst.remove(i)
        temp = permutations(temp_lst)
        for j in temp:
            j.insert(0, i)
            result.append(j)
    return result
复制代码
  • 方法二
import itertools
itertools.permutations(["Alice", "Bob", "Carol"])
复制代码

方法一耗时 3.867292899999484s,方法二耗时 0.3875405000007959s,性能提升 89.98% 🚀

扩展itertools库详解:点击此链接

结语

根据上面的测试数据,我绘制了下面这张实验结果图,可以更加直观的看出不同方法带来的性能差异。

从图中可以看出,大部分的技巧所带来的性能增幅还是比较可观的,但也有少部分技巧的增幅较小(例如编号5、7、8,其中,第 8 条的两种方法几乎没有差异)。

总结下来,我觉得其实就是下面这两条原则:

1.尽量使用内置库函数

内置库函数由专业的开发人员编写并经过了多次测试,很多库函数的底层是用C语言开发的。因此,这些函数总体来说是非常高效的(比如sort()join()等),自己编写的方法很难超越它们,还不如省省功夫,不要重复造轮子了,何况你造的轮子可能更差。所以,如果函数库中已经存在该函数,就直接拿来用。

2.尽量使用优秀的第三方库

有很多优秀的第三方库,它们的底层可能是用 C 和 Fortran 来实现的,像这样的库用起来绝对不会吃亏,比如前文提到的 Numpy 和 Numba,它们带来的提升都是非常惊人的。类似这样的库还有很多,比如Cython、PyPy等,这里我只是抛砖引玉。

其实加快 Python 代码执行速度的方法还有很多,比如避免使用全局变量使用最新版本使用合适的数据结构利用if条件的惰性等等,我这里就不一一例举了。这些方法都需要我们亲身去实践才会有深刻的感受和理解,但最根本的方法就是保持我们对编程的热情和对最佳实践的追求,这才是我们能不断突破自我、勇攀高峰的不竭动力源泉!👏

❤️ 感谢大家

感谢大家能看到这里,如果你觉得这篇内容对你有帮助的话:

  1. 点赞支持下吧,让更多的人也能看到这篇内容。

  2. 欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。

  3. 感兴趣的话也可以阅读我之前的一些文章:

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
以下是100个Python技巧: 1. 使用for循环可以遍历列表、字符串等可迭代对象。 2. 使用range()函数生成指定范围的数字序列。 3. 使用enumerate()函数可以同时获得元素索引和值。 4. 使用zip()函数可以同时遍历多个可迭代对象。 5. 使用列表推导式可以快速生成列表。 6. 使用集合可以进行高效的集合操作。 7. 使用字典可以进行键值对的映射。 8. 使用切片可以快速获取列表、字符串的子序列。 9. 使用函数可以封装可复用的代码块。 10. 使用类可以实现面向对象的编程。 11. 使用模块可以组织和管理代码。 12. 使用异常处理可以捕获和处理程序的错误。 13. 使用with语句可以自动管理资源的释放。 14. 使用装饰器可以增强函数的功能。 15. 使用生成器可以按需计算大量数据。 16. 使用协程可以实现异步编程。 17. 使用多线程可以并发执行任务。 18. 使用多进程可以充分利用多核处理器。 19. 使用文件操作可以读写文件。 20. 使用正则表达式可以进行复杂的文本匹配。 21. 使用time模块可以获取当前时间和进行时间操作。 22. 使用random模块可以生成随机数。 23. 使用math模块可以进行数学运算。 24. 使用json模块可以进行JSON数据的处理。 25. 使用pickle模块可以进行对象的序列化和反序列化。 26. 使用os模块可以进行文件和目录的操作。 27. 使用sys模块可以获取和修改Python解释器的运行时环境。 28. 使用re模块可以进行正则表达式匹配。 29. 使用argparse模块可以解析命令行参数。 30. 使用logging模块可以进行日志记录。 31. 使用unittest模块可以编写和执行单元测试。 32. 使用requests库可以发送HTTP请求。 33. 使用BeautifulSoup库可以解析HTML文档。 34. 使用numpy库可以进行数组和矩阵计算。 35. 使用pandas库可以进行数据处理和分析。 36. 使用matplotlib库可以进行数据可视化。 37. 使用scikit-learn库可以进行机器学习。 38. 使用tensorflow库可以进行深度学习。 39. 使用flask库可以构建Web应用。 40. 使用Django库可以构建全功能的Web应用。 41. 使用SQLite数据库可以进行轻量级的数据存储。 42. 使用MySQL数据库可以进行关系型数据存储。 43. 使用MongoDB数据库可以进行文档型数据存储。 44. 使用Redis数据库可以进行缓存和键值存储。 45. 使用Elasticsearch可以进行搜索和分析。 46. 使用OpenCV库可以进行图像处理和计算机视觉。 47. 使用pygame库可以进行游戏开发。 48. 使用tkinter库可以进行桌面应用程序开发。 49. 使用wxPython库可以进行跨平台的GUI开发。 50. 使用Flask-RESTful可以构建RESTful API。 51. 使用Celery可以进行异步任务的调度和执行。 52. 使用pytest可以进行更简洁和灵活的单元测试。 53. 使用Selenium可以进行Web自动化测试。 54. 使用Faker可以生成随机的测试数据。 55. 使用IPython可以进行交互式的开发和调试。 56. 使用Jupyter Notebook可以进行数据分析和可视化。 57. 使用Spyder可以进行科学计算和Python开发。 58. 使用cookiecutter可以快速构建项目模板。 59. 使用virtualenv可以创建和管理Python虚拟环境。 60. 使用pip可以安装和管理Python包。 61. 使用pyenv可以管理多个Python版本。 62. 使用conda可以创建和管理Python环境。 63. 使用autopep8可以自动格式化Python代码。 64. 使用black可以自动格式化Python代码。 65. 使用flake8可以检查Python代码是否符合PEP8规范。 66. 使用bandit可以检查Python代码中的安全漏洞。 67. 使用isort可以自动排序Python导入语句。 68. 使用mypy可以进行静态类型检查。 69. 使用pylint可以进行代码质量检查。 70. 使用pyinstaller可以将Python程序打包成可执行文件。 71. 使用cx_Freeze可以将Python程序打包成可执行文件。 72. 使用py2exe可以将Python程序打包成可执行文件。 73. 使用pyodbc可以连接和操作数据库。 74. 使用paramiko可以进行SSH远程操作。 75. 使用fabric可以进行任务的自动化部署。 76. 使用pytest-django可以简化Django的单元测试。 77. 使用django-rest-framework可以快速构建RESTful API。 78. 使用scrapy可以进行Web爬虫。 79. 使用sqlalchemy可以进行高级数据库操作。 80. 使用pymongo可以连接和操作MongoDB数据库。 81. 使用tqdm可以在循环中显示进度条。 82. 使用click可以构建命令行接口。 83. 使用cProfile可以进行性能分析。 84. 使用line_profiler可以逐行分析代码性能。 85. 使用memory_profiler可以分析内存使用情况。 86. 使用profilehooks可以进行分析函数调用性能。 87. 使用PyInstaller可以将Python程序打包成可执行文件。 88. 使用Openpyxl可以读写Excel文件。 89. 使用Pillow可以进行图像处理。 90. 使用pyautogui可以进行图像识别和自动化操作。 91. 使用pytesseract可以进行文字识别。 92. 使用pywin32可以操作Windows系统API。 93. 使用pyserial可以进行串口通信。 94. 使用opencv-python可以进行图像处理。 95. 使用catboost可以进行梯度提升决策树算法。 96. 使用fasttext可以进行文本分类和词向量训练。 97. 使用gensim可以进行文本相似度计算。 98. 使用jieba可以进行中文分词。 99. 使用lightgbm可以进行梯度提升算法。 100. 使用xgboost可以进行梯度提升算法。 这些小技巧涵盖了Python的各个方面,包括语法、标准库、第三方库和常见开发任务。通过学习和应用这些技巧,可以提高Python编程的效率和质量。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一个乖乖码字的程序猪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值