目录
f_localsplus 存 和 取
首页 后端开发 Python教程 Python函数局部变量如何执行?浅析python函数变量的应用

Python函数局部变量如何执行?浅析python函数变量的应用

Sep 03, 2018 pm 05:33 PM
python 变量

本篇文章给大家带来的内容是关于Python函数局部变量如何执行?浅析python函数变量的应用 ,有一定的参考价值,有需要的朋友可以参考一下,希望对你有所帮助。

前言

这两天在 CodeReview 时,看到这样的代码

# 伪代码
import somelib
class A(object):
    def load_project(self):
        self.project_code_to_name = {}
        for project in somelib.get_all_projects():
            self.project_code_to_name[project] = project
        ...
登录后复制

意图很简单,就是将 somelib.get_all_projects 获取的项目塞入的 self.project_code_to_name

然而印象中这个是有优化空间的,于是提出调整方案:

import somelib
class A(object):
    def load_project(self):
        project_code_to_name = {}
        for project in somelib.get_all_projects():
            project_code_to_name[project] = project
        self.project_code_to_name = project_code_to_name
        ...
登录后复制

方案很简单,就是先定义局部变量 project_code_to_name,操作完,再赋值到self.project_code_to_name

在后面的测试,也确实发现这样是会好点,那么结果知道了,接下来肯定是想探索原因的!

局部变量

其实在网上很多地方,甚至很多书上都有讲过一个观点:访问局部变量速度要快很多,粗看好像好有道理,然后又看到下面贴了一大堆测试数据,虽然不知道是什么,但这是真的屌,记住再说,管他呢!

但是实际上这个观点还是有一定的局限性,并不是放诸四海皆准。所以先来理解下这句话吧,为什么大家都喜欢这样说。

先看段代码理解下什么是局部变量:

#coding: utf8
a = 1
def test(b):
    c = 'test'    
    print a   # 全局变量
    print b   # 局部变量
    print c   # 局部变量

test(3)
登录后复制
# 输出
1
3
test
登录后复制
简单来说,局部变量就是只作用于所在的函数域,超过作用域就被回收
登录后复制

理解了什么是局部变量,就需要谈谈 Python 函数 和 局部变量 的爱恨情仇,因为如果不搞清楚这个,是很难感受到到底快在哪里;

为避免枯燥,以上述的代码来阐述吧,顺便附上 test 函数执行 的 dis 的解析:

# CALL_FUNCTION

  5           0 LOAD_CONST               1 ('test')
              3 STORE_FAST               1 (c)

  6           6 LOAD_GLOBAL              0 (a)
              9 PRINT_ITEM
             10 PRINT_NEWLINE

  7          11 LOAD_FAST                0 (b)
             14 PRINT_ITEM
             15 PRINT_NEWLINE

  8          16 LOAD_FAST                1 (c)
             19 PRINT_ITEM
             20 PRINT_NEWLINE
             21 LOAD_CONST               0 (None)
             24 RETURN_VALUE
登录后复制

在上图中比较清楚能看到 a、b、c 分别对应的指令块,每一块的第一行都是 LOAD_XXX,顾名思义,是说明这些变量是从哪个地方获取的。

LOAD_GLOBAL 毫无疑问是全局,但是 LOAD_FAST 是什么鬼?似乎应该叫LOAD_LOCAL 吧?

然而事实就是这么神奇,人家就真的是叫 LOAD_FAST,因为局部变量是从一个叫 fastlocals 的数组里面读,所以名字也就这样叫了(我猜的)。

那么主角来了,我们要重点理解这个,因为这个确实还挺有意思。

Python 函数执行

Python 函数的构建和运行,说复杂不复杂,说简单也不简单,因为它需要区分很多情况,比方说需要区分 函数 和 方法,再而区分是有无参数,有什么参数,有木有变长参数,有木有关键参数。

