掌握这九个技巧,让Python代码快如闪电

开发 前端
当工程师们说到Python时,默认情况下指的是CPython。因为CPython是Python语言的默认实现,也是使用最广泛的实现。

前言

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

事实是,如果能以Pythonic的方式编写Python代码,它是很快的。

细节决定成败。经验丰富的Python开发者掌握了一系列微妙而强大的技巧,可以显著提高代码的性能。

这些技巧乍看之下似乎微不足道,但它们可以带来效率的大幅提升。让我们深入了解其中的9种方法,改变编写和优化Python代码的方式。

1. 更快的字符串连接:巧妙选择“join()”或“+”

如果有大量字符串等待处理,字符串连接将成为Python程序的瓶颈。

基本上,Python有两种字符串连接的方式:

  1. 使用join()函数将一组字符串合并为一个字符串。
  2. 使用+或+=符号将每个单独的字符串添加到一个字符串中。

那么哪种方式更快呢?

现在,让我们定义3个不同的函数来连接相同的字符串:

mylist = ["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"

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

真正的结果可能会让你惊讶:

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()方法专门针对连接一系列字符串进行了优化。它会预先计算出所生成字符串的大小,然后一次性创建它。因此,它避免了循环中+=操作带来的开销,从而使速度更快。

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

  • Python解释器可以在编译时优化字符串文字的连接,将它们转换为一个单独的字符串文字。这里不涉及循环迭代或函数调用,因此是一种非常高效的操作。
  • 由于所有字符串在编译时都是已知的,Python可以非常快速地执行此操作,比在循环中进行的运行时连接或经过优化的.join()方法要快得多。

总之,如果需要连接一组字符串,请选择join()而不是+=。如果想要直接连接字符串,只需使用+即可。

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

创建列表并不是很难的事情。常见的两种方式是:

  1. 使用list()函数。
  2. 直接使用[]。

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

import timeit

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

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

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

从同样的逻辑出发,在创建字典时,我们也应该使用{}而不是dict()。

3. 更快的成员测试:使用集合而不是列表

成员测试操作的性能在很大程度上依赖于底层数据结构:

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

正如上述代码所示,使用集合进行成员测试比使用列表更快。

为什么会这样呢?

  • 在Python的列表中,成员测试(列表中的元素)是通过迭代每个元素直到找到所需的元素或达到列表的末尾来完成。因此,此操作的时间复杂度为O(n)。
  • Python中的集合实现形式为哈希表。在进行成员检查(集合中的元素)时,Python使用哈希机制,其平均时间复杂度为O(1)。

这里的关键在于:在编写程序时要仔细考虑底层数据结构。正确利用合适的数据结构可以显著加快代码的运行速度。

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

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

5. 更快的循环:优先使用局部变量

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

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

6. 更快的执行速度:优先使用内置模块和库

当工程师们说到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模块中内置的Counter函数比自己编写的for循环更快、更简洁、更好。

7. 更快的函数调用:利用缓存装饰器轻松实现记忆化

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

幸运的是,在大多数情况下,我们不需要自己编写缓存处理代码,因为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)


# 测试每个函数的执行时间
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函数效率较低,因为在计算fibonacci(30)结果的过程中,它会多次重新计算相同的斐波那契数。

而使用缓存的版本要快得多,因为它缓存了之前的计算结果。因此,它只计算每个斐波那契数一次,并且对于相同的参数再次调用时会从缓存中获取结果。

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

8. 更快的无限循环:优先使用"while 1"而不是"while True"

要创建一个无限的while循环,我们可以使用while True或while 1。

它们的性能差异通常可以忽略不计。但是,了解while 1稍微快一些还是很有趣的。

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

让我们在代码片段中进一步比较这两种方式的真实性能:

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更易读了。

9. 更快的启动:巧妙地导入Python模块

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

实际上,我们并不需要这样做。

而且,如果一个模块太大,在需要时导入它是一个更好的选择。

def my_function():
    import heavy_module
    # 函数的其余部分

以上代码中,heavy_module是在函数内部导入的。这是一种“延迟加载”的思想,即延迟到在调用my_function时才进行导入。

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

结语

综上所述,就是9个优化Python代码性能的实用技巧,但在实际应用时需要根据具体情况进行权衡和调整。通过综合考虑代码的性能、可读性和可维护性,进而编写出高效且易于理解的Python代码。

责任编辑:武晓燕 来源: Python学研大本营
相关推荐

2019-09-09 16:30:42

Redis架构数据库

2019-12-25 14:19:21

Python编程语言Java

2017-08-14 10:52:17

小米MIUIMIUI9

2023-08-11 07:20:04

开源工具项目

2020-05-21 21:36:54

Windows 10Windows 7Windows

2016-12-07 08:36:58

2024-01-08 17:09:07

Python解释器CPython

2023-12-06 12:52:00

Python

2020-07-07 14:35:41

Python数据分析命令

2017-08-29 16:25:21

数据库GPU数据存储

2022-03-03 09:10:24

css隐藏滚动条前端开发

2019-09-10 11:31:16

Python数据分析表达式

2023-12-29 14:13:41

PyTorch模型开发

2023-07-26 07:51:30

CSSgap 属性

2022-01-06 22:31:21

Python技巧代码

2022-03-26 19:25:40

Python包Python开发

2022-04-01 15:17:05

Java开发技巧

2012-10-17 11:15:30

2018-07-17 10:58:33

Python编程技巧

2020-08-06 00:25:38

Python代码开发
点赞
收藏

51CTO技术栈公众号