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

不言
发布: 2018-09-03 17:33:53
原创
3021人浏览过

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

前言

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

1

2

3

4

5

6

7

8

# 伪代码

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

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

立即学习Python免费学习笔记(深入)”;

1

2

3

4

5

6

7

8

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。

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

局部变量

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

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

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

1

2

3

4

5

6

7

8

9

#coding: utf8

a = 1

def test(b):

    c = 'test'   

    print a   # 全局变量

    print b   # 局部变量

    print c   # 局部变量

 

test(3)

登录后复制

1

2

3

4

# 输出

1

3

test

登录后复制

1

简单来说,局部变量就是只作用于所在的函数域,超过作用域就被回收

登录后复制

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

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

# 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,它在这里的调用是:

1

2

3

// 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. 用 当前线程状态、co 、globals 来新建栈对象 f;

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

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

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

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

1

2

3

4

5

6

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 这样的方式加载进来,所以如何找参数的问题就变得呼之欲出了;

1

2

3

4

5

6

7

8

9

// 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字节。

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

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

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

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

f_localsplus 存 和 取

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

为了简单阐述,直接引用名词:fastlocals,  其中 fastlocals = f->f_localsplus

刚才只是简单看到了,Python 会把传入的参数,以此塞入 fastlocals 里面去,那么毋庸置疑,传入的位置参数,必然属于局部变量了,那么关键字参数呢?那肯定也是局部变量,因为它们都被特殊对待了嘛。

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

1

2

3

# CALL_FUNCTION

  5           0 LOAD_CONST               1 ('test')

              3 STORE_FAST               1 (c)

登录后复制

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

# 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 函数是怎样定义的:

1

2

3

4

5

6

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

 

def test(b):

    c = 'test'   

    print b   # 局部变量

    print c   # 局部变量

登录后复制

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

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

1

22 LOAD_FAST                1 (c)

登录后复制

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

# 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 的方式,对于下面的使用方式,也是随手就来:

1

2

3

4

5

6

class SS(object):

    def __init__(self):

        self.fuck = {}

 

    def test(self):

        print self.fuck

登录后复制

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

1

2

3

4

5

6

7

8

class SS(object):

    def __init__(self):

        self.fuck = {}

 

    def test(self):

        num = 10

        for i in range(num):

            self.fuck[i] = i

登录后复制

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

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

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

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 循环的次数就越多,那么两者的差距也就越大了。

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

// 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_ITER 和 POP_BLOCK 之间。

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

1

2

3

4

5

6

7

8

9

10

11

12

13

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 所维护的一个 名字数组,基本上每个块所使用到的字符串,都会在这里面存着,同样也是有序的:

1

2

// PyCodeObject 结构体成员

PyObject *co_names;        /* list of strings (names used) */

登录后复制

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

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

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

最后

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

相关推荐:

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

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

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

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

python速学教程(入门到精通)
python速学教程(入门到精通)

python怎么学习?python怎么入门?python在哪学?python怎么学才快?不用担心,这里为大家提供了python速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!

下载
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
作者最新文章
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习
PHP中文网抖音号
发现有趣的

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号