在最近参加的一些技术会议上,我常常听到参会员在会中讨论技术选型时提到“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 True
比while 1
更为直观。
9、加快Python脚本启动:按需导入模块
虽然在Python脚本顶部导入所有需要的模块看起来是一种自然的做法,但实际上我们并不必须这样做。
特别是对于体积较大的模块,根据实际的使用需求来导入它们会是一个更好的选择。
def my_function ():
import Heavy_module
# 函数的其余部分
如上述代码所示,heavy_module
模块被导入到了函数内部。这体现了“延迟加载”的概念,即将导入操作延迟到my_function
函数被实际调用的时候。
这种做法的优点在于,如果在脚本的执行过程中my_function
函数从未被调用,那么heavy_module
模块将不会被加载。这样不仅节省了资源,还缩短了脚本的启动时间。