全部展开仔细讲是不可能的啦,不过可以简单图解下大致的流程(忽略参数变化细节):

810588816-5b8c9229bbb00_articlex.png

一路顺流而下,直达 fast_function,它在这里的调用是:

// ceval.c -> call_function

x = fast_function(func, pp_stack, n, na, nk);
登录后复制

参数解释下:

  1. func: 传入的 test;

  2. pp_stack: 近似理解调用栈 (py方式);

  3. na: 位置参数个数;

  4. nk: 关键字个数;

  5. n = na + 2 * nk;

那么下一步就看看 fast_function 要做什么吧。

初始化一波

  1. 定义 co 来存放 test 对象里面的 func_code

  2. 定义 globals 来存放 test 对象里面的 func_globals (字典)

  3. 定义 argdefs 来存放 test 对象里面的 func_defaults (构建函数时的关键字参数默认值)

来个判断,如果 argdefs 为空 && 传入的位置参数个数 == 函数定义时候的位置形参个数  && 没有传入关键字参数

那就

  1. 当前线程状态coglobals 来新建栈对象 f;

  2. 定义fastlocals  ( fastlocals = f->f_localsplus; );

  3. 把 传入的参数全部塞进去 fastlocals

那么问题来了,怎么塞?怎么找到传入了什么鬼参数:这个问题还是只能有 dis 来解答:

我们知道现在这步是在 CALL_FUNCTION 里面进行的,所以塞参数的动作,肯定是在此之前的,所以:

 12          27 LOAD_NAME                2 (test)
             30 LOAD_CONST               4 (3)
             33 CALL_FUNCTION            1
             36 POP_TOP
             37 LOAD_CONST               1 (None)
             40 RETURN_VALUE
登录后复制

CALL_FUNCTION 上面就看到 30 LOAD_CONST               4 (3),有兴趣的童鞋可以试下多传几个参数,就会发现传入的参数,是依次通过LOAD_CONST 这样的方式加载进来,所以如何找参数的问题就变得呼之欲出了;

// fast_function 函数

fastlocals = f->f_localsplus;
stack = (*pp_stack) - n;

 for (i = 0; i < n; i++) {
     Py_INCREF(*stack);
     fastlocals[i] = *stack++;
 }
登录后复制

这里出现的 n 还记得怎么来的吗?回顾上面有个 n = na + 2 * nk; ,能想起什么吗?

其实这个地方就是简单的通过将 pp_stack 偏移 n 字节 找到一开始塞入参数的位置。

那么问题来了,如果 n 是 位置参数个数 + 关键字参数,那么 2 * nk 是什么意思?其实这答案很简单,那就是 关键字参数字节码 是属于带参数字节码, 是占 2字节。

到了这里,栈对象 ff_localsplus 也登上历史舞台了,只是此时的它,还只是一个未经人事的少年,还需历练。

做好这些动作,终于来到真正执行函数的地方了: PyEval_EvalFrameEx,在这里,需要先交代下,有个和 PyEval_EvalFrameEx 很像的,叫 PyEval_EvalCodeEx,虽然长得像,但是人家干得活更多了。

请看回前面的 fast_function 开始那会有个判断,我们上面说得是判断成立的,也就是最简单的函数执行情况。如果函数传入多了关键字参数或者其他情况,那就复杂很多了,此时就需要由 PyEval_EvalCodeEx 处理一波,再执行 PyEval_EvalFrameEx

PyEval_EvalFrameEx 主要的工作就是解析字节码,像刚才的那些 CALL_FUNCTIONLOAD_FAST 等等,都是由它解析和处理的,它的本质就是一个死循环,然后里面有一堆 swith - case,这基本也就是 Python 的运行本质了。

f_localsplus 存 和 取

讲了这么长的一堆,算是把 Python 最基本的 函数调用过程简单扫了个盲,现在才开始探索主题。。

