让 Python 代码效率更高的 9 个编程技巧

3,240 阅读10分钟

在最近参加的一些技术会议上,我常常听到参会员在会中讨论技术选型时提到“Python太慢了”。然而,这种观点往往没有考虑到Python的众多优点。实际上,如果能够遵循Pythonic的编程风格,Python的运行速度可以非常快。这其中的关键在于掌握一些技术细节上的巧妙技巧。那些经验丰富的Python开发者通常掌握着许多既微妙又强大的方法来提升代码性能。这些技巧虽然看似简单,但实际上能够显著提高编程效率。接下来,我们将深入讨论九种可以改变你编写和优化Python代码方式的方法。

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

在Python程序中,如果需要处理大量字符串,字符串拼接的效率会成为关键因素。在Python里,主要有两种进行字符串拼接的方法:

  • 利用join()函数,将一个字符串列表合并成一个单一的字符串。
  • 使用++=运算符,逐一将单独的字符串添加到已有的字符串中。

那么,哪一种方法的效率更高呢?为了验证这一点,我们可以定义三个不同的函数来实现相同的字符串拼接任务:

mylist = ["Yang", "Zhou", "is", "writing"]"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()方法针对连接字符串序列进行了专门优化。它会预先计算结果字符串的总大小,然后一次性构建最终的字符串。这样,它就避免了循环中+=操作所带来的额外开销,从而运行更快。

然而,在我们的测试中,直接连接字符串字面量的方法表现得最快。这是因为:

  • Python解释器能够在编译时优化字符串字面量的连接,将它们合并为单个字符串字面量。这个过程不涉及循环迭代或函数调用,因此非常高效。
  • 由于所有字符串在编译时都已确定,Python能够非常迅速地执行这一操作,其速度远超循环中的实时拼接,甚至超过了优化过的.join()方法。

总结来说,如果你需要拼接一系列字符串,建议选择join()而不是+=。而如果你的目标是直接连接固定的字符串,简单使用+即可完成任务。

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

在创建列表时,有两种常见的方法:

  • 使用list()函数。
  • 直接使用[]字面量。

通过一个简单的代码测试,我们可以比较这两种方法的性能。

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的列表中,成员测试(即element in list)是通过逐个遍历元素,直到找到目标元素或到达列表末尾来实现的。因此,这一操作的时间复杂度为O(n)。

而Python中的集合则是基于哈希表实现的。进行成员检查(element in set)时,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,这是Python语言的默认实现,也是应用最广泛的版本。

鉴于大多数Python的内置模块和库都是用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)

# 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 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的执行确实略微快于while True

然而,在现代Python解释器(例如CPython)中,由于经过了高度优化,这种差异通常非常微小,几乎可以忽略不计。因此,在实际编程中,我们不需要过分关注这种微小的差异。而且从代码可读性的角度考虑,while Truewhile 1更为直观。

9、加快Python脚本启动:按需导入模块

虽然在Python脚本顶部导入所有需要的模块看起来是一种自然的做法,但实际上我们并不必须这样做。

特别是对于体积较大的模块,根据实际的使用需求来导入它们会是一个更好的选择。

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

如上述代码所示,heavy_module模块被导入到了函数内部。这体现了“延迟加载”的概念,即将导入操作延迟到my_function函数被实际调用的时候。

这种做法的优点在于,如果在脚本的执行过程中my_function函数从未被调用,那么heavy_module模块将不会被加载。这样不仅节省了资源,还缩短了脚本的启动时间。