1. 引言
只需简单搜索一下,就很容易获得许多试图告诉我们关于 Python 技巧的文章。这些技巧要么更 “Pythonic”,要么能让我们的程序更快。这些文章并没有错,因为大多数技巧都非常有用。事实上,我自己也写过很多这类文章。
然而,这类文章经常受到批判,因为没有适用于所有情况的技巧。这也是事实。在我看来,更重要的是了解这些技巧存在的背后的原因,这样我们才能明白什么时候该用,什么时候不该用。
在这篇文章中,我将选取其中常见的两种技巧,并对其背后的机制进行详细解释。
2. 更快地连接字符串
通常如何将字符串连接在一起?我们来看一个字符串列表,如下:
strs = ['Life', 'is', 'short,', 'I', 'use', 'Python']
当然,最直观的方法是循环列表,并使用 + 运算符连接所有带空格的子串。
在上述代码中,我们只需定义一个空字符串,然后在该字符串上不断添加空格和列表中的子字符串。最后,我们返回从第 2 个字符开始的结果字符串,这样前导的空格就会被忽略。
不过,在这种情况下,我们有一个更好的方法来实现它。那就是使用 join() 函数,如下所示。
它不仅可读性更强,而且速度更快。请看下面的性能对比。
3. 原因分析
接下来我们来分析二者的具体执行步骤,首先来分析第一种方案:
● 每个for循环都会在列表中查找字符串
● Python 执行器遇到表达式result += ' ' + s
时,会并为空格' '
申请一个内存地址。
● 然后,执行器意识到需要将空格与字符串连接起来,因此它将申请字符串s
的内存地址,即第一个循环的"Life"
。
● 对于每次循环,执行器都需要申请两次内存地址,一次是空格,另一次是字符串
● 共有12
次内存分配
接下来,让我们看看使用 join()
函数的步骤。
● 执行器将计算列表中有多少个字符串。共有
6
个。
● 这意味着用于字符串列表中的连接字符串需要重复6-1=5
次。
● 它知道总共需要11
个内存空间,因此所有这些空间都将一次性预先分配。
● 按顺序排列字符串,并返回结果。
因此,显而易见,内存分配次数是上述技巧性能提升的主要原因。
4. 使用集合而非列表
List
在 Python
中非常常用。但是,你有没有想过,我们是否应该使用其他东西呢?我们知道 Python
中的 列表List
的元素是有顺序的,但如果顺序并不重要呢?例如,有时我们只需要知道一个元素是否存在,此时Python
中的集合 Set
就能满足我们的要求。在这种情况下,集合set的性能会比列表list
好得多。
让我们定义一个具有完全相同元素的列表和集合。
my_list = list(range(100))
my_set = set(range(100))
如上,二者都有从 0
到 99
共 100
个整数。唯一不同的是数据结构类型。现在,假设我们要检查数字 "99 "
是否在容器中,并比较其性能。
可见,列表的性能会因需要扫描的范围不同而变化。在上面的示例中,如果我们要测试列表中是否有 "0"
,那么其性能将与集合相同。如果我们测试的是位于中间的 "50"
,性能就会变差。如果我们测试的是最后一个元素,则会比其他情况慢得多。另一方面,集合的性能往往相当稳定。无论我们测试哪个数字,其性能都不会有太大的变化。
5. 原因分析
简而言之,列表和集合的数据结构完全不同。Python
中 Set
使用哈希表实现,而 Python
中List
是典型的数组。
图1: 建立哈希表
哈希表是什么样的?一般来说,数值会被送入一个哈希函数,这个哈希函数会将原始数值转换成另一个数值,而这个新的数值会被用作哈希表的索引。
图2: 验证 "值 3 "是否存在
现在,让我们来看看 Python中的 List,它是一种数组类型的数据结构。列表中的元素以固定的顺序链接在一起。
图3: Python 列表中的值
因此,当我们寻找某个值时,它必须从列表的开头开始,逐个元素检查,直到找到目标时(如果不匹配,则到达结尾)。
图4: Python 列表查找过程
因此,最幸运的情况是第一个列表中第一个元素就是我们要找的,结果会立即返回。最糟糕的情况是,我们要找的元素是列表中的最后一项。这也解释了为什么当我们试图在列表中查找 0、50 和 99 时,性能会有所不同。
6. 总结
在本文中,我挑选了两种常见的 Python 技巧,并尽力解释了其背后的原因。它为什么有效,为什么性能更好。这也将帮助我们决定何时使用这些技巧,您学废了嘛?