为了简单阐述,直接引用名词:fastlocals, 其中 fastlocals = f->f_localsplus<p>刚才只是简单看到了,Python 会把传入的参数,以此塞入 <code>fastlocals 里面去,那么毋庸置疑,传入的位置参数,必然属于局部变量了,那么关键字参数呢?那肯定也是局部变量,因为它们都被特殊对待了嘛。

那么除了函数参数之外,必然还有函数内部的赋值咯? 这块字节码也一早在上面给出了:

# CALL_FUNCTION
  5           0 LOAD_CONST               1 ('test')
              3 STORE_FAST               1 (c)
登录后复制

这里出现了新的字节码 STORE_FAST,一起来看看实现把:

# PyEval_EvalFrameEx 庞大 switch-case 的其中一个分支:

        PREDICTED_WITH_ARG(STORE_FAST);
        TARGET(STORE_FAST)
        {
            v = POP();
            SETLOCAL(oparg, v);
            FAST_DISPATCH();
        }

# 因为有涉及到宏,就顺便给出:
#define GETLOCAL(i)     (fastlocals[i])
#define SETLOCAL(i, value)      do { PyObject *tmp = GETLOCAL(i); \
                                     GETLOCAL(i) = value; \
                                     Py_XDECREF(tmp); } while (0)
登录后复制

简单解释就是,将 POP() 获得的值 v,塞到 fastlocals 的  oparg 位置上。此处,v 是 "test", oparg 就是 1。用图表示就是:

3536973323-5b8c923e0ac38_articlex.png

有童鞋可能会突然懵了,为什么突然来了个 b ?我们又需要回到上面看 test 函数是怎样定义的:

// 我感觉往回看的概率超低的,直接给出算了

def test(b):
    c = 'test'    
    print b   # 局部变量
    print c   # 局部变量
登录后复制

看到函数定义其实都应该知道了,因为 b 是传的参数啊,老早就塞进去了~

那存储知道了,那么怎么取呢?同样也是这段代码的字节码:

22 LOAD_FAST                1 (c)
登录后复制

虽然这个用脚趾头想想都知道原理是啥,但公平起见还是给出相应的代码:

# PyEval_EvalFrameEx 庞大 switch-case 的其中一个分支:
TARGET(LOAD_FAST)
{
    x = GETLOCAL(oparg);
    if (x != NULL) {
        Py_INCREF(x);
        PUSH(x);
        FAST_DISPATCH();
    }
    format_exc_check_arg(PyExc_UnboundLocalError,
        UNBOUNDLOCAL_ERROR_MSG,
        PyTuple_GetItem(co->co_varnames, oparg));
    break;
}
登录后复制

直接用 GETLOCAL 通过索引在数组里取值了。

到了这里,应该也算是把 f_localsplus  讲明白了。这个地方不难,其实一般而言是不会被提及到这个,因为一般来说忽略即可了,但是如果说想在性能方面讲究点,那么这个小知识就不得忽视了。

变量使用姿势

因为是面向对象,所以我们都习惯了通过 class 的方式,对于下面的使用方式,也是随手就来:

class SS(object):
    def __init__(self):
        self.fuck = {}

    def test(self):
        print self.fuck
登录后复制

这种方式一般是没什么问题的,也很规范。到那时如果是下面的操作,那就有问题了:

class SS(object):
    def __init__(self):
        self.fuck = {}

    def test(self):
        num = 10
        for i in range(num):
            self.fuck[i] = i
登录后复制

这段代码的性能损耗,会随着 num 的值增大而增大, 如果下面循环中还要涉及到更多类属性的读取、修改等等,那影响就更大了

这个类属性如果换成 全局变量,也会存在类似的问题,只是说在操作类属性会比操作全局变量要频繁得多。

我们直接看看两者的差距有多大把?

