为什么 Python 代码在函数中运行得更快?

简介: 为什么 Python 代码在函数中运行得更快?

哈喽大家好,我是咸鱼

当谈到编程效率和性能优化时,Python 常常被调侃为“慢如蜗牛”

有趣的是,Python 代码在函数中运行往往比在全局范围内运行要快得多

小伙伴们可能会有这个疑问:为什么在函数中运行的 Python 代码速度更快?

今天这篇文章将会解答大家心中的疑惑

原文链接:https://stackabuse.com/why-does-python-code-run-faster-in-a-function/

译文

要理解为什么 Python 代码在函数中运行得更快,我们需要首先了解 Python 是如何执行代码的

我们知道,python 是一种解释型语言,它会逐行读取并执行代码

当运行一个 python 程序的时候,首先将代码编译成字节码(一种更接近机器码的中间语言)然后 python 解释器执行字节码

def hello_world():
    print("Hello, World!")

import dis
dis.dis(hello_world)
#结果
  2           0 LOAD_GLOBAL              0 (print)
              2 LOAD_CONST               1 ('Hello, World!')
              4 CALL_FUNCTION            1
              6 POP_TOP
              8 LOAD_CONST               0 (None)
             10 RETURN_VALUE

由上所示,python 中的 dis 模块将函数 hello_world 分解为字节码

需要注意的是,python 解释器是一个执行字节码的虚拟机,默认的 python 解释器是用 C 编写的,即 CPython

还有其他的 python 解释器如 Jython(用 Java 编写),IronPython(用于 .net)和PyPy(用 Python 和 C 编写)

为什么 Python 代码在函数中运行得更快

我们来编写一个简单的例子:定义一个函数 my_function,函数内部包含一个 for 循环

def my_function():
    for i in range(100000000):
        pass

编译该函数的时候,字节码可能如下所示

  SETUP_LOOP              20 (to 23)
  LOAD_GLOBAL             0 (range)
  LOAD_CONST              3 (100000000)
  CALL_FUNCTION           1
  GET_ITER            
  FOR_ITER                6 (to 22)
  STORE_FAST              0 (i)
  JUMP_ABSOLUTE           13
  POP_BLOCK           
  LOAD_CONST              0 (None)
  RETURN_VALUE

这里的关键指令是 STORE_FAST ,用于存储循环变量 i

现在我们把这个 for 循环放在 python 脚本的顶层(全局范围内),然后再来看一下字节码

for i in range(100000000):
    pass
  SETUP_LOOP              20 (to 23)
  LOAD_NAME               0 (range)
  LOAD_CONST              3 (100000000)
  CALL_FUNCTION           1
  GET_ITER            
  FOR_ITER                6 (to 22)
  STORE_NAME              1 (i)
  JUMP_ABSOLUTE           13
  POP_BLOCK           
  LOAD_CONST              2 (None)
  RETURN_VALUE

可以看到关键指令变成了 STORE_NAME,而不是 STORE_FAST

字节码 STORE_FASTSTORE_NAME 快,因为在函数中,局部变量存储在固定长度的数组中,而不是存储在字典中。这个数组可以通过索引直接访问,使得变量检索非常快

基本上,它只是一个指向列表的指针,并增加了 PyObject 的引用计数,这两个都是高效的操作

另一方面,全局变量存储在一个字典。当访问全局变量时,Python 必须执行哈希表查找,这涉及计算哈希值,然后检索与之关联的值

虽然经过优化,但仍然比基于索引的查找慢

基准测试验证

我们知道在 Python 中,代码执行的速度取决于代码执行的位置——在函数中还是在全局作用域中

让我们用一个简单的基准测试的例子来比较一下

首先定义一个求阶乘的函数

def factorial(n):
    result = 1
    for i in range(1, n + 1):
        result *= i
    return result

然后在全局范围内执行相同的代码

n = 20
result = 1
for i in range(1, n + 1):
    result *= i

为了对这两段代码进行基准测试,我们可以在 Python 中使用 timeit 模块,它提供了一种简单的方法来对少量 Python 代码进行计时

import timeit

# 函数
def benchmark():
    start = timeit.default_timer()

    factorial(20)

    end = timeit.default_timer()
    print(end - start)

benchmark()
# Prints: 3.541994374245405e-06

# 全局范围
start = timeit.default_timer()

n = 20
result = 1
for i in range(1, n + 1):
    result *= i

end = timeit.default_timer()
print(end - start) 
# Pirnts: 5.375011824071407e-06

可以看到,函数代码的执行速度比全局作用域代码要快

需要注意的是,这两段代码最好不要放在同一脚本中,要分开单独运行

这是因为 benchmark() 函数在执行时间上增加了一些开销,并且全局代码在内部进行了优化

cProfile 分析

python 提供了一个 cProfile 内置模块

让我们用它来分析一个新例子:在局部和全局范围内计算平方和

import cProfile

def sum_of_squares():
    total = 0
    for i in range(1, 10000000):
        total += i * i

i = None
total = 0
def sum_of_squares_g():
    global i
    global total
    for i in range(1, 10000000):
        total += i * i

def profile(func):
    pr = cProfile.Profile()
    pr.enable()

    func()

    pr.disable()
    pr.print_stats()
#
# Profile function code
#
print("Function scope:")
profile(sum_of_squares)

#
# Profile global scope code
#
print("Global scope:")
profile(sum_of_squares_g)

