“Python太慢了。”

这种观点在关于编程语言的讨论中经常出现,常常掩盖了Python的诸多优点。

事实上,如果你能以Pythonic的方式编写代码,Python其实是快速的。

关键在于细节。经验丰富的Python开发者掌握着一系列微妙而强大的技巧,以显著提升他们代码的性能。

这些技巧乍一看可能看起来微不足道,但它们可以带来效率上的实质性改进。让我们深入探讨这9种方法,转变你编写和优化Python代码的方式。

1、更快的字符串连接:技巧地选择“join()”或“+” 如果有大量字符串等待处理,字符串连接将成为你Python程序的瓶颈。

Python中有两种字符串连接方式:

  • 使用函数将字符串列表组合成一个join()
  • 使用++=符号将每个单独的字符串加成一个

那么哪种方式更快呢?

说起来容易,让我们定义3个不同的函数来连接相同的字符串:

pythonCopy codemylist = ["Yang", "Zhou", "is", "writing"]``   ``# 使用 '+'``def concat_plus():`    `result = ""`    `for word in mylist:`        `result += word + " "`    `return result``   ``# 使用 'join()'``def concat_join():`    `return " ".join(mylist)``   ``# 直接连接,不使用列表``def concat_directly():`    `return "Yang" + "Zhou" + "is" + "writing"

根据你的第一印象,你认为哪个函数最快,哪个最慢?

真实结果可能会让你感到惊讶:

pythonCopy codeimport 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()方法比在循环中一个接一个添加字符串的方式更快。

原因很简单。一方面,字符串在Python中是不可变数据,每次操作都会创建一个新字符串并复制旧字符串,这在计算上是昂贵的。另一方面,join()方法针对连接字符串序列进行了特别优化。它预先计算结果字符串的大小,然后一次性构建它。因此,它避免了在循环中使用+=操作的开销,所以更快。

然而,在我们的测试中最快的函数是直接连接字符串字面量。它的高速度归因于:

  • Python解释器可以在编译时优化字符串字面量的连接,将它们转换成单个字符串字面量。没有循环迭代或函数调用涉及,使其成为非常高效的操作。
  • 由于所有字符串在编译时都是已知的,Python可以非常快速地执行此操作,比循环中的运行时连接甚至优化的join()方法都要快。

总之,如果你需要连接字符串列表,选择join()而不是+=。如果你想直接连接字符串,只需使用+来完成。

2、更快的列表创建:使用“[]”而不是“list()”

创建列表并不是什么大问题。两种常见的方式是:

  • 使用list()函数
  • 直接使用[]

让我们使用一个简单的代码片段来测试它们的性能:

pythonCopy codeimport timeit``   ``print(timeit.timeit('[]', number=10 ** 7))``# 0.1368238340364769``print(timeit.timeit(list, number=10 ** 7))``# 0.2958830420393497

如结果所示,执行list()函数比直接使用[]慢。

这是因为[]是一个字面量语法,而list()是一个构造器调用。毫无疑问,调用函数需要额外的时间。

同样的逻辑,当创建字典时,我们也应该优先使用{}而不是dict()

3、更快的成员检测:使用集合而set不是列表list

成员检测操作的性能在很大程度上取决于底层数据结构:

pythonCopy codeimport 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

如上述代码所示,集合中的成员测试比列表中快得多。

为什么会这样呢?

在Python列表中,成员检测(element in list)是通过迭代每个元素直到找到所需元素或到达列表末尾来完成的。因此,这种操作的时间复杂度为O(n)。

Python中的集合是作为哈希表实现的。检查成员时(element in set),Python使用哈希机制,其时间复杂度平均为O(1)。

这里的重点是,在编写程序时要仔细考虑底层数据结构。利用正确的数据结构可以显著加快我们代码的速度。

4、更快的数据生成:使用推导式而不是For循环

Python中有四种类型的推导式:列表(list)、字典(dictionary)、集合(set)和生成器(generator)。它们不仅提供了一种更简洁的语法来创建相关数据结构,而且比使用循环的性能更好。因为它们在Python的C实现中进行了优化。

pythonCopy codeimport 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循环之间的一个简单速度比较。结果显示,列表推导式更快。

5、更快的循环:优先考虑局部变量

在Python中,访问局部变量比访问全局变量或对象的属性更快。

这是一个证明这一点的例子:

pythonCopy codeimport 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的工作方式。直观地说,当一个函数被编译时,它内部的局部变量是已知的,但其他外部变量需要时间来检索。

这是一个小问题,但当处理大量数据时,我们可以利用它来优化我们的代码。

6、更快的执行:优先选择内置模块和库

当工程师说起Python时,默认指的是CPython。因为CPython是Python语言最常用的实现方式。

考虑到大多数内置模块和库都是用C语言编写的,C语言是一种更快更低级的语言,我们应该利用内置的工具库,避免重复造轮子。

pythonCopy codeimport 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模块中的Counter内置函数更快、更整洁、更好,而不是自己编写for循环。

7、更快的函数调用:利用缓存装饰器进行简易的记忆化

缓存是一种常用的技术,用于避免重复计算并加速程序。

幸运的是,大多数情况下我们不需要编写自己的缓存处理代码,因为Python提供了一个开箱即用的装饰器来实现这一目的——@functools.cache

例如,下面的代码将执行两个斐波那契数生成函数,一个使用了缓存装饰器,另一个没有:

pythonCopy codeimport 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)``   ``# 测试每个函数的执行时间``print(timeit.timeit(lambda: fibonacci(30), number=1))``# 0.09499712497927248``print(timeit.timeit(lambda: fibonacci_cached(30), number=1))``# 6.458023563027382e-06

结果证明了functools.cache装饰器如何使我们的代码更快。

基本函数效率低下,因为在获取fibonacci(30)的结果过程中,它多次重复计算相同的斐波那契数。

缓存版本显著更快,因为它缓存了之前计算的结果。因此,它只计算每个斐波那契数一次,随后带有相同参数的调用从缓存中检索。

仅仅添加一个内置装饰器就可以带来如此巨大的改进,这就是所谓的Pythonic。😎

8、更快的无限循环:优先使用“while 1”而不是“while True”

要制造一个无限的while循环,我们可以使用while Truewhile 1

它们之间性能的差异通常可以忽略不计。但了解while 1稍微更快总是有趣的。

这源于1是字面量,而True是一个需要在Python的全局作用域中查找的全局名称,因此需要一点点额外的开销。

让我们也在代码片段中检查这两种方式的真实比较:

pythonCopy codeimport 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 Truewhile 1更具可读性。

9、更快的启动:智能地导入Python模块

在Python脚本的顶部导入所有模块似乎是自然的。

实际上,我们不必那样做。

此外,如果一个模块太大,根据需要导入它是一个更好的主意。

pythonCopy codedef my_function():`    `import heavy_module`    `# 函数的其余部分

如上代码所示,heavy_module是在一个函数内部导入的。这是“懒加载”的一个想法,其中导入被推迟到my_function被调用时。

这种方法的好处是,如果在我们脚本的执行过程中从未调用my_function,那么heavy_module就永远不会被加载,从而节省资源并减少我们脚本的启动时间。

以上就是“9 个微妙的技巧,让你的 Python 代码更快”的全部内容,希望对你有所帮助。