惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

P
Proofpoint News Feed
L
LangChain Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
云风的 BLOG
云风的 BLOG
月光博客
月光博客
F
Full Disclosure
G
Google Developers Blog
MongoDB | Blog
MongoDB | Blog
T
Tailwind CSS Blog
F
Fortinet All Blogs
A
About on SuperTechFans
Stack Overflow Blog
Stack Overflow Blog
J
Java Code Geeks
Microsoft Azure Blog
Microsoft Azure Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
Recorded Future
Recorded Future
Y
Y Combinator Blog
博客园 - 聂微东
爱范儿
爱范儿
D
DataBreaches.Net
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
Threat Research - Cisco Blogs
L
Lohrmann on Cybersecurity
The Hacker News
The Hacker News
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Scott Helme
Scott Helme
L
LINUX DO - 热门话题
Apple Machine Learning Research
Apple Machine Learning Research
C
CERT Recently Published Vulnerability Notes
B
Blog RSS Feed
The Last Watchdog
The Last Watchdog
SecWiki News
SecWiki News
Webroot Blog
Webroot Blog
Engineering at Meta
Engineering at Meta
T
Tenable Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
O
OpenAI News
Google DeepMind News
Google DeepMind News
Security Archives - TechRepublic
Security Archives - TechRepublic
W
WeLiveSecurity
Hacker News: Ask HN
Hacker News: Ask HN
Hacker News - Newest:
Hacker News - Newest: "LLM"
T
Troy Hunt's Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
aimingoo的专栏
aimingoo的专栏
GbyAI
GbyAI
V
Vulnerabilities – Threatpost
N
News and Events Feed by Topic

Xudong's Blog

AI杀死了个人博客 让MDC在各种线程间穿梭自如 新手选购微单的策略 HTTP报文结构 Python日志库logging总结 写定向爬虫时遇到的问题 Java中的协变与逆变 给Hexo主题添加LaTeX公式支持 Python3中的Iterator与Iterable Rss订阅源分享 Git Tips Redis的数据库与持久化 Redis中的数据结构 控制反转与依赖注入 Vim命令笔记 Java中的equals和hashCode方法 Java中对象域的初始化 Java中的基本类型和自动装拆箱 Hello, world. 使用Hexo搭建静态博客
一些让Python代码更快的技巧
Xudong Sun · 2021-11-08 · via Xudong's Blog

更快的字符串拼接

在你的Python程序中,如果有大量字符串等待处理,字符串拼接可能会成为一个瓶颈。基本上,Python有两种字符串拼接的方式:

  1. 使用join()函数将字符串列表合并为一个字符串。

  2. 使用++=符号将每个单独的字符串添加到一个字符串中。

那么哪一种方式更快呢?
废话少说,让我们为拼接相同字符串定义三个不同的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
mylist = ["Yang", "Zhou", "is", "writing"]


def concat_plus():
result = ""
for word in mylist:
result += word + " "
return result


def concat_join():
return " ".join(mylist)


def concat_directly():
return "Yang" + "Zhou" + "is" + "writing"

根据你的第一印象,你认为哪个函数是最快的,哪个是最慢的呢?
真实的结果可能会让你惊讶:

1
2
3
4
5
6
7
8
import timeit

print(timeit.timeit(concat_plus, number=10000))

print(timeit.timeit(concat_join, number=10000))

print(timeit.timeit(concat_directly, number=10000))

如上所示,对于拼接字符串列表,join()方法比在循环中逐个添加字符串更快。原因很简单,一方面,字符串在Python中是不可变数据,每次+=操作都会导致新字符串的创建和旧字符串的复制,这在计算上是昂贵的。另一方面,.join()方法专门针对连接字符串序列进行了优化。它预先计算了结果字符串的大小,然后一次性构建它,因此避免了在循环中使用+=操作时的开销,因此更快。

然而,在测试中,速度最快的函数是直接拼接字符串字面值。其高速度归功于:

  • Python解释器可以在编译时优化字符串字面值的拼接,将它们转化为一个单一的字符串字面值。这没有循环迭代或函数调用的参与,使其成为非常高效的操作。

  • 由于所有字符串在编译时都是已知的,Python可以非常快速地执行此操作,比运行时在循环中拼接甚至优化过的.join()方法都要快。

总而言之,如果你需要拼接一个字符串列表,选择使用join()而不是+=。如果你想直接拼接字符串,只需使用+即可。

列表创建:使用[]而非list()

创建一个列表并不是什么大问题。常见的两种方式是:

  1. 使用list()函数
  2. 直接使用[]

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

1
2
3
4
5
6
import timeit

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

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

