让你的 Python 代码更快的 9 个小技巧

"Python太慢了"。

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

事实上,如果你能以 Python 的方式编写 Python,那么 Python 是很快的。

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

这些技巧乍看之下可能不起眼,但却能大幅提高效率。让我们深入了解其中的 9 种方法,改变您编写和优化 Python 代码的方式。

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

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

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

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

那么哪种方式更快呢?

废话不多说,让我们定义 3 个不同的函数来连接相同的字符串:

python 复制代码
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"

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

真正的结果可能会让你大吃一惊:

python 复制代码
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()

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

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

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

python 复制代码
import timeit

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

print(timeit.timeit(list, number=10 ** 7))
# 0.2958830420393497

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

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

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

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

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

python 复制代码
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 语言实现中进行了优化。

python 复制代码
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 中,访问局部变量比访问全局变量或对象的属性更快。

下面是一个例子来证明这一点:

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 语言是一种更快、更低级的语言,因此我们应该利用其内置的武器库,避免重复造轮子。

python 复制代码
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

上面的程序比较了两种计算列表中元素频率的方法。我们可以看到,利用集合模块内置的 Counter 比自己编写 for 循环更快、更整洁、更好。

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

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

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

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

python 复制代码
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 装饰器如何使我们的代码变得更快。

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

缓存版本则快得多,因为它缓存了之前的计算结果。因此,它只对每个斐波那契数字计算一次,随后的调用会从缓存中获取相同的参数。

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

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

要实现无限 while 循环,我们可以使用 while Truewhile 1

它们的性能差异通常可以忽略不计。不过,while 1 的速度要稍快一些,这一点还是很有趣的。

这是因为 1 是字面意思,而 True 是一个全局名称,需要在 Python 的全局作用域中查找,因此需要的开销很小。

让我们在代码片段中看看这两种方法的实际对比:

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 Truewhile 1 更可读。

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

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

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

此外,如果一个模块太大,在需要时导入会更好。

python 复制代码
def my_function():
    import heavy_module
    # rest of the function

在上面的代码中,heavy_module 是在函数内部导入的。这是一种 "懒加载 "的概念,即在调用 my_function 时才导入。

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

原文链接:9 Subtle Tricks To Make Your Python Code Much Faster

相关推荐
Estar.Lee1 小时前
查手机号归属地免费API接口教程
android·网络·后端·网络协议·tcp/ip·oneapi
湫ccc2 小时前
《Python基础》之字符串格式化输出
开发语言·python
mqiqe2 小时前
Python MySQL通过Binlog 获取变更记录 恢复数据
开发语言·python·mysql
AttackingLin2 小时前
2024强网杯--babyheap house of apple2解法
linux·开发语言·python
2401_857610033 小时前
SpringBoot社团管理:安全与维护
spring boot·后端·安全
哭泣的眼泪4083 小时前
解析粗糙度仪在工业制造及材料科学和建筑工程领域的重要性
python·算法·django·virtualenv·pygame
凌冰_3 小时前
IDEA2023 SpringBoot整合MyBatis(三)
spring boot·后端·mybatis
码农飞飞3 小时前
深入理解Rust的模式匹配
开发语言·后端·rust·模式匹配·解构·结构体和枚举
一个小坑货3 小时前
Rust 的简介
开发语言·后端·rust
湫ccc3 小时前
《Python基础》之基本数据类型
开发语言·python