import timeit
class SS(object):
    def test(self):
        num = 100
        self.fuck = {}        # 为了公平,每次执行都同样初始化新的 {}
        for i in range(num):
            self.fuck[i] = i

    def test_local(self):
        num = 100
        fuck = {}             # 为了公平,每次执行都同样初始化新的 {}
        for i in range(num):
            fuck[i] = i
        self.fuck = fuck

s = SS()
print timeit.timeit(stmt=s.test_local)
print timeit.timeit(stmt=s.test)
登录后复制

1407468344-5b8c92521de2d_articlex.png

通过上图可以看出,随着 num 的值越大,for 循环的次数就越多,那么两者的差距也就越大了。

那么为什么会这样,也是在字节码可以看出写端倪:

// s.test
        >>   28 FOR_ITER                19 (to 50)
             31 STORE_FAST               2 (i)

  8          34 LOAD_FAST                2 (i)
             37 LOAD_FAST                0 (self)
             40 LOAD_ATTR                0 (hehe)
             43 LOAD_FAST                2 (i)
             46 STORE_SUBSCR
             47 JUMP_ABSOLUTE           28
        >>   50 POP_BLOCK

// s.test_local
        >>   25 FOR_ITER                16 (to 44)
             28 STORE_FAST               3 (i)

 14          31 LOAD_FAST                3 (i)
             34 LOAD_FAST                2 (hehe)
             37 LOAD_FAST                3 (i)
             40 STORE_SUBSCR
             41 JUMP_ABSOLUTE           25
        >>   44 POP_BLOCK

 15     >>   45 LOAD_FAST                2 (hehe)
             48 LOAD_FAST                0 (self)
             51 STORE_ATTR               1 (hehe)
登录后复制

上面两段就是两个方法的 for block 内容,大家对比下就会知道,  s.test 相比于 s.test_local,  多了个 LOAD_ATTR 放在 FOR_ITERPOP_BLOCK 之间。

这说明什么呢? 这说明,在每次循环时,s.test 都需要 LOAD_ATTR,很自然的,我们需要看看这个是干什么的:

TARGET(LOAD_ATTR)
{
     w = GETITEM(names, oparg);
     v = TOP();
     x = PyObject_GetAttr(v, w);
     Py_DECREF(v);
     SET_TOP(x);
     if (x != NULL) DISPATCH();
     break;
 }

# 相关宏定义
#define GETITEM(v, i) PyTuple_GetItem((v), (i))
登录后复制

这里出现了一个陌生的变量 name, 这是什么?其实这个就是每个 codeobject 所维护的一个 名字数组,基本上每个块所使用到的字符串,都会在这里面存着,同样也是有序的:

// PyCodeObject 结构体成员
PyObject *co_names;        /* list of strings (names used) */
登录后复制

那么 LOAD_ATTR 的任务就很清晰了:先从名字列表里面取出字符串,结果就是 "hehe", 然后通过 PyObject_GetAttr 去查找,在这里就是在 s 实例中去查找。

且不说查找效率如何,光多了这一步,都能失之毫厘差之千里了,当然这是在频繁操作次数比较多的情况下。

所以我们在一些会频繁操作 类/实例属性 的情况下,应该是先把 属性 取出来存到 局部变量,然后用 局部变量 来完成操作。最后视情况把变动更新到属性上。

最后

其实相比变量,在函数和方法的使用上面更有学问,更值得探索,因为那个原理和表面看起来差别更大,下次有机会再探讨。平时工作多注意下,才能使得我们的 PY 能够稍微快点点点点点。

相关推荐:

理解python的全局变量和局部变量

python函数局部变量用法实例分析

详解Python的局部变量和全局变量使用难点

以上是Python函数局部变量如何执行?浅析python函数变量的应用的详细内容。更多信息请关注PHP中文网其他相关文章!

本站声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

Video Face Swap

Video Face Swap

使用我们完全免费的人工智能换脸工具轻松在任何视频中换脸!

热工具

记事本++7.3.1

记事本++7.3.1