结果显示,执行list()函数比直接使用[]要慢。这是因为[]是一个字面语法,而list()是一个构造函数调用。毫无疑问,调用函数需要额外的时间。

从相同的逻辑来看,当创建一个字典时,我们也应该使用{}而不是dict()

成员测试:使用集合而非列表

成员测试操作的性能严重依赖于底层的数据结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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))


print(timeit.timeit(set_membership_test, number=1000))

正如上述代码所演示的,集合中的成员测试比列表中的更快。为什么呢?

在Python列表中,成员测试(element in list)是通过迭代每个元素直到找到所需元素或达到列表末尾来完成的。因此,此操作具有O(n)的时间复杂度。

Python中的集合实现为哈希表。在检查成员资格(element in set)时,Python使用哈希机制,其平均时间复杂度为O(1)。

这里的要点是在编写程序时要仔细考虑底层数据结构。合理利用正确的数据结构可以显著加快我们的代码。

使用推导式替代for循环

在Python中,有四种推导式:列表推导式、字典推导式、集合推导式和生成器表达式。它们不仅提供了更简洁的语法来创建相关的数据结构,而且比使用for循环具有更好的性能,因为它们在Python的C实现中进行了优化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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))


print(timeit.timeit(generate_squares_comprehension, number=10000))

上述代码是对列表推导式和for循环进行简单速度比较的示例。结果显示,列表推导式更快。除此之外列表推导式在很多情况下也更为简洁,呈现了Python之美 ;)

优先使用局部变量

在Python中,访问局部变量比访问全局变量或对象属性更快。以下是一个实例来证明这一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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))


print(timeit.timeit(test_local_variable, number=1000))

这就是Python的工作原理。直观地说,当函数被编译时,其中的局部变量是已知的,但是其他外部变量需要时间来检索。这里的差异时间可能很少,但在处理大量数据时,我们可以利用它来优化我们的代码。

优先使用内置模块和库

提到Python时,默认指的是CPython。因为CPython是Python语言的默认且最广泛使用的实现。

考虑到它的大多数内置模块和库是用C语言编写的,这是一种更快且更底层的语言,我们应该充分利用这些内置工具,避免重新发明轮子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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))

print(timeit.timeit(lambda: count_frequency_builtin(large_list), number=100))

上述程序比较了两种在列表中计算元素频率的方法。利用collections模块中内置的Counter比自己编写for循环更快、更整洁,也更优秀。

利用缓存装饰器加快函数调用

缓存是一种常用的技术,用于避免重复计算并提高程序速度。在大多数情况下,我们无需自己编写缓存处理代码,因为Python提供了一个开箱即用的装饰器,即@functools.cache

以下是一个执行两个斐波那契数生成函数的示例,其中一个使用了缓存装饰器,而另一个没有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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))

print(timeit.timeit(lambda: fibonacci_cached(30), number=1))

结果证明了functools.cache装饰器如何使我们的代码更快。基本的斐波那契函数效率低下,因为在获取fibonacci(30)结果的过程中多次重新计算相同的斐波那契数。

缓存版本明显更快,因为它缓存了先前计算的结果。因此,它仅计算每个斐波那契数一次,随后使用相同参数调用将从缓存中检索。

但是,需要注意的是,使用cache函数缓存的结果会被保存在内存中,并且是永久性的,因此如果使用cache装饰器缓存了一些不再需要的结果,这些结果就会一直占用内存。为了避免这种情况,可以手动清除缓存。并且cache装饰器只能缓存有限的参数类型,例如基本数据类型、元组和不可变集合等。对于可变对象(如列表和字典)和自定义类对象,需要自行实现缓存机制。

更快的无限循环

在创建无限循环时,我们可以使用while Truewhile 1。它们的性能差异通常微乎其微,但有趣的是while 1稍微更快。

这源于数字1是一个字面常量,而True是Python全局名称,需要在Python的全局范围内查找,因此需要一点点的额外开销。

让我们通过一个代码片段来比较这两种方式的实际性能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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))

print(timeit.timeit(loop_with_one, number=10000))

正如我们所见,while 1确实稍微更快。

然而,现代的Python解释器(如CPython)经过高度优化,这样的差异通常微不足道。因此,我们不需要过于担心这个微小的差异。更不用说,while Truewhile 1更易读。

惰性加载Python模块

在Python脚本的顶部导入所有模块似乎是一种自然的做法。
但实际上,我们并不一定要这样做。
而且,如果一个模块过于庞大,按需导入是一个更好的选择。

1
2
3
def my_function():
import heavy_module

如上代码所示,heavy_module 是在函数内部导入的。这是一种“惰性加载”的思想,其中导入被延迟到 my_function 被调用的时候。

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