Python 関数のローカル変数はどのように実行されますか? Python 関数変数の適用の簡単な分析

不言
リリース: 2018-09-03 17:33:53
オリジナル
2790 人が閲覧しました

この記事では、Python 関数のローカル変数を実行する方法について説明します。 Python 関数の変数の適用に関する簡単な分析は、一定の参考値を持っています。必要な友人はそれを参照することができます。

まえがき

この 2 日間、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 に割り当てます。

その後のテストでは、この方が良いことがわかりました。結果がわかったので、次はその理由を探っていきたいと思います。

ローカル変数

実際、インターネット上のさまざまな場所や多くの書籍でさえ言及されている観点があります。

ローカル変数へのアクセスは非常に困難です。 fast 、一見意味がありそうに見えますが、下にたくさんのテスト データが掲載されています。何のことかわかりませんが、非常に素晴らしいものです。覚えておいてください。心配しないでください。

しかし実際には、この見解にはまだ一定の制限があり、普遍的に適用できるわけではありません。それでは、まずこの文と、なぜ誰もがこの文を好んで言うのかを理解しましょう。

まず、ローカル変数とは何かを理解するためにコードを見てください:

#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 です。 . 名前が示すように、これらの変数は Explain から取得されます。

LOAD_GLOBAL グローバルであることは間違いありませんが、LOAD_FAST とは一体何なのでしょうか? LOAD_LOCAL という名前にする必要があるようですね。

しかし、事実は非常に魔法です。実際には

LOAD_FAST と呼ばれます。ローカル変数は fastlocals という配列から読み取られるため、名前は次のようになります。と叫んだ(たぶん)。

それでは主人公が登場します。これを理解することに集中する必要があります。これは確かに非常に興味深いものだからです。

Python 関数の実行

Python 関数の構築と操作は、複雑とも言えますが、簡単とも言い切れません。たとえば、関数とメソッドは、パラメーターの有無、パラメーターの種類、可変長パラメーターの有無、およびキー パラメーターの有無によってさらに区別されます。

すべてを詳細に説明することは不可能ですが、一般的なプロセスを簡単に説明します (パラメーター変更の詳細は無視します):

Python 関数のローカル変数はどのように実行されますか? Python 関数変数の適用の簡単な分析

fast_function まで進みます。ここでの呼び出しは:

// ceval.c -> call_function

x = fast_function(func, pp_stack, n, na, nk);
ログイン後にコピー
パラメータの説明:

  1. func: Pass Enter

    test;

  2. ##pp_stack: コールスタック (py モード) のおおよその理解;
  3. na: 位置パラメータ数値;
  4. ##nk: キーワードの数;
  5. n = na 2 * nk;
  6. それでは、何を見てみましょう
  7. fast_function
が次に実行します。

wave の初期化

テスト オブジェクトに
    func_code を格納するために co を定義します
  1. 定義globals
  2. func_globals
  3. (辞書) をテスト オブジェクトに格納するには

    argdefs を定義して、
  4. func_defaults
  5. (関数構築時のキーワード) を格納しますテストオブジェクト パラメータのデフォルト値)

  6. argdefsが空なら判定してみましょう
&&

渡される位置パラメータの数 == 位置パラメータ関数が定義されているとき Number && キーワードパラメータが渡されない場合はthen

use
    現在のスレッドステータス
  1. ,

    co, globals 新しいスタック オブジェクトを作成しますf;

    define
  2. fastlocals
  3. (fastlocals = f- >f_localsplus; );

    渡されたすべてのパラメータを入力します
  4. fastlocals
  5. 次に、どのように接続するかという質問が生じます。どのゴースト パラメータが渡されるかを確認する方法: この質問には
  6. 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 を確認できます。興味のある方は、さらにいくつかのパラメータを渡してみてください。渡されたパラメータは LOAD_CONST を通じて順次ロードされることがわかり、パラメータを見つける方法の問題が明らかになります。

// fast_function 函数

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

 for (i = 0; i <p>这里出现的 n 还记得怎么来的吗?回顾上面有个 <code>n = na + 2 * nk;</code> ,能想起什么吗?</p><p>其实这个地方就是简单的通过将 <code>pp_stack</code> 偏移 n 字节 找到一开始塞入参数的位置。</p><p>那么问题来了,如果 n 是 位置参数个数 + 关键字参数,那么 2 * nk 是什么意思?其实这答案很简单,那就是 关键字参数字节码 是属于带参数字节码, 是占 2字节。</p><p>到了这里,栈对象 <code>f</code> 的 <code>f_localsplus</code> 也登上历史舞台了,只是此时的它,还只是一个未经人事的少年,还需历练。</p><p>做好这些动作,终于来到真正执行函数的地方了: <code>PyEval_EvalFrameEx</code>,在这里,需要先交代下,有个和 <code>PyEval_EvalFrameEx</code> 很像的,叫 <code>PyEval_EvalCodeEx</code>,虽然长得像,但是人家干得活更多了。</p><p>请看回前面的 <code>fast_function</code> 开始那会有个判断,我们上面说得是判断成立的,也就是最简单的函数执行情况。如果函数传入多了关键字参数或者其他情况,那就复杂很多了,此时就需要由 <code>PyEval_EvalCodeEx</code> 处理一波,再执行 <code>PyEval_EvalFrameEx</code>。</p><p><code>PyEval_EvalFrameEx</code>  主要的工作就是解析字节码,像刚才的那些 <code>CALL_FUNCTION</code>,<code>LOAD_FAST</code> 等等,都是由它解析和处理的,它的本质就是一个死循环,然后里面有一堆 <code>swith - case</code>,这基本也就是 Python 的运行本质了。</p><h4>f_localsplus 存 和 取</h4><p>讲了这么长的一堆,算是把 Python 最基本的 函数调用过程简单扫了个盲,现在才开始探索主题。。</p><p>为了简单阐述,直接引用名词:<code>fastlocals</code>,  其中 <code>fastlocals = f->f_localsplus</code></p><p>刚才只是简单看到了,Python 会把传入的参数,以此塞入 <code>fastlocals</code> 里面去,那么毋庸置疑,传入的位置参数,必然属于局部变量了,那么关键字参数呢?那肯定也是局部变量,因为它们都被特殊对待了嘛。</p><p>那么除了函数参数之外,必然还有函数内部的赋值咯? 这块字节码也一早在上面给出了:</p><pre class="brush:php;toolbar:false"># 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。用图表示就是:

Python 関数のローカル変数はどのように実行されますか? Python 関数変数の適用の簡単な分析

有童鞋可能会突然懵了,为什么突然来了个 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)
ログイン後にコピー

Python 関数のローカル変数はどのように実行されますか? Python 関数変数の適用の簡単な分析

通过上图可以看出,随着 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 中国語 Web サイトの他の関連記事を参照してください。

関連ラベル:
ソース:php.cn
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
最新の問題
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート
私たちについて 免責事項 Sitemap
PHP中国語ウェブサイト:福祉オンライン PHP トレーニング,PHP 学習者の迅速な成長を支援します!