上面的例子中,可以认为sum_of_squares_g() 函数是全局的,因为它使用了两个全局变量, itotal

从性能分析结果中,可以看到函数代码在执行时间方面比全局更有效

Function scope:
         2 function calls in 0.903 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
   1       0.903    0.903    0.903    0.903 profiler.py:3(sum_of_squares)
   1       0.000    0.000    0.000    0.000 {
   method 'disable' of '_lsprof.Profiler' objects}


Global scope:
         2 function calls in 1.358 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
   1       1.358    1.358    1.358    1.358 profiler.py:10(sum_of_squares_g)
   1       0.000    0.000    0.000    0.000 {
   method 'disable' of '_lsprof.Profiler' objects}

如何优化 python 函数的性能

前面我们知道,Python 代码在函数中运行往往比在全局范围内运行要快得多

如果想要进一步提高 python 函数代码效率,不妨考虑一下使用局部变量而不是全局变量

另一种方法是尽可能使用内置函数和库。Python 的内置函数是用 C 实现的,比 Python 快得多

比如 NumPy 和 Pandas,也是用 C 或 C++ 实现的,它们比实现同样功能的 Python 代码速度更快

又比如同样是实现数字求和的功能,python 内置的 sum 函数要比你自己编写函数速度更快

相关文章
|
1天前
|
开发者 Python
Python入门:8.Python中的函数
### 引言 在编写程序时,函数是一种强大的工具。它们可以将代码逻辑模块化,减少重复代码的编写,并提高程序的可读性和可维护性。无论是初学者还是资深开发者,深入理解函数的使用和设计都是编写高质量代码的基础。本文将从基础概念开始,逐步讲解 Python 中的函数及其高级特性。
Python入门:8.Python中的函数
|
23天前
|
存储 缓存 Java
Python高性能编程:五种核心优化技术的原理与Python代码
Python在高性能应用场景中常因执行速度不及C、C++等编译型语言而受质疑,但通过合理利用标准库的优化特性,如`__slots__`机制、列表推导式、`@lru_cache`装饰器和生成器等,可以显著提升代码效率。本文详细介绍了这些实用的性能优化技术,帮助开发者在不牺牲代码质量的前提下提高程序性能。实验数据表明,这些优化方法能在内存使用和计算效率方面带来显著改进,适用于大规模数据处理、递归计算等场景。
58 5
Python高性能编程:五种核心优化技术的原理与Python代码
|
2月前
|
Python
课程设计项目之基于Python实现围棋游戏代码
游戏进去默认为九路玩法,当然也可以选择十三路或是十九路玩法 使用pycharam打开项目,pip安装模块并引用,然后运行即可, 代码每行都有详细的注释,可以做课程设计或者毕业设计项目参考
78 33
|
1月前
|
Python
[oeasy]python057_如何删除print函数_dunder_builtins_系统内建模块
本文介绍了如何删除Python中的`print`函数,并探讨了系统内建模块`__builtins__`的作用。主要内容包括: 1. **回忆上次内容**:上次提到使用下划线避免命名冲突。 2. **双下划线变量**:解释了双下划线(如`__name__`、`__doc__`、`__builtins__`)是系统定义的标识符,具有特殊含义。
32 3
|
2月前
|
JavaScript API C#
【Azure Developer】Python代码调用Graph API将外部用户添加到组,结果无效,也无错误信息
根据Graph API文档,在单个请求中将多个成员添加到组时,Python代码示例中的`members@odata.bind`被错误写为`members@odata_bind`,导致用户未成功添加。
52 10
|
1月前
|
JSON 监控 安全
深入理解 Python 的 eval() 函数与空全局字典 {}
`eval()` 函数在 Python 中能将字符串解析为代码并执行,但伴随安全风险,尤其在处理不受信任的输入时。传递空全局字典 {} 可限制其访问内置对象,但仍存隐患。建议通过限制函数和变量、使用沙箱环境、避免复杂表达式、验证输入等提高安全性。更推荐使用 `ast.literal_eval()`、自定义解析器或 JSON 解析等替代方案,以确保代码安全性和可靠性。
45 2
|
1月前
|
存储 人工智能 Python
[oeasy]python061_如何接收输入_input函数_字符串_str_容器_ 输入输出
本文介绍了Python中如何使用`input()`函数接收用户输入。`input()`函数可以从标准输入流获取字符串,并将其赋值给变量。通过键盘输入的值可以实时赋予变量,实现动态输入。为了更好地理解其用法,文中通过实例演示了如何接收用户输入并存储在变量中,还介绍了`input()`函数的参数`prompt`,用于提供输入提示信息。最后总结了`input()`函数的核心功能及其应用场景。更多内容可参考蓝桥、GitHub和Gitee上的相关教程。
16 0
|
2月前
|
数据挖掘 vr&ar C++
让UE自动运行Python脚本:实现与实例解析
本文介绍如何配置Unreal Engine(UE)以自动运行Python脚本,提高开发效率。通过安装Python、配置UE环境及使用第三方插件,实现Python与UE的集成。结合蓝图和C++示例,展示自动化任务处理、关卡生成及数据分析等应用场景。
178 5
|
2月前
|
Python
Python中的函数是**一种命名的代码块,用于执行特定任务或计算
Python中的函数是**一种命名的代码块,用于执行特定任务或计算
64 18
|
2月前
|
数据可视化 DataX Python
Seaborn 教程-绘图函数
Seaborn 教程-绘图函数
87 8

热门文章

最新文章

推荐镜像

更多