好用且免费的代码编辑器

SublimeText3汉化版

SublimeText3汉化版

中文版,非常好用

禅工作室 13.0.1

禅工作室 13.0.1

功能强大的PHP集成开发环境

Dreamweaver CS6

Dreamweaver CS6

视觉化网页开发工具

SublimeText3 Mac版

SublimeText3 Mac版

神级代码编辑软件(SublimeText3)

PHP和Python:解释了不同的范例 PHP和Python:解释了不同的范例 Apr 18, 2025 am 12:26 AM

PHP主要是过程式编程,但也支持面向对象编程(OOP);Python支持多种范式,包括OOP、函数式和过程式编程。PHP适合web开发,Python适用于多种应用,如数据分析和机器学习。

在PHP和Python之间进行选择:指南 在PHP和Python之间进行选择:指南 Apr 18, 2025 am 12:24 AM

PHP适合网页开发和快速原型开发,Python适用于数据科学和机器学习。1.PHP用于动态网页开发,语法简单,适合快速开发。2.Python语法简洁,适用于多领域,库生态系统强大。

visual studio code 可以用于 python 吗 visual studio code 可以用于 python 吗 Apr 15, 2025 pm 08:18 PM

VS Code 可用于编写 Python,并提供许多功能,使其成为开发 Python 应用程序的理想工具。它允许用户:安装 Python 扩展,以获得代码补全、语法高亮和调试等功能。使用调试器逐步跟踪代码,查找和修复错误。集成 Git,进行版本控制。使用代码格式化工具,保持代码一致性。使用 Linting 工具,提前发现潜在问题。

vs code 可以在 Windows 8 中运行吗 vs code 可以在 Windows 8 中运行吗 Apr 15, 2025 pm 07:24 PM

VS Code可以在Windows 8上运行,但体验可能不佳。首先确保系统已更新到最新补丁,然后下载与系统架构匹配的VS Code安装包,按照提示安装。安装后,注意某些扩展程序可能与Windows 8不兼容,需要寻找替代扩展或在虚拟机中使用更新的Windows系统。安装必要的扩展,检查是否正常工作。尽管VS Code在Windows 8上可行,但建议升级到更新的Windows系统以获得更好的开发体验和安全保障。

vscode 扩展是否是恶意的 vscode 扩展是否是恶意的 Apr 15, 2025 pm 07:57 PM

VS Code 扩展存在恶意风险,例如隐藏恶意代码、利用漏洞、伪装成合法扩展。识别恶意扩展的方法包括:检查发布者、阅读评论、检查代码、谨慎安装。安全措施还包括:安全意识、良好习惯、定期更新和杀毒软件。

Python vs. JavaScript:学习曲线和易用性 Python vs. JavaScript:学习曲线和易用性 Apr 16, 2025 am 12:12 AM

Python更适合初学者,学习曲线平缓,语法简洁;JavaScript适合前端开发,学习曲线较陡,语法灵活。1.Python语法直观,适用于数据科学和后端开发。2.JavaScript灵活,广泛用于前端和服务器端编程。

PHP和Python:深入了解他们的历史 PHP和Python:深入了解他们的历史 Apr 18, 2025 am 12:25 AM

PHP起源于1994年,由RasmusLerdorf开发,最初用于跟踪网站访问者,逐渐演变为服务器端脚本语言,广泛应用于网页开发。Python由GuidovanRossum于1980年代末开发,1991年首次发布,强调代码可读性和简洁性,适用于科学计算、数据分析等领域。

vscode怎么在终端运行程序 vscode怎么在终端运行程序 Apr 15, 2025 pm 06:42 PM

在 VS Code 中,可以通过以下步骤在终端运行程序:准备代码和打开集成终端确保代码目录与终端工作目录一致根据编程语言选择运行命令(如 Python 的 python your_file_name.py)检查是否成功运行并解决错误利用调试器提升调试效率

See all articles