Python提速秘籍:九个让你的代码飞速运行的巧妙技巧!

开发 前端
缓存是一种常用的技术,用于避免重复计算并加快程序的运行速度。幸运的是,在大多数情况下,我们不需要编写自己的缓存处理代码,因为Python提供了一个用于此目的的开箱即用的装饰器 — @functools.cache。

引言

“Python太慢了。”这种观点在编程语言的讨论中频频出现,常常使人忽视Python的众多优点。

但事实真的如此吗?与普遍看法相反,如果你掌握了Python式的编程技巧,Python其实可以像冠军选手一样快速奔跑。

在表面之下,精通Python的开发者们掌握着一系列微妙而强大的技巧,这些技巧能显著提升他们代码的性能,远超常规水平。这些不仅仅是技巧,它们甚至改变了游戏规则。

今天,我们将揭示九种变革性的策略,这些策略可以彻底改变你对Python编程的看法。这些策略乍看之下或许很简单,但它们具有强大的效力,能以你从未想象的方式提升效率。准备好给你的Python技能加速了吗?让我们深入了解并开始优化吧!

1.join 或 +:更快的字符串连接

如果你的程序中经常进行字符串操作,那么字符串连接可能会成为你的 Python 程序的瓶颈。

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

  • 使用join()函数将一系列字符串合并为一个
  • 使用+或+=符号逐一将每个字符串添加到一个中

那么哪种方法更快?废话少说,下面我们使用3种不同的方式连接相同的字符串:

str_list = ['Facts', 'speak', 'louder', 'than', 'words!']

# 使用 + 号
def concat_plus(strings):
    result = ''
    for word in strings:
        result += word + ' '
    return result

# 使用 join() 方法
def concat_join(strings):
    return ' '.join(strings)

# 直接连接
def concat_directly():
    return 'Facts' + 'speak' + 'louder' + 'than' + 'words!'

根据您那作为男士or女士神奇的第六感(🤭😜),悄悄告诉我您认为哪个函数速度最快,哪个最慢?实际结果可能会让您感到惊讶哦😛😛😛:

import timeit

print(f'The plus symbol: {timeit.timeit(concat_plus, number=10000)}')
print(f'The join function: {timeit.timeit(concat_join, number=10000)}')
print(f'The direct concatenation: {timeit.timeit(concat_directly, number=10000)}')

图片图片

如上所示,对于字符串连接,join() 方法比通过循环逐个添加字符串要快。

原因很简单。一方面,在Python中,字符串是不可变数据,每个 += 操作都会伴随新字符串变量的创建和旧字符串的复制,这会额外消耗更多的计算资源。另一方面,.join() 方法专门针对连接列表字符串进行了优化。它预先计算生成字符串的大小,然后一次性为其分配存储空间。因此,它避免了循环中的 += 操作带来的开销,因此更快。

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

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

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

2.更快的列表创建:选择“[]”而非“list()”

创建列表并不困难。两种常见的方法是:

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

print('The List Creation:')
print(f"[]: {timeit.timeit('[]', number=10 ** 7)}")
print(f'The list function: {timeit.timeit(list, number=10 ** 7)}')

图片图片

正如结果所示,直接使用 [] 比执行 list() 函数要快差不多2倍。这是因为 [] 是一种字面语法,而 list() 是一个构造函数调用。毫无疑问,调用函数需要额外的时间。相同的逻辑,在创建字典时,我们也应该利用 {} 而不是 dict()。

3.更快的成员检查:用 Set 而不用 List

成员检查操作的性能在很大程度上取决于底层数据结构,一起来看看下面这个例子:

import timeit

target_dataset = range(1000000)
search_element = 1314
target_list = list(target_dataset)
target_set = set(target_dataset)

def list_membership_test():
    return search_element in target_list

def set_membership_test():
    return search_element in target_set

print(f'The list membership test: {timeit.timeit(list_membership_test, number=1000)}')
print(f'The set membership test: {timeit.timeit(set_membership_test, number=1000)}')

图片图片

结果显示,在集合中进行成员检查比在列表中快得多。我还发现一个问题,那就是搜索的元素越靠前则耗时越短,如果搜索一个不存在的元素则耗时最长。上面我们搜索的目标元素是1314,如果我们搜索一个不存在的元素1314520,则明显耗时更多:

图片图片

因为搜索一个不存在的元素必须遍历完整个列表或集合。By the way,从这个例子可以看出要做到一生一世(1314)很容易,因为每个人生来便有,但是要做到一生一世我爱你(1314520)却并不简单,因为需要付出更多的代价。哈哈😄😄😄,开个玩笑,扯远了,权当是给您枯燥的阅读带来一点小乐趣!

回到主题,为什么成员检查用集合比列表更快呢?

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

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

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

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

一起看看下面这个生成1-10000的立方示例:

import timeit

def generate_cubes_for_loop():
    cubes = []
    for i in range(10000):
        cubes.append(i * i * i)
    return cubes

def generate_cubes_comprehension():
    return [i * i * i for i in range(10000)]

print(f'For loop: {timeit.timeit(generate_cubes_for_loop, number=10000)}')
print(f'Comprehension: {timeit.timeit(generate_cubes_comprehension, number=10000)}')

上述代码只是列表推导式和 for 循环之间的简单速度比较。正如如结果所示,列表推导式更快。

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

在Python中,访问局部变量比访问全局变量或对象属性要快。这里用一个简单例子来证明这一点:

import timeit

class Test:
    def __init__(self):
        self.value = 0
        
obj = Test()

def access_global_variable():
    for _ in range(1000):
        obj.value += 1
        
