大家好,在使用Python解决问题时很多人觉得处理时间太长、速度太慢,这种观点在关于编程语言的讨论中经常出现,经常掩盖了Python的众多优点。经验丰富的Python开发者掌握一系列微妙而强大的技巧,可以显著提高代码的性能。
这些技巧乍看之下似乎微不足道,但它们可以带来效率的大幅提升。本文将介绍其中的9种方法,改变编写和优化Python代码的方式。
join()
”或“+
”如果有大量字符串等待处理,字符串连接将成为Python程序的瓶颈。
基本上,Python有两种字符串连接的方式:
使用join()
函数将一组字符串合并为一个字符串。
使用+
或+=
符号将每个单独的字符串添加到一个字符串中。
为探究哪种方式更快,定义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()
而不是+=
。如果想要直接连接字符串,只需使用+
即可。
[]
”而不是“list()
”创建列表并不是很难的事情。常见的两种方式是:
使用list()
函数。
直接使用[]
。
使用一个简单的代码片段来测试它们的性能:
import?timeit
print(timeit.timeit('[]',?number=10?**?7))
#?0.1368238340364769
print(timeit.timeit(list,?number=10?**?7))
#?0.2958830420393497
结果显示,执行list()
函数比直接使用[]
要慢。
这是因为[]
是一种字面量语法,而list()
是一个构造函数调用,调用函数无疑需要额外的时间。
从同样的逻辑出发,在创建字典时应该使用{}
而不是dict()
。
成员测试操作的性能在很大程度上依赖于底层数据结构:
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)。
这里的关键在于,在编写程序时要仔细考虑底层数据结构,正确利用合适的数据结构可以显著加快代码的运行速度。
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
循环之间的简单速度比较。结果显示,列表推导式更快。
在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的工作原理。直观地说,当一个函数被编译时,其中的局部变量是已知的,但其他外部变量需要时间来检索。
这可能是一个小问题,但是当处理大量数据时,可以利用它来优化我们的代码。
当工程师们说到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
循环更快、更简洁、更好。
缓存是一种常用的技术,用于避免重复计算并加快程序的运行速度。
在大多数情况下,我们不需要自己编写缓存处理代码,因为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的意义所在。
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
更易读。
在Python脚本的顶部导入所有模块似乎是很自然的,实际上并不需要这样做。
而且,如果一个模块太大,在需要时导入它是一个更好的选择。
def?my_function():
????import?heavy_module
????#?函数的其余部分
以上代码中,heavy_module
是在函数内部导入的。这是一种“延迟加载”的思想,即延迟到在调用my_function
时才进行导入。
这种方法的好处是,如果在执行脚本的过程中从未调用过my_function
,那么heavy_module
就不会被加载,从而节省资源并减少脚本的启动时间。
综上所述就是本文介绍的9个优化Python代码性能的实用技巧,但在实际应用时需要根据具体情况进行权衡和调整。通过综合考虑代码的性能、可读性和可维护性,进而编写出高效且易于理解的Python代码。