哈喽大家好,我是咸鱼
我们经常听到 “Python 太慢了”,“Python 性能不行”这样的观点。但是,只要掌握一些编程技巧,就能大幅提升 Python 的运行速度。
今天就让我们一起来看下让 Python 性能更高的 9 个小技巧
原文链接:
https://medium.com/techtofreedom/9-fabulous-python-tricks-that-make-your-code-more-elegant-bf01a6294908
如果有大量字符串等待处理,字符串连接将成为 Python 的瓶颈。
一般来讲,Python 中有两种字符串拼接方式:
join()
函数将字符串列表合并为一个字符串+
or +=
符号将每个字符串加成一个那么哪种方式更快呢?我们一起来看一下
mylist = ["Yang", "Zhou", "is", "writing"] # Using '+' def concat_plus(): result = "" for word in mylist: result += word + " " return result # Using 'join()' def concat_join(): return " ".join(mylist) # Directly concatenation without the list def concat_directly(): return "Yang" + "Zhou" + "is" + "writing"
import timeit print(timeit.timeit(concat_plus, number=10000)) # 0.002738415962085128 print(timeit.timeit(concat_join, number=10000)) # 0.0008482920238748193 print(timeit.timeit(concat_directly, number=10000)) # 0.00021425005979835987
如上所示,对于拼接字符串列表, join()
方法比在 for 循环中逐个添加字符串更快。
原因很简单。一方面,字符串是 Python 中的不可变数据,每个 +=
操作都会导致创建一个新字符串并复制旧字符串,这会导致非常大的开销。
另一方面,.join()
方法是专门为连接字符串序列而优化的。它预先计算结果字符串的大小,然后一次性构建它。因此,它避免了与循环中 +=
操作相关的开销,因此速度更快。
但是,我们发现最快其实是直接用 +
拼接字符串,这是因为:
.join()
方法快得多。总之,如果需要拼接字符串列表,请选择 join()
;如果直接拼接字符串,只需使用 +
即可。
Python 中创建列表的两种常见方法是:
list()
[]
直接使用我们来看下这两种方法的性能
import timeit print(timeit.timeit('[]', number=10 ** 7)) # 0.1368238340364769 print(timeit.timeit(list, number=10 ** 7)) # 0.2958830420393497
结果表明,执行 list()
函数比直接使用 []
要慢。
这是因为 是 []
字面语法( literal syntax ),而 list()
是构造函数调用。毫无疑问,调用函数需要额外的时间。
同理,在创建字典时,我们也应该利用 {}
而不是 dict()
成员关系测试的性能很大程度上取决于底层数据结构
import timeit large_dataset = range(100000) search_element = 2077 large_list = list(large_dataset) large_set = set(large_dataset) def list_membership_test(): return search_element in large_list def set_membership_test(): return search_element in large_set print(timeit.timeit(list_membership_test, number=1000)) # 0.01112208398990333 print(timeit.timeit(set_membership_test, number=1000)) # 3.27499583363533e-05
如上面的代码所示,集合中的成员关系测试比列表中的成员关系测试要快得多。
这是为什么呢?
element in list
) 是通过遍历每个元素来完成的,直到找到所需的元素或到达列表的末尾。因此,此操作的时间复杂度为 O(n)。element in set
) 时,Python 使用哈希机制,其时间复杂度平均为 O(1)。这里的技巧重点是在编写程序时仔细考虑底层数据结构。利用正确的数据结构可以显著加快我们的代码速度。
Python 中有四种类型的推导式:列表、字典、集合和生成器。它们不仅为创建相对数据结构提供了更简洁的语法,而且比使用 for 循环具有更好的性能。
因为它们在 Python 的 C 实现中进行了优化。
import timeit def generate_squares_for_loop(): squares = [] for i in range(1000): squares.append(i * i) return squares def generate_squares_comprehension(): return [i * i for i in range(1000)] print(timeit.timeit(generate_squares_for_loop, number=10000)) # 0.2797503340989351 print(timeit.timeit(generate_squares_comprehension, number=10000)) # 0.2364629579242319
上面的代码是列表推导式和 for 循环之间的简单速度比较。如结果所示,列表推导式速度更快。
在 Python 中,访问局部变量比访问全局变量或对象的属性更快。
import timeit class Example: def __init__(self): self.value = 0 obj = Example() def test_dot_notation(): for _ in range(1000): obj.value += 1 def test_local_variable(): value = obj.value for _ in range(1000): value += 1 obj.value = value print(timeit.timeit(test_dot_notation, number=1000)) # 0.036605041939765215 print(timeit.timeit(test_local_variable, number=1000)) # 0.024470250005833805
原理也很简单:当编译一个函数时,它内部的局部变量是已知的,但其他外部变量需要时间来检索。
当我们讨论 Python 的时候,通常指的是 CPython,因为 CPython 是 Python 语言的默认和使用最广泛的实现。
考虑到它的大多数内置模块和库都是用C语言编写的,C语言是一种更快、更低级的语言,我们应该利用它的内置库,避免重复造*。
import timeit import random from collections import Counter def count_frequency_custom(lst): frequency = {} for item in lst: if item in frequency: frequency[item] += 1 else: frequency[item] = 1 return frequency def count_frequency_builtin(lst): return Counter(lst) large_list = [random.randint(0, 100) for _ in range(1000)] print(timeit.timeit(lambda: count_frequency_custom(large_list), number=100)) # 0.005160166998393834 print(timeit.timeit(lambda: count_frequency_builtin(large_list), number=100)) # 0.002444291952997446
上面的程序比较了计算列表中元素频率的两种方法。正如我们所看到的,利用 collections
模块的内置计数器比我们自己编写 for
循环更快、更简洁、更好。
缓存是避免重复计算和提高程序速度的常用技术。
幸运的是,在大多数情况下,我们不需要编写自己的缓存处理代码,因为 Python 提供了一个开箱即用的装饰器 — @functools.cache
。
例如,以下代码将执行两个斐波那契数生成函数,一个具有缓存装饰器,但另一个没有:
import timeit import functools def fibonacci(n): if n in (0, 1): return n return fibonacci(n - 1) + fibonacci(n - 2) @functools.cache def fibonacci_cached(n): if n in (0, 1): return n return fibonacci_cached(n - 1) + fibonacci_cached(n - 2) # Test the execution time of each function print(timeit.timeit(lambda: fibonacci(30), number=1)) # 0.09499712497927248 print(timeit.timeit(lambda: fibonacci_cached(30), number=1)) # 6.458023563027382e-06
可以看到 functools.cache
装饰器如何使我们的代码运行得更快。
缓存版本的速度明显更快,因为它缓存了先前计算的结果。因此,它只计算每个斐波那契数一次,并从缓存中检索具有相同参数的后续调用
如果要创建无限 while 循环,我们可以使用 while True
or while 1
.
它们的性能差异通常可以忽略不计。但有趣的是, while 1
稍微快一点。
这是因为是 1 字面量,但 True 是一个全局名称,需要在 Python 的全局作用域中查找。所以 1 的开销很小。
import timeit def loop_with_true(): i = 0 while True: if i >= 1000: break i += 1 def loop_with_one(): i = 0 while 1: if i >= 1000: break i += 1 print(timeit.timeit(loop_with_true, number=10000)) # 0.1733035419601947 print(timeit.timeit(loop_with_one, number=10000)) # 0.16412191605195403
正如我们所看到的,确实 while 1
稍微快一些。
然而,现代 Python 解释器(如 CPython )是高度优化的,这种差异通常是微不足道的。所以我们不需要担心这个可以忽略不计的差异。更不用说 while True
比 while 1
可读性更好。
在 Python 脚本开头导入所有模块似乎是每个人都会这么做的操作,事实上我们没有必要导入全部的模块。如果模块太大,则根据需要导入它是一个更好的主意。
def my_function(): import heavy_module # rest of the function
如上面的代码所示,heavy_module
在函数中导入。这是一种“延迟加载”的思想:只有 my_function
被调用的时候该模块才会被导入。
这种方法的好处是,如果 my_function
在脚本执行期间从未调用过,则 heavy_module
永远不会加载,从而节省资源并减少脚本的启动时间。