def access_local_variable():
    value = obj.value
    for _ in range(1000):
        value += 1
        
print(f'Access global variable: {timeit.timeit(access_global_variable, number=1000)}')
print(f'Access local variable: {timeit.timeit(access_local_variable, number=1000)}')

这就是 Python 的工作原理。直观地说,当函数编译时,其中的局部变量是已知的,但其他外部变量则需要时间来检索。

这只是一个很小的改良,但有时候我们缺可以利用它来优化我们的代码,特别是在处理大数据集时。

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

当工程师们说 Python 时,默认是指 CPython。因为 CPython 是 Python 语言的默认和最广泛使用的实现。

考虑到大多数内置模块和库都是用更快速和更底层的语言 C 编写的,我们应该尽可能利用这些内置工具并避免重复发明轮子。

import timeit
import random
from collections import Counter

def counter_custom(lst):
    frequency = {}
    for item in lst:
        if item in frequency:
            frequency[item] += 1
        else:
            frequency[item] = 1
    return frequency

def counter_builtin(lst):
    return Counter(lst)

target_dataset = [random.randint(0, 100) for _ in range(1000)]
print(f'Counter custom: {timeit.timeit(lambda: counter_custom(target_dataset), number=100)}')
print(f'Counter builtin: {timeit.timeit(lambda: counter_custom(target_dataset), number=100)}')

这里比较了在列表中计算元素频率的两种方法。正如我们所看到的,利用 collections 模块中的内置 Counter 比自己编写 for 循环更快,更整洁,更好(但有时候自定义的性能又会比内置模块更好,尚不知道原因)。

7.更快的函数调用:利用缓存装饰器

缓存是一种常用的技术,用于避免重复计算并加快程序的运行速度。幸运的是,在大多数情况下,我们不需要编写自己的缓存处理代码,因为Python提供了一个用于此目的的开箱即用的装饰器 — @functools.cache。

例如,以下代码将执行两个斐波那契数生成函数,一个带有缓存装饰器,而另一个没有:

import timeit
from functools import cache

def fibonacci_norm(n):
    if n <= 1:
        return n
    return fibonacci_norm(n - 1) + fibonacci_norm(n - 2)

@cache
def fibonacci_cached(n):
    if n <= 1:
        return n
    return fibonacci_cached(n - 1) + fibonacci_cached(n - 2)

print(f'fibonacci normal: {timeit.timeit(lambda: fibonacci_norm(30), number=1)}')
print(f'fibonacci cached: {timeit.timeit(lambda: fibonacci_cached(30), number=1)}')

结果显示 cache 装饰器版本比普通版本的速度快得多。

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

缓存版本明显更快,因为它缓存了先前计算的结果。因此,每个斐波那契数它只计算一次,并且使用相同参数进行的后续调用都从缓存中获取。

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

8.更快的无限循环: 优先选择“while 1”而不是“while True”

要创建一个无限循环,我们可以使用 while True 或 while 1 。它们的性能差异通常是可以忽略的。但有趣的是 while 1 稍微更快。这源于 1 是字面值,而 True 是 Python 全局范围内需要查找的全局名称,因此需要微小的额外开销。

我们也通过一个简单的示例比较这两种方式的性能:

import timeit

def infinite_loop_with_true():
    result = 0
    while True:
        if result >= 10000:
            break
        result += 1
        
def infinite_loop_with_one():
    result = 0
    while 1:
        if result >= 10000:
            break
        result += 1
        
print(f'Infinite loop with true: {timeit.timeit(infinite_loop_with_true, number=10000)}')
print(f'Infinite loop with one: {timeit.timeit(infinite_loop_with_one, number=10000)}')

正如我们所看到的,while 1 确实略快。但是,现代 Python 解释器(如CPython)经过高度优化,这样的差异通常微不足道。因此,我们无需在意这种微不足道的差异。另外,从代码可读性角度来说,其实 while True 的可读性比 while 1 更强。

9.更快的脚本启动:智能导入Python模块

通常情况下,我们都习惯在Python 脚本顶部导入所有模块。事实上,有些时候不必这样做。此外,如果模块太大,则按需导入可能会是一个更好的主意。比如,在用到模块的函数内部导入:

def target_function():
    import specific_module
    # rest of the function

如上面的代码所示,specific_module 在函数内部执行导入操作。这是“惰性加载”的思想,在函数调用时才导入指定模块。

这种方法的好处是,如果在脚本执行期间从未调用 target_function,则永远不会加载 specific_module,从而节省资源并减少脚本的启动时间。

责任编辑:武晓燕 来源: 数据派探险家
相关推荐

2024-01-08 17:09:07

Python解释器CPython

2020-07-08 17:06:00

Python开发工具

2023-12-29 14:13:41

PyTorch模型开发

2022-01-06 22:31:21

Python技巧代码

2024-01-26 06:15:44

PythonCPython技巧

2020-08-06 00:25:38

Python代码开发

2019-11-25 10:20:54

CSS代码javascript

2021-09-27 10:03:55

装饰器代码

2021-05-07 16:02:54

Python代码优化

2021-06-16 10:50:16

Python代码优化

2024-01-19 13:45:00

Pandas代码深度学习

2024-02-26 16:40:58

2020-07-03 14:50:23

Python代码编程语言

2021-07-12 07:08:54

责任链模式对象

2023-12-06 13:43:00

python代码

2021-09-06 10:25:27

Python代码优化

2014-08-11 12:54:27

构建模块代码审查编程

2019-04-29 08:31:25

PythonPandas数据

2021-02-05 16:20:54

代码Linux技巧

2020-05-07 17:03:49

Python编码开发
点赞
收藏

51CTO技术栈公众号