目次

2008y 08m 12d

dis/inspect モジュールと ceval.c を使った Python のハッキング

Python の Virtual Machine Code を追跡することで、python の言語仕様・働きを早く/的確に理解できることが よくあります。そのためには ディスアッセンブラー:dis モジュールと 検査:inspect モジュールが有効です。また関数の func_code オブジェクトにもコンパイル結果が詰まっており有用です。さらに python 内部の動きを深く理解するために ceval.c のソース・コードを追跡できることが望まれます。ceval.c が Virtual Machine Code の各命令を実行している部分だからです。ceval.c を追跡することでローカル変数・グローバル変数の解決方法、ガーベージ・コレクションの方法、python 内部で辞書と hash が果たしている役割などが見えるようになります。

dis/inspce モジュールを使いこなしたり、ceval.c を追跡したりするためには、python インタープリターが動いていくときの全体の概要・俯瞰図を理解しておく必要があります。三つの基本要素:CodeObject, FunctionObject, FrameObject を理解する必要があります。Python インタープリターが三つの基本要素:データ構造を中心に動いていく様子を理解する必要があります。

本稿では dis モジュールを使った disassembler の使い方を説明します。「 python virtual machine がどのように動いているのか」「python virtual machine の動きを inspect する方法」を示します。この過程で三つの基本要素:CodeObject, FunctionObject, FrameObject を理解していきます。

三つの基本要素を理解できると、ceval.c C ソース・コードを追跡できるようになります。ceval.c などを読めるようになることで、python interpreter の詳細な動きが把握できるようになります。Referecne Manual だけでは説明されていない詳細が解るようになります。また ceval.c などの C ソースが解れば python で書くべきか C で書くべきかの明確なイメージを持てるようになります。

後半では dis, inspect, func_code, C ソース を組み合わせて、python interpreter 内部の動きを追跡していきます。最後に python の decorator 構文をハッキングします。解り難い decorator 構文処理を dis/inspect/func_code の視点から python コードの解剖・ハッキングを行って明確にします。

dis モジュールと python interpreter

dis モジュール

dis モジュールは python virtual machine code の逆アッセンブル:disassemble を行うモジュールです。標準モジュールの中にあり、全ての python 配布に最初から入っています。dis.dis(.) 関数により逆アッセンブルを行わせます。

>python -m pydoc "dis.dis"
dis.dis = dis(x=None)
    Disassemble classes, methods, functions, or code.
    With no argument, disassemble the last traceback.

dis.dis(.) の引数には関数やコード・オブジェクトなどを指定します。モジュールやクラスも引数にできるのですが、そのときは関数全部を逆アッセンブルしてくれるだけです。残念ですが、モジュール処理の逆アッセンブルはしてくれません。そのためには、後で述べる inspect モジュールを経由するような細工が必要となります。

関数への dis の適用

dis(.) 関数の引数に関数名を与えれば、その関数の python virtual machine code を標準出力に打ち出してくれます。

ブロック実行

本稿では、下に示すような //@@ と //@@@ で囲んだコードの記述を何度も使います。これは //@@ と //@@@ で囲んだ部分の文字列を \#####.### と名付けたファイルにし、//@@@ の次に続く // で始まる行の文字列をコンソール・コマンド文字列として実行するという意味です。下の例では //@@ から //@@@ の範囲の文字列を \#####.### ファイルにし、そのファイルをカレント・ディレクトリの a.cpp にコピーし、a.cpp を cl.exe でコンパイルする処理を意味します。

//@@
    ・
    ・
//@@@
//copy \#####.### a.cpp
//cl a.cpp -I.\

私はこのような処理をエディタで自動的に行うようにしています。wz エディタでのマクロで実装しています。ctrl O + E で、この一連の処理を行わせています。GPL 条件で公開していますので、wz エディタを使える方はご利用ください

python のときは、実行オプションが付かないことが大部分なので「 //@@ ... //@@@ 文字列を copy \#####.### temp.py と copy させ、カレント・ディレクトリで python.exe temp.py を実行させる」ことまでを ctrl O + P に割り振ったエディタ・マクロで行わせています。sf でのブロック式を計算させるときは 「temp.se を作り sf.exe @@temp.se を実行させる」ことまでを ctrl O + S に割り振ったエディタ・マクロで行わせています。

以下では //@@ と //@@@ で囲んだプログラム・コードが何度も出てきますが、このような意味であることを御承知ください。

まずは実例を見てみましょう。

//@@
def testF(strAg="abc"):
    strAt = strAg+'xyz'
    print strAt
import dis
dis.dis(testF)
//@@@

# dis.dis(testF) 出力
# 行番号  byte数 ニューモニック       参照変数index    命令バイト・コード
  2           0 LOAD_FAST                0 (strAg)     # 7c 00 00
              3 LOAD_CONST               1 ('xyz')     # 64 01 00
              6 BINARY_ADD                             # 17      
              7 STORE_FAST               1 (strAt)     # 7d 01 00
                                                       
  3          10 LOAD_FAST                1 (strAt)     # 7c 01 00
             13 PRINT_ITEM                             # 4d      
             14 PRINT_NEWLINE                          # 4b      
             15 LOAD_CONST               0 (None)      # 64 00 00
             18 RETURN_VALUE                           # 6b      

上の dis.dis(.) 実行例で、「行番号, byte 数, 命令、参照変数index」の四つは dis.dis(testF) が打ち出したものです。でも右端の命令バイト・コード #... は私が手で追加したものです。python プログラムの出力ではありません。命令バイト・コードの出させ方は次の kbplay.py... の節および後の inspect モジュールの所で説明します。関数はコンパイルされると、上の命令バイト・コードが生成されます。python interpreter は、命令バイト・コードを一バイトずつ読み込んで、逐次処理していきます。dis.dis(.) は命令バイト・コードをニューモニックに置き換えているわけです。

行番号はソース行番号を意味します。byte 数は命令バイト・コードの何バイト目かを示します。ニューモニックは、命令バイト・コードの一バイト目を人間に分りやすい 機能を表す LOAD_FAST などの文字列に置き換えたものです。

kbplay.py: hex byte code 表示つきの逆アッセンブル

Assembler 出力のように、ニューモニックと一緒に 具体的な hex byte code も出力しておいたほうが便利なこともあります。でも dis.dis(.) は hex byte コードを出力してくれません。dis モジュールに修正を加えて hex コードを出力させることも試みましたが、他のモジュールとの絡み合いが強くて、私の能力では無理でした。

でも byteplay.py という文字列として扱える dis assembler モジュールが公開されてします。これに手を加えて kbplay.py を作り、hex byte code 付きの disassmeble を可能にしました。下のようにimport kbplay as bp; print bp.Code.from_code(?????.func_code).code の行を ????? の部分を disassemble させたい関数名に置き換えた一行を挿入するだけで、hex byte code 付の disassmble 結果を出力します。

//@@
#07.06.26
def testF():
    inAt = 3
    print 1 + inAt
    print 2

import kbplay as bp; print bp.Code.from_code(testF.func_code).code
//@@@
  3           1  64 01 00 LOAD_CONST           3
              2  7d 00 00 STORE_FAST           inAt

  4           4  64 02 00 LOAD_CONST           1
              5  7c 00 00 LOAD_FAST            inAt
              6  17       BINARY_ADD           
              7  47       PRINT_ITEM           
              8  48       PRINT_NEWLINE        

  5          10  64 03 00 LOAD_CONST           2
             11  47       PRINT_ITEM           
             12  48       PRINT_NEWLINE        
             13  64 00 00 LOAD_CONST           None
             14  53       RETURN_VALUE         

ここで関数の func_code オブジェクトを使っています。func_code オブジェクトについては、直下の次の節で説明します。とりあえずは ????? の部分を書き換えるためだけの black box 行と思ってください。

ただし、kbplay.Code.from_code(.) は func_code オブジェクトしか引数にできません。クラスなどを引数にできません。関数の disassemble しかできません。dis.dis() のような柔軟性がありません。

本腰を入れて kbplay.py モジュールを弄れば、func_code 以外でも disassmple の対象にできそうです。私自身は、もうあまり興味がないので手間を掛ける気になりません。このような限定された kbplay.py ですが、最初の段階では役立つと思います。dis モジュールと kbplay(または byteplay) を適宜組み合わせて使ってみてください。



関数に属する func_code オブジェクトとco_varnames, co_names, co_consts 属性

関数オブジェクトは code オブジェクトを持ちます。この code オブジェクトは、関数オブジェクトの func_code 属性として Python プログラムから参照できます。Code オブジェクトは co_varnames, co_names, co_consts 属性を持っています。co_varnames タプルはローカル変数文字列を保持しています。co_names タプルはグローバル変数も含んだ変数文字列を保持しています。co_consts タプルは定数変数文字列を保持しています。これらのタプル文字列はニューモニックのオペランド数値を定めるのに使われます。

Python virtual machine code のオペランドには、この co_varnames, co_names, co_consts タプルへのインデックスが入ります。下のような具合です。

//@@
def testF(inAg):
    global inGlb
    inAt = inAg + inGlb + 3
    inGlb = 4
    return inAg + inGlb

import dis;dis.dis(testF)
print "------------------"
print "co_code:    ", testF.func_code.co_code
print "co_argcount:", testF.func_code.co_argcount
print "co_consts:  ", testF.func_code.co_consts
print "co_varnames:", testF.func_code.co_varnames
print "co_names:   ", testF.func_code.co_names
//@@@
  3           0 LOAD_FAST                0 (inAg)
              3 LOAD_GLOBAL              1 (inGlb)
              6 BINARY_ADD          
              7 LOAD_CONST               1 (3)
             10 BINARY_ADD          
             11 STORE_FAST               1 (inAt)

  4          14 LOAD_CONST               2 (4)
             17 STORE_GLOBAL             1 (inGlb)

  5          20 LOAD_FAST                0 (inAg)
             23 LOAD_GLOBAL              1 (inGlb)
             26 BINARY_ADD          
             27 RETURN_VALUE        
------------------
co_code:     |td}da|tS
co_argcount: 1
co_consts:   (None, 3, 4)
co_varnames: ('inAg', 'inAt')
co_names:    ('inAg', 'inGlb', 'inAt')

co_varnames:('inAg', 'inAt') タプルがローカル変数の文字列タプルです。python では関数の引数変数もローカル変数に含まれます。「LOAD_FAST i」の命令コードでローカル変数を data stack に push します。LOAD_FAST のオペランドにある index i は co_varnames タプルの i 番目の要素であることを意味します。co_varnames 文字列タプルはコンパイル時に定まります。

関数は func_code オブジェクトを持ち、func_code オブジェクトは co_code, co_argcount, co_consts, co_varnames, co_names を持ちます。

関数
──┬
  func_code;コード・オブジェクト
  ─┬──
    ├─ co_code            : 関数の virtual machine code
    ├─ co_argments        : 関数の引数の個数 デフォルト引数の設定で必要になります
    │
    ├─ co_consts[i]       : 数値や文字列などの定数値タプル
    ├─ co_varnames[i]     : ローカル変数名文字列のタプル
    ├─ co_names[i]        : 関数で使われている変数名文字列のタプル
    ├─ co_cellvars        : クロージャーで使われる変数名文字列のタプル
    ├─ co_freevars        : クロージャーで使われる変数名文字列のタプル

co_names:('inAg', 'inAt', 'inGlb') がグローバル変数も含んだ文字列タプルです。「LOAD_GLOBAL i」の命令コードで、グローバル変数を data stack に push します。LOAD_GLOBAL のオペランドにある index i は co_names タプルの i 番目の要素であることを意味します。Python compiler は、 global 宣言された変数を co_varnames タプルには入れずに co_names タプルのみに配置します。そして global 変数の読み書きには LOAD_GLOBAL/STORE_GLOBAL コードを割り当てます。

testF 関数オブジェクトに属する func_code オブジェクトは python コードのコンパイル時に定まります。co_varnames, co_names もコンパイル時に定まります。上のコード例ではグローバル変数 inGlb は何処にも実体が存在しないので testF() 関数を実行したときエラー例外が発生します。でも実行せずにコンパイルするだけなら python インタープリタは動いてしまいます。

関数 testF(.) で、グローバル変数 inGlb が具体的に指し示している先を決めるのは、関数 testF(.) を含んだモジュールを実行するときです。ここらの仕組みを理解するには、次に述べる python interpreter の働き方、FrameObject, eval.c などの理解が必要です。



python interpreter

Python 関数は以下のように python interpreter によって実行されます。

関数を実行する前に、関数ごとに data stack 領域が設けられます。Data stack 領域には、文字列などヒープ・メモリー領域にある実体への参照値が積まれます。ですから data stack は 1 ワードごとに何らかのヒープ・メモリにあるオブジェクト:実体を指すことになります。Python virtual machine は C 言語とは異なり、 data stack にオブジェクト自体を積むことはありません。オブジェクトへの参照のみを積みます。下のように「print 5」を行わせる python code で data stack に詰まれるのは数値オブジェクトの参照値配列のインデックス番号 1 にある参照値です。整数 5 のオブジェクトはヒープ領域に作られるのであり、data stack には作られません。

//@@
def testF():
    print 5
    inAt = 3
    print inAt+5

import dis; dis.dis(testF)
print "co_consts:  ", testF.func_code.co_consts
print "co_varnames:", testF.func_code.co_varnames
//@@@
python temp.py
  2           0 LOAD_CONST               1 (5)
              3 PRINT_ITEM          
              4 PRINT_NEWLINE       

  3           5 LOAD_CONST               2 (3)
              8 STORE_FAST               0 (inAt)

  4          11 LOAD_FAST                0 (inAt)
             14 LOAD_CONST               1 (5)
             17 BINARY_ADD          
             18 PRINT_ITEM          
             19 PRINT_NEWLINE       
             20 LOAD_CONST               0 (None)
             23 RETURN_VALUE        
co_consts:   (None, 5, 3)
co_varnames: ('inAt',)

Python Interpretor は命令バイトコードを一命令ずつ参照しながら、data stack 領域に参照値を積み(LOAD_FAST, LOAD_CONST)、演算処理(BINARY_ADD)を施した結果作られるヒープ領域実体への参照値を data_stack の Top の値と置き換えるといった一連の処理を繰り返します。

ヒープ領域のデータの実体への参照と、命令バイト・コードのオペランドとの対応はコンパイル時に定められ co_const[], co_varnames[] などの配列データとして関数オブジェクトの中の CodeObject に構築されます。

汎用 CPU の働き

Python virtual machine との比較のために汎用的な CPU の構成を下に示します。ここでは 32 bit CPU だと考えておきましょう。Internal bus は 32 ビット・サイズとしましょう。

汎用 CPU は ALU(Arithmetic Logic Unit) を中心に、program counter, instruction decoder, register が配置され、それらが互いに 32 bit internal bus で bus switch を介して繋がっています。命令コードに応じて ALU への bus switch が切り替えられます。ALU は and/or/add などの演算処理を行います。演算された結果は bus switch と internal bus を経由して register, stack RAM, data memory RAM などに保存されます。

汎用 CPU は下のようにして code memory にあるプログラム・コードを実行していきます

汎用 CPU では、2^32 サイズの code memory 領域から一つのコードを取り出し、ALU で処理させ、その結果を 2^32 サイズの stack RAM 領域、 data memory RAM 領域の一箇所、またはレジスタに保存します。このような program counter や stack pointer などのレジスタがメモリ・アドレスを保持・変化させること、また instruction decoder や ALU の働きを、CMOS トランジスタを組み合わせた電子回路で実装できます。C 言語の関数記述は、この汎用 CPU の動作を高水準言語で記述するものです。C 言語の関数を実行することで 2^32 サイズの code momory 領域から program code が取り出され、演算処理の結果が 2^32 サイズ stack/data Memory RAM 領域の特定の場所に保存されます。

stack RAM は一本であり、全てのルーチンで共用されます。一本の共通スタックにオート変数やサブ・ルーチンからの戻りアドレスを積み上げていきます。サブルーチンの処理が終了して return 命令が実行されると、スタック・ポインタ・レジスタが呼び出す前の位置に戻されます。 Data Memory RAM 領域も一つであり、全てのルーチンで共用されます。

ダイナミックに生成され参照されるオブジェクト・インスタンスは malloc 関数で確保される heap 領域の一部に、その実体を確保します。Python は 参照カウンタを使った garbage collection 機能が組み込まれており、heap 領域のオブジェクト・インスタンスがどこからも参照されなくなると自動的に heap 領域から消してくれます。C/C++ などの garbege collection 機能が無い言語では、インスタンスの消滅に伴う heap 領域の解放を自分で明示的に記述します。C++ では destructor と delete 文を使います。

Python Virtual Machine と FrameObject

普通の CPU 上で実行される マシン・コードに比較して、Python Virtual Machine は より抽象化されています。例えば x86 マシンでは、コード・セグメント上の共通 code memory 上にマシン・コード があります。データ・セグメント上の共通 stack/data RAM の上に様々のデータを乗せます。でも Python では module, class, function オブジェクトが先にあって、その中に 命令 code や data stack が設けられます。

モジュールが実行されるときに、モジュールの python virtual machine コードが取り出されます。そのモジュール専用の data stack が作られます。data stack 上に co_varnames[], co_names[], co_consts[] 経由でのオブジェクト・インスタンスへの参照が載せられ、ニューモニックに応じた処理がなされていきます。クラスや関数のときも同様です。

汎用 CPU における program counter や stack pointer の代わりに FrameObject が module, class function それぞれ毎に作られます。FrameObject の f_lasti が program counter に相当し、最後に実行した命令コードを指し示しています。f_stacktop が stack pointer に相当し、最後の命令を実行した後に stack の最上段を指しています。

関数に data stack や FrameObject は関数の呼び出し毎に生成されるので、関数ごとに定まったスタック・サイズだけであっても関数の recursive call が問題なく実行できます。また generator 関数のような、関数の途中でのコンテキスト・スイッチが簡単に実現できます。dis.dis(.) で generator 関数を見てやれば、普通の関数のときと殆ど同じコードを使っていることが解ります。

Python interpreter では FrameObject が全体動作の要の役割を果します。この FrameObject の中身を inspect module を使って読み出せます。でも inspect モジュールの説明の前に python virtual machine の C コードを見ておきましょう。ここで扱われる PyFrameObject と殆ど同じものを inspect モジュールを使って覗き込むからです。

ceval.c:Python Virtual Machine の C ソース・コード

Python Virtual Machine 自体は C 言語で記述されています。そのソースも公開されています。各 machine コードについて Language Reference に書いてある以上のことを知りたいときは、ceval.c や それと関連する c ソースを読まねばなりません。 その C source のうち FrameObject を表す構造体 PyFrameObject と、python machie code を実行する ceval.c が python interpreter の動きを理解するのに重要です。

ceval.c を逐次追う気がなくとも、概要だけでも知っておくと、python interpreter の動きをイメージしやすくなります。

なお Python Reference Manual:パイソン・レファランス・マニュアルは ceval.c などの c source も参照しながら読む資料だと私は主張します。Python Reference Manual だけでは何を言っているのか分からない文章が数多く散りばめられています。

PyFrameObject 構造体

ceval.c の動作を追うとき、FrameObject 構造体のデータがどのように操作されるかを見ることになります。ceval.c の動きを詳細に追跡するためには、最初に下の PyFrameObject 構造体を理解しておくべきです。PyFrameObject のインスタンスが、module/class/function/ 毎に設けられる FrameObject だからです。また PyFrameObject の中の多くのメンバーを、後で述べる inspect module を使って python から参照できるからです。まず PyFrameObject 構造体の C ソースを下に示します。

typedef struct _frame {                                                         
    PyObject_VAR_HEAD                                                           
    struct _frame *f_back;      /* previous frame, or NULL */                   
    PyCodeObject *f_code;       /* code segment */                              
    PyObject *f_builtins;       /* builtin symbol table (PyDictObject) */       
    PyObject *f_globals;        /* global symbol table (PyDictObject) */        
    PyObject *f_locals;         /* local symbol table (any mapping) */          
    PyObject **f_valuestack;    /* points after the last local */               
    /* Next free slot in f_valuestack.  Frame creation sets to f_valuestack.    
       Frame evaluation usually NULLs it, but a frame that yields sets it       
       to the current stack top. */                                             
    PyObject **f_stacktop;                                                      
    PyObject *f_trace;          /* Trace function */                            
    PyObject *f_exc_type, *f_exc_value, *f_exc_traceback;                       
    PyThreadState *f_tstate;                                                    
    int f_lasti;                /* Last instruction if called */                
    /* As of 2.3 f_lineno is only valid when tracing is active (i.e. when       
       f_trace is set) -- at other times use PyCode_Addr2Line instead. */       
    int f_lineno;               /* Current line number */                       
    int f_restricted;           /* Flag set if restricted operations            
                                   in this scope */                             
    int f_iblock;               /* index in f_blockstack */                     
    PyTryBlock f_blockstack[CO_MAXBLOCKS]; /* for try and loop blocks */        
    int f_nlocals;              /* number of locals */                          
    int f_ncells;                                                               
    int f_nfreevars;                                                            
    int f_stacksize;            /* size of value stack */                       
    PyObject *f_localsplus[1];  /* locals+stack, dynamically sized */           
} PyFrameObject;       

上で int f_lasti メンバーが実行中の machine code を指しています。int f_lineno がソース行を指しています。デバッガは f_lineno を追跡しています。

f_stacktop が現在の stack の Top 位置を指しています。f_stacksize はモジュールや関数が使う stack サイズを保持しています。コンパイル時に定まります。なお python では関数呼出しの度に data stack が作られるので、関数毎に固定されているスタック・サイズでも、リカーシブな関数呼び出しが可能です。C 言語で必要だった共通する大きなスタック・メモリを必要としません。

f_locals は関数内部で使うローカル変数と関数引数の配列です。命令バイト:LOAD_FAST i のオペランド i とオブジェクトとの対応はコンパイル時に定まっています。ソースがなくても、変数名文字列は保持されています。f_globals はモジュールで定義されているグローバル変数の配列です。モジュール・オブジェクトや関数オブジェクトごとに f_locals や f_globals への参照配列が作られます。これらは inspect モジュールを使って参照できます。

f_ncells, f_nfreevars は closure 関数を扱うときに使われます。やはり inspect モジュールを使って参照できます。

f_back は、PyFrameObject インスタンスを含む関数を呼び出した、元の側の PyFrameObject インスタンスを指しています。例外が発生したときの巻き戻し処理に使います。また inspect モジュールを使う時には、間接的に f_back にも世話になります。

ceval.c ソース

PyFrameObject 構造体を理解すると ceval.c ソースを追えるようにもなります。

PyObject * PyEval_EvalFrame(PyFrameObject *f) 関数

python interpreter が Python Virtual Machine コードを実行するとき ceval.c ソースにある PyObject * PyEval_EvalFrame(PyFrameObject *f) 関数を呼び出します。ここで PyFrameObject* 引数が PyEval_EvalFrame(.) 関数の引数に与えられていることに注目してください。PyFrameObject 構造体へのポインタ引数が与えられていれば、PyFramObject の PyObject **f_stacktop メンバーより、data stack 上に積まれたデータを取り出せます。 PyCodeObject *f_code と int f_lasti より、Python Virtual Machine コードの OP コードを取り出せます。その OP コードが BUILD_LIST のときは、下の部分を実行することになります。

PyObject * PyEval_EvalFrame(PyFrameObject *f)
{
            ・
            ・
        first_instr = PyString_AS_STRING(co->co_code);
        next_instr = first_instr + f->f_lasti + 1;
        stack_pointer = f->f_stacktop;
            ・
            ・
        for (;;){
            #define NEXTOP()    (*next_instr++)
            opcode = NEXTOP();

            switch (opcode) {
                ・
                ・
            case BUILD_LIST:
                ・
                ・
            }
            case ..........:
                ・
                ・
            }
            ・
            ・
        }
}

PyEval_EvalFrame(.) 関数は PyCodeObject *f_code が指し示す python virtual machine code を return 命令に遭遇するまで実行し続けます。for(;;){....} の内部を回り続けます。

BUILD_LIST

BUILD_LIST について、より詳しく見てみましょう。まずは下のような python リストを生成するコードを見てみましょう。

//@@
def testF():
    lstAt = [1,2,3]
import dis;dis.dis(testF)
//@@@
  2           0 LOAD_CONST               1 (1)
              3 LOAD_CONST               2 (2)
              6 LOAD_CONST               3 (3)
              9 BUILD_LIST               3
             12 STORE_FAST               0 (lstAt)
             15 LOAD_CONST               0 (None)
             18 RETURN_VALUE        

BUILD_LIST virtual machine code によりスタックに積んである 1,2,3 の値からリスト・オブジェクトを生成し lstAt のラベル変数に参照値を代入しています。

ceval.c における BUILD_LIST のコードは下のようになっています

PyObject * PyEval_EvalFrame(PyFrameObject *f)
{
            ・
            ・
        case BUILD_LIST:
                x =  PyList_New(oparg);
                if (x != NULL) {
                        for (; --oparg <= 0;) {
                                w = POP();
                                PyList_SET_ITEM(x, oparg, w);
                        }
                        PUSH(x);
                        continue;
                }
                break;
                ・
                ・
        }
}

PyList_SET_ITEM(.) 関数については Google Code Search を使って「PyList_SET_ITEM 2.4」で検索してみると下のコードが見つかります。Python のリストは、C 言語のソースから見れば配列というべきだと分かります。

#define PyList_SET_ITEM(op, i, v) (((PyListObject *)(op))->ob_item[i] = (v))



inspect モジュール

inspect モジュールは python 内部状態を調べ上げて人間に解りやすい形で報告させる機能を纏めたモジュールです。ただし以下では python code を調べるための機能に限定して説明します。FrameObject 経由で調べられる機能を中心に説明していきます

CodeObject/FramObject/FunctionObject

モジュール 関数 クラスといった Python interpreter で処理を行うオブジェクトでは、プログラムの実行において CodeObject, FramObject の二つのオブジェクトが常に 働いています。 また python では関数もオブジェクトです。自由に持ち運びできます。C 言語において関数ポインタに行わていせることが python でもできてしまいます。そのために関数の宣言時に FunctionObject が作られています。

また inspect モジュールを使うとき、また Python の動きを C source のレベルで理解しようとしたとき、この CodeObject, FramObject, FunctionObject 三つのオブジェクトを詳しく理解しておく必要があります。

 ┌───────────┬ Module/class ────┬──────────────────┐
 │FrameObject           │                      │CodeObject                          │
 │    f_code            │                      │    co_varname  # ローカル変数文字列│
 │    f_builtins 辞書   │                      │    co_names    # 全部の変数名文字列│
 │    f_globals 辞書    │                      │    co_filename                     │
 │    f_locals  辞書    │                      │コンパイル時に定まる                │
 │# stack とローカル変数│                      │                                    │
 │    f_localsplus      │                      │                                    │
 │                      │                      │                                    │
 │モジュールの f_globals│                      │                                    │
 │の値は│実行していく過│                      │                                    │
 │程で定まる            │                      │                                    │
 └───────────┴───────────┴──────────────────┘
 ┌───────────┬ Function ──────┬──────────────────┐
 │                      │                      │                                    │
 │FrameObject           │ FucntionObject       │CodeObject                          │
 │    f_code            │     func_code        │    co_varname  # ローカル変数文字列│
 │    f_builtins 辞書   │     func_globals     │    co_names    # 全部の変数名文字列│
 │    f_globals 辞書    │                      │    co_filename                     │
 │    f_locals  辞書    │ 関数の宣言時に定まる │コンパイル時に定まる                │
 │# stack とローカル変数│                      │                                    │
 │    f_localsplus      │                      │                                    │
 │実行開始時に定まる    │                      │                                    │
 └───────────┴───────────┴──────────────────┘

FunctionObject は関数のみに作られます。関数の宣言時に作られます。モジュールやクラスには引数と一緒に呼び出すということがないのて、FunctionObject がありません。

f_, func_, co_ 名前付け規則

f_locals, func_globals, co_varnames などに似た機能の変数名が、これから何度も出てきます。これらの変数名で接頭辞の f_, func_, co_ は下の意味を持っています。

ここで f_ が functiontion の意味に誤解しやすいのですか、frame の意味です。これを覚えておくと、inspect など python の中身を覗き込むとき楽になります。

FrameObject Stack

PyFrameObject オブジェクトを含む caller's frame 配列全部を inspect モジュールの stack() 関数を使って調べられます。 FrameObject が下のように f_back を介してリンクされた状態で python interpreter が動いているからです。

python -m pydoc inspect.stack 
Help on function stack in inspect:

inspect.stack = stack(context=1)
    Return a list of records for the stack above the caller's frame.

具体的には下のような inspect.stack() 関数を使って FrameObject を含んだリストの一覧を inspect できます。

//@@
#07.05.24
inGl1 = 8
inGl2 = 9
def testF():
    inAt = 3
    inAt2 = 4
    def innerF():
        global inGl1, inGl2
        inInAt = 5
        print inAt + inGl1 + inGl2 + inAt2 + inInAt

        import pprint as pp;import inspect as ins;pp.pprint(ins.stack())
    innerF()
testF()
//@@@

python temp.py
29
[(<frame object at 0x008FE6D8>, 'temp.py', 12, 'innerF', ['        pp.pprint(ins.stack())\n'],  0),
 (<frame object at 0x00981840>, 'temp.py', 13, 'testF', ['    innerF()\n'], 0),
 (<frame object at 0x008AE990>, 'temp.py', 14, '?', ['testF()\n'], 0)]

上のように inspect.stack() により、呼び出した関数の frame object, ファイル名、呼び出し行、呼び出し関数情報を収集してくれます。デバッガ上でマニュアル操作から import pprint as pp;import inspect as ins;pp.pprint(ins.stack()) を実行させても同じ結果を得られます。これを使えば どんな経路で呼び出されたか確認できます。ただし本稿での一番重要な情報は frame object にあります。PyFrameObject 情報が入っているからです。

inspect.getmembers(FrameObject)

inspect.getmembers(inspect.stack[i][0]) によって、C ソース:eval.c でも説明した frame object:PyFrameObject の中身を調べられます。次のような具合です。

//@@
#07.05.24
inGl1 = 8
inGl2 = 9
def testF():
    inAt = 3
    inAt2 = 4
    def innerF():
        global inGl1, inGl2
        inInAt = 5
        print inAt + inGl1 + inGl2 + inAt2 + inInAt

        #import pprint as pp;import inspect as ins;pp.pprint(ins.getmembers(ins.stack()[0][0]) )
        import pprint as pp;import inspect as ins;print(ins.getmembers(ins.stack()[0][0]) )
    innerF()
testF()
//@@@
python temp.py
29
[('__class__', ),
 ('__delattr__', ),
 ('__doc__', None),
 ('__getattribute__', ),
        ・
        ・ snip
        ・
 ('f_code', <code object innerF at 0091FC60, file "temp.py", line 7>),
 ('f_exc_traceback', None),
 ('f_exc_type', None),
 ('f_exc_value', None),
 ('f_globals',
  {'__builtins__': ,
   '__doc__': None,
   '__file__': 'temp.py',
   '__name__': '__main__',
   'inGl1': 8,
   'inGl2': 9,
   'testF': }),
 ('f_lasti', 74),
 ('f_lineno', 12),
 ('f_locals',
  {'inAt': 3,
   'inAt2': 4,
   'inInAt': 5,
   'ins': ,
   'pp': }),
 ('f_restricted', 0),
 ('f_trace', None)]

frame object の f_globals 属性にモジュールに属するグローバル変数文字列と値(より厳密には値への参照)が記録されています。f_locals 属性にローカル変数の文字列と値(より厳密には値への参照)が記録されています。これらは MAKE_FUNCTION, CALL_FUNCTION によって作られます。ここで、コード・オブジェクトの co_names[], co_varnames[] は配列であり、 f_globals/f_locals は辞書であることに注意すべきです。Python interpreter は co_varnames[] から変数名文字列を取り出し、それを元にローカル変数のインデックスを定め data stack にローカル変数を設定しながら f_globals/f_locals は辞書を作っていきます。

なお FrameObject で f_locals のペアになっている下の辞書は pprint を使ってキーのアルファベット順で出力させています。co_varnames[] リストの時とは異なり f_locals 辞書のキーの順序には、特に意味はありません。ceval.c のソースを追っても f_locals を使うのは NULL と比較してローカル変数の有無を見ているだけです。

 ('f_locals',
  {'inAt': 3,
   'inAt2': 4,
   'inInAt': 5,
   'ins': ,
   'pp': }),

f_globals 辞書も同様に、キーの順序には意味はありません。

inspect.getmembers(FrameObject.f_code)

もう一つ重要なのが FrameObject f_code 属性です。次のように、この f_code 属性を inspect できるからです。これを使えば、今までの説明では見ることができなかったモジュールやクラスの python virtual machine code も見ることができるからです。dis.dis(.) では無理だった、モジュールやクラスの byte code を FrameObject.f_code から見ることができるからです。各命令の hex 値も見れるからです。

//@@
#07.05.24
inGl1 = 8
inGl2 = 9
def testF():
    inAt = 3
    inAt2 = 4
    def innerF():
        global inGl1, inGl2
        inInAt = 5
        print inAt + inGl1 + inGl2 + inAt2 + inInAt

        import pprint as pp;import inspect as ins;pp.pprint(ins.getmembers(ins.stack()[0][0].f_code) )
    innerF()
testF()
//@@@
[('__class__', ),
 ('__cmp__', ),
 ('__delattr__', ),
 ('__doc__',
        ・
        ・ snip
        ・
 ('co_argcount', 0),
 ('co_cellvars', ()),
 ('co_code',
  'd\x01\x00}\x02\x00\x88\x00\x00t\x02\x00\x17t\x03\x00\x17\x88\x01\x00\x17|\x02
  \x00\x17GHd\x00\x00k\x05\x00}\x00\x00d\x00\x00k\x07\x00}\x01\x00|\x00\x00i\x05
  \x00|\x01\x00i\t\x00|\x01\x00i\n\x00\x83\x00\x00d\x02\x00\x19d\x02\x00\x19i\x0b
  \x00\x83\x01\x00\x83\x01\x00\x01d\x00\x00S'),
 ('co_consts', (None, 5, 0)),
 ('co_filename', 'temp.py'),
 ('co_firstlineno', 7),
 ('co_flags', 3),
 ('co_freevars', ('inAt', 'inAt2')),
 ('co_lnotab', '\x00\x01\x00\x01\x06\x01\x15\x02'),
 ('co_name', 'innerF'),
 ('co_names',
  ('inInAt',
   'inAt',
   'inGl1',
   'inGl2',
   'inAt2',
   'pprint',
   'pp',
   'inspect',
   'ins',
   'getmembers',
   'stack',
   'f_code')),
 ('co_nlocals', 3),
 ('co_stacksize', 4),
 ('co_varnames', ('pp', 'ins', 'inInAt'))]

下の部分が python virtula machine の具体的なバイト・コードの値の列です。

 ('co_code',
  'd\x01\x00}\x02\x00\x88\x00\x00t\x02\x00\x17t\x03\x00\x17\x88\x01\x00\x17|\x02
  \x00\x17GHd\x00\x00k\x05\x00}\x00\x00d\x00\x00k\x07\x00}\x01\x00|\x00\x00i\x05
  \x00|\x01\x00i\t\x00|\x01\x00i\n\x00\x83\x00\x00d\x02\x00\x19d\x02\x00\x19i\x0b
  \x00\x83\x01\x00\x83\x01\x00\x01d\x00\x00S'),

上では 'd' == 0x64 など ASCII 文字で表せるコードは '\x..' hex 表示ではなく ASCII 文字を使って表示しています。下のようにしてやれば、命令バイト・コード:f_code.co_code 部分を文字を含まない hex 数値で表現できます。

//@@
import inspect as ins
import sys
for i, x in enumerate(ins.stack()[0][0].f_code.co_code):
    strAt = hex(ord(x))
    if (i%10==0):
        print

    if len(strAt) == 3:
        print "0x0" + strAt[-1],
    else:
        print strAt,

#print
#print [hex(ord(x)) for x in ins.stack()[0][0].f_code.co_code]
//@@@
0x64 0x00 0x00 0x6b 0x00 0x00 0x5a 0x01 0x00 0x64
0x00 0x00 0x6b 0x02 0x00 0x5a 0x02 0x00 0x78 0x7d
0x00 0x65 0x03 0x00 0x65 0x01 0x00 0x69 0x04 0x00
0x83 0x00 0x00 0x64 0x01 0x00 0x19 0x64 0x01 0x00
0x19 0x69 0x05 0x00 0x69 0x06 0x00 0x83 0x01 0x00
0x44 0x5d 0x5b 0x00 0x5c 0x02 0x00 0x5a 0x07 0x00
0x5a 0x08 0x00 0x65 0x09 0x00 0x65 0x0a 0x00 0x65
0x08 0x00 0x83 0x01 0x00 0x83 0x01 0x00 0x5a 0x0b
0x00 0x65 0x07 0x00 0x64 0x02 0x00 0x16 0x64 0x01
0x00 0x6a 0x02 0x00 0x6f 0x05 0x00 0x01 0x48 0x6e
0x01 0x00 0x01 0x65 0x0c 0x00 0x65 0x0b 0x00 0x83
0x01 0x00 0x64 0x03 0x00 0x6a 0x02 0x00 0x6f 0x10
0x00 0x01 0x64 0x04 0x00 0x65 0x0b 0x00 0x64 0x05
0x00 0x19 0x17 0x47 0x71 0x33 0x00 0x01 0x65 0x0b
0x00 0x47 0x71 0x33 0x00 0x57 0x64 0x00 0x00 0x53

inspect 経由による モジュールの disassemble

関数ではなくモジュールの virtual machine code を見たいときもあります。でも関数のときのように単純には見られません。dis.dis(module) とやっても、関数しか disassemble してくれません。モジュールを import した後では、モジュールの code オブジェクトは捨てられてしまうからです。用済みになってしまうからです。

でも inspect.stack()[0,0].f_code と、FrameObject から、実行中のフレームの code object を取り出せます。これを逆アッセンブルすることで、今までの説明ではできなかったモジュールの逆アッセンブルが可能になります。

//@@
#07.05.24
inGlb = 8
def testF():
    global inClb
    inAt = 3
    print inAt + inGlb

testF()
import dis;import inspect as ins;dis.dis(ins.stack()[0][0].f_code)
//@@@
python temp.py
11
  2           0 LOAD_CONST               0 (8)
              3 STORE_NAME               0 (inGlb)

  3           6 LOAD_CONST               1 (<code object testF at 0091FC20, file "temp.py", line 3>)
              9 MAKE_FUNCTION            0
             12 STORE_NAME               1 (testF)

  8          15 LOAD_NAME                1 (testF)
             18 CALL_FUNCTION            0
             21 POP_TOP             
            ・snip
            ・
            

下のように FrameObject stack の f_code 経由でモジュールの co_varnames[], co_names[], co_consts[] を見られるようになります。

//@@
#07.05.24
inGlb = 8
def testF():
    global inClb
    inAt = 3
    print inAt + inGlb

testF()
import inspect as ins;print(ins.stack()[0][0].f_code.co_varnames)
import inspect as ins;print(ins.stack()[0][0].f_code.co_names)
import inspect as ins;print(ins.stack()[0][0].f_code.co_consts)
//@@@
python temp.py
11
('inGlb', 'ins', 'testF')
('inGlb', 'testF', 'inspect', 'ins', 'stack', 'f_code', 'co_varnames', 'co_names', 'co_consts')
(8, <code object testF at 0091FC20, file "temp.py", line 3>, None, 0)

inspect を使わない モジュールの disassemble

モジュールの逆アッセンブルをさせたいとき、FramObject.f_code 経由ではなく __bultin__.compile(.) 関数を使う方法もあります。

>type temp2.py
inGl = 5
def testF(inAg):
    inAg = 3
    inAg+=inGl
    return inAg

testF(4)

上のようにモジュール temp2.py ファイルに python コードを書き込んでおき、下の python コードを実行すれば、一応 virtual machine code を見ることができます。でも temp2.py ファイルの python source 文字列をコンパイルした後に逆アッセンブルしており、面倒です。

//@@
f = open('temp2.py', 'U')
codestring = f.read()
f.close()
codeobject = __builtins__.compile(codestring, 'temp2','exec')
import dis
dis.dis(codeobject)
//@@@
  1           0 LOAD_CONST               0 (5)
              3 STORE_NAME               0 (inGl)

  2           6 LOAD_CONST               1 (<code object testF at 0091FCA0, file "temp2", line 2>)
              9 MAKE_FUNCTION            0
             12 STORE_NAME               1 (testF)

  7          15 LOAD_NAME                1 (testF)
             18 LOAD_CONST               2 (4)
             21 CALL_FUNCTION            1
             24 POP_TOP             
             25 LOAD_CONST               3 (None)
             28 RETURN_VALUE        

なにより、temp2.py のような別ファイルに一旦書き出すのが面倒です。でも inspect のときのように、disassemble のための余分なコードが入らないのがメリットです。import pprint as pp などを追加すると、ローカル変数が増えてしまいます。モジュールの machine code が変わってしまいます。

でも FrameObject.f_code 経由でも、下のように dis() 関数を自分で書いてやることで、モジュール側を逆アッセンブルすることもできます。こうすれば、モジュール側への影響を少なくできます。

//@@
def dis():
    import dis;import inspect as ins;dis.dis(ins.stack()[1][0].f_code)

inAt = 3
print inAt
dis()
//@@@

私自身は上の dis() 関数を自分専用のモジュール kcommon.py の中にに書いてあります。それを下のように使っています。

//@@
inAt = 3
print inAt

import kcommon as kc;kc.dis()
//@@@
3
  1           0 LOAD_CONST               0 (3)
              3 STORE_NAME               0 (inAt)

  2           6 LOAD_NAME                0 (inAt)
              9 PRINT_ITEM          
             10 PRINT_NEWLINE       
                ・snip
                ・

どれを使ってモジュールやクラスのディス・アッセンブルするかは適宜判断願います。

inspect.getmebers をクラスに適用する

inspect.getmebers(.) は引数オブジェクトのメンバーをかき集めさて報告させる機能ですから、クラスにも使えます。下のような具合です。

//@@
class ClTest:
    m_inStt=5
    def __init__(self):
        self.m_in = 3

clAt = ClTest()
import inspect as ins;import pprint as pp;pp.pprint( ins.getmembers(clAt) )
print "----------------"
import inspect as ins;import pprint as pp;pp.pprint( ins.getmembers(ClTest) )
//@@@
C:\my\vc7\mtCm>python temp.py
[('__doc__', None),
 ('__init__',
  >),
 ('__module__', '__main__'),
 ('m_in', 3),
 ('m_inStt', 5)]
----------------
[('__doc__', None),
 ('__init__', ),
 ('__module__', '__main__'),
 ('m_inStt', 5)]

今度は object を継承させる、すなわち new style class に inspect.getmembers(.) を適用してみましょう。

//@@
class ClTest(object):
    m_inStt=5
    def __init__(self):
        self.m_in = 3

clAt = ClTest()
import inspect as ins;import pprint as pp;pp.pprint( ins.getmembers(clAt) )
print "----------------"
import inspect as ins;import pprint as pp;pp.pprint( ins.getmembers(ClTest) )
//@@@
C:\my\vc7\mtCm>python temp.py
[('__class__', ),
 ('__delattr__', ),
 ('__dict__', {'m_in': 3}),
 ('__doc__', None),
 ('__getattribute__', ),
 ('__hash__', ),
 ('__init__',
  >),
 ('__module__', '__main__'),
 ('__new__', ),
 ('__reduce__', ),
 ('__reduce_ex__',
  ),
 ('__repr__', ),
 ('__setattr__', ),
 ('__str__', ),
 ('__weakref__', None),
 ('m_in', 3),
 ('m_inStt', 5)]
----------------
[('__class__', ),
 ('__delattr__', ),
 ('__dict__', ),
 ('__doc__', None),
 ('__getattribute__', ),
 ('__hash__', ),
 ('__init__', ),
 ('__module__', '__main__'),
 ('__new__', ),
 ('__reduce__', ),
 ('__reduce_ex__', ),
 ('__repr__', ),
 ('__setattr__', ),
 ('__str__', ),
 ('__weakref__', ),
 ('m_inStt', 5)]



dis, inspect を使った一行コード

dis, inspect モジュールに関して、私がよく使う python 一行コードを下にまとめておきます。Python virtual machine が何をやっているのか覗き込みたいときに、これらの行を python source に挿入してやります。今までの説明で、各コードの意味は分ると思います。関数名 testF などのパラメータを適宜修正してご利用ください。

import dis; dis.dis(testF)
import kcommon as kc; kd.dis()
print testF.func_globals
print testF.func_code.co_names
import pprint as pp;pp.pprint(testF.func_globals )
import inspect as ins;import pprint as pp;pp.pprint(ins.getmembers(testF))
import inspect as ins;print dict(ins.getmembers(testF.func_code))['co_names'] 
import inspect as ins; print(ins.stack()[0][0].f_globals )
import inspect as ins; print(ins.stack()[0][0].f_globals )
import inspect as ins; import pprint as pp;pp.pprint(ins.stack()[0][0].f_globals )
import inspect as ins; import pprint as pp;pp.pprint(ins.stack()[0][0].f_locals )
import inspect as ins; import pprint as pp;pp.pprint(ins.stack()[0][0].f_builtins )
import inspect as ins; import pprint as pp;pp.pprint(ins.stack()[0][0].f_code.co_names )
import inspect as ins; import dis;dis.dis(ins.stack()[0][0].f_code)
import inspect as ins;import pprint as pp;pp.pprint( ins.getmembers(testF.func_code) )
import inspect as ins;import pprint as pp;pp.pprint( ins.getmembers(ClTest) )
import inspect as ins;import pprint as pp;pp.pprint( ins.getmembers(ins.stack()[0][0]) )

python コードの hack

今まで説明してきた dis モジュール, func_code オブジェクト、ceval.c, inspect モジュールを使って python が内部で行っている処理の様子を強引に覗き込んでみましょう。オープンな python は見ようと思えば、どこまででも覗き込ませてくれます。

x,y = y,x, x,z,y = z,y,x

まずは手始めに、最も単純な例として python virtual machine code による変数の入れ替えを見てみましょう。python では x,y = y,x や x,z,y = z,y,z といった文で変数の入れ替えを行えます。これをコンパイラはどのように行っているのでしょうか。python virtual machine code を見てみましょう。

//@@
x,y = (1,2)
x,y = y,x
print (x,y)
import kcommon as kc; kc.dis()
//@@@
(2, 1)
  1           0 LOAD_CONST               3 ((1, 2))
              3 UNPACK_SEQUENCE          2
              6 STORE_NAME               0 (x)
              9 STORE_NAME               1 (y)

  2          12 LOAD_NAME                1 (y)
             15 LOAD_NAME                0 (x)
             18 ROT_TWO             
             19 STORE_NAME               0 (x)
             22 STORE_NAME               1 (y)

  3          25 LOAD_NAME                0 (x)
             28 LOAD_NAME                1 (y)
             31 BUILD_TUPLE              2
             34 PRINT_ITEM          
             35 PRINT_NEWLINE       

x,y = y,x のような二つの変数の入れ替えは ROT_TWO 命令を使って data stack 上の上位二つの変数参照値を入れ替えているだけでした。下の擬似コードのように buffer 変数 tempAt を介在させるように真似はしていませんでした。コンパイラに効率的なコード生成をさせている努力が伺えます。

    tempAt = x
    x = y
    y = tempAt

x,z,y = z,y,z のように複数の変数の入れ替ええはどうやっているのでしょうか。

//@@
x,y,z = (1,2,3)
x,z,y = z,y,x
print (x,y,z)
import kcommon as kc; kc.dis()
//@@@
python temp.py
(3, 1, 2)
  1           0 LOAD_CONST               4 ((1, 2, 3))
              3 UNPACK_SEQUENCE          3
              6 STORE_NAME               0 (x)
              9 STORE_NAME               1 (y)
             12 STORE_NAME               2 (z)

  2          15 LOAD_NAME                2 (z)
             18 LOAD_NAME                1 (y)
             21 LOAD_NAME                0 (x)
             24 ROT_THREE           
             25 ROT_TWO             
             26 STORE_NAME               0 (x)
             29 STORE_NAME               2 (z)
             32 STORE_NAME               1 (y)

  3          35 LOAD_NAME                0 (x)
             38 LOAD_NAME                1 (y)
             41 LOAD_NAME                2 (z)
             44 BUILD_TUPLE              3
             47 PRINT_ITEM          
             48 PRINT_NEWLINE       

こんどは ROT_THREE と ROT_TWO と data stack 上のデータの二つの回転操作を組み合わせて x,z,y = z,y,x を実現していました。入れ替え操作を回転操作で実現しているとは以外でした。コンパイラで入れ替え状態を判定し ROT_TWO などの回転操作の組み合わせにしていけるなんて思いもしませんでした。

Python オブジェクト

少し踏み込んで、基本となる PyObject を見ておきます。 Python では全てをオブジェクトとして扱います。C source code 中では PyObject* として扱われます。C source では PyObject は下のように定義されています。

/* PyObject_HEAD defines the initial segment of every PyObject. */
#define PyObject_HEAD           \
    _PyObject_HEAD_EXTRA        \   # 非デバッグ時は空文
    int ob_refcnt;          \
    struct _typeobject *ob_type;


/* Nothing is actually declared to be a PyObject, but every pointer to
 * a Python object can be cast to a PyObject*.  This is inheritance built
 * by hand.  Similarly every pointer to a variable-size Python object can,
 * in addition, be cast to PyVarObject*.
 */
typedef struct _object {
    PyObject_HEAD
} PyObject;

要は Python のオブジェクトとは「レファランス・カウント:ob_refcnt」と「タイプ:ob_type」を最初の部分に備えた構造体だということです。例えば辞書オブジェクトは下のように定義されています。

typedef struct _dictobject PyDictObject;
struct _dictobject {
    PyObject_HEAD
    int ma_fill;  /* # Active + # Dummy */
    int ma_used;  /* # Active */

    /* The table contains ma_mask + 1 slots, and that's a power of 2.
     * We store the mask instead of the size because the mask is more
     * frequently needed.
     */
    int ma_mask;

    /* ma_table points to ma_smalltable for small tables, else to
     * additional malloc'ed memory.  ma_table is never NULL!  This rule
     * saves repeated runtime null-tests in the workhorse getitem and
     * setitem calls.
     */
    # 小さいときは ma_smalltable
    # 要素数が増えると、別に領域を確保する
    PyDictEntry *ma_table;
    # lookdict(.) or lookdict_string(.)
    PyDictEntry *(*ma_lookup)(PyDictObject *mp, PyObject *key, long hash);
    PyDictEntry ma_smalltable[PyDict_MINSIZE];
};

辞書・リスト・タプル・関数なんであっても、 最初の部分に PyObjectHead があるので、C souce code 中で PyObject* を使って cast できます。指し示せます。

struct _dictobject {
    PyObject_HEAD
    int ma_fill;  /* # Active + # Dummy */
    int ma_used;  /* # Active */
        ・
        ・
}

    PyDictObject                      ==>    PyObject
    ┌──────────────┐        ┌──────────────┐
    │int ob_refcnt;              │        │int ob_refcnt;              │
    │struct _typeobject *ob_type;│  ==>   │struct _typeobject *ob_type;│
    │                            │        └──────────────┘
    │int ma_fill;                │
    │int ma_used;                │
    │      ・                    │
    │      ・                    │
    │                            │
    └──────────────┘

Python オブジェクトの生成と抹消の管理

PyObject の ob_refcnt は reference count 方式でオブジェクトの生成・消滅を管理するために使われます。

ceval.c の中で machine code を実行させるたびに Py_INCREF(.), Py_INCREF(.), Py_XINCREF(.), Py_XDECREF(.) が使わます。このことは、python virtual macnine code を実行するたびにオブジェクトが生成され、また抹消されることを意味します。これらのマクロは object.h で次のように定義されています

#define Py_XINCREF(op) if ((op) == NULL) ; else Py_INCREF(op)
#define Py_XDECREF(op) if ((op) == NULL) ; else Py_DECREF(op)


#define Py_INCREF(op) (             \               # in object.h
    _Py_INC_REFTOTAL  _Py_REF_DEBUG_COMMA   \       # 非デバッグ時は空文
    (op)->ob_refcnt++)

#define Py_DECREF(op)                   \           
    if (_Py_DEC_REFTOTAL  _Py_REF_DEBUG_COMMA   \   # 非デバッグ時は空文
        --(op)->ob_refcnt != 0)         \
        _Py_CHECK_REFCNT(op)            \           # 非デバッグ時は空文
    else                        \
        _Py_Dealloc((PyObject *)(op))

以上より Python でのガーベージ・コレクションはレファランス・カウント方式で行われていることが解ります。オブジェクトのレファランス・カウントが 0 になるたびにオブジェクトが抹消されてメモリが開放されることが解ります。使わなくなったオブジェクト全部を纏めて一箇所で消しているわけではないと解ります。Python を組み込み制御用途に使ったとしても、Java のときのようにガーベージ・コレクションで応答できない期間が集中してしまうわけではないことが解ります。

実際に python の id(.) 組み込み関数がオブジェクトのメモリ・アドレスを返すこと利用して、下のようにリスト・オブジェクト[0,1] がメモリ上に配置される様子を覗けます。lstAt 変数で参照されたあとは、リスト・オブジェクト[0,1]は別のメモリ位置に生成されます。

//@@
def testF():
    print hex(id( [0,1] ))
    print hex(id( [0,1] ))

    lstAt = [0,1]
    print hex(id( [0,1] ))
    print hex(id( lstAt ))

testF()
import dis; dis.dis(testF)
//@@@
0x925698
0x925698
0x9252d8
0x925698
  2           0 LOAD_GLOBAL              0 (hex)
              3 LOAD_GLOBAL              1 (id)
              6 LOAD_CONST               1 (0)
              9 LOAD_CONST               2 (1)
             12 BUILD_LIST               2
             15 CALL_FUNCTION            1
             18 CALL_FUNCTION            1
             21 PRINT_ITEM          
             22 PRINT_NEWLINE       

  3          23 LOAD_GLOBAL              0 (hex)
             26 LOAD_GLOBAL              1 (id)
             29 LOAD_CONST               1 (0)
             32 LOAD_CONST               2 (1)
             35 BUILD_LIST               2
             38 CALL_FUNCTION            1
             41 CALL_FUNCTION            1
             44 PRINT_ITEM          
             45 PRINT_NEWLINE       

  5          46 LOAD_CONST               1 (0)
             49 LOAD_CONST               2 (1)
             52 BUILD_LIST               2
             55 STORE_FAST               0 (lstAt)

  6          58 LOAD_GLOBAL              0 (hex)
             61 LOAD_GLOBAL              1 (id)
             64 LOAD_CONST               1 (0)
             67 LOAD_CONST               2 (1)
             70 BUILD_LIST               2
             73 CALL_FUNCTION            1
             76 CALL_FUNCTION            1
             79 PRINT_ITEM          
             80 PRINT_NEWLINE       

  7          81 LOAD_GLOBAL              0 (hex)
             84 LOAD_GLOBAL              1 (id)
             87 LOAD_FAST                0 (lstAt)
             90 CALL_FUNCTION            1
             93 CALL_FUNCTION            1
             96 PRINT_ITEM          
             97 PRINT_NEWLINE       
             98 LOAD_CONST               0 (None)
            101 RETURN_VALUE        

print hex(id( [0,1] )) のたびに BUILD_LIST 命令で [0,1] のリスト・オブジェクトをヒープ・メモリ内に生成すると同時に PRINT_ITEM 処理が終わると、生成された [0,1] リスト・オブジェクトを抹消しています。二回目の print hex(id( [0,1] )) でも BUILD_LIST 命令で [0,1] のリスト・オブジェクトをヒープ・メモリ内に生成するのですが、前回の消されたメモリの場所に生成されるので、二回目の id( [0,1] ) が前回の値と同じ 0x925698 になるわけです。三回目に [0,1] のリスト・オブジェクトをヒープ・メモリ内に生成するときには、前回のオブジェクトが残っています。 lstAt 変数で参照されているためです。ですから 0x925698 とは別のメモリの位置に [0,1] のリスト・オブジェクト を生成せねばなりません。実際に、三回目の id( [0,1] ) は新しいメモリ・アドレス 0x9252d8 になっています。



関数

ここでは関数処理における python virtual machine code の様子を見ていきます。

関数呼び出し

まず関数呼び出しで python interpreter がどんなことをしているか python virtual machine code を使って見てみましょう。下の python code で関数の宣言と、関数の呼び出し時に実行される python virtual machine code を出力できます。

//@@
def testF(inAg):
    print inAg

testF(3)
import kcommon as kc;kc.dis()
//@@@
python temp.py
3
  1           0 LOAD_CONST               0 (<code object testF at 0091FC20, file "temp.py", line 1>)
              3 MAKE_FUNCTION            0
              6 STORE_NAME               0 (testF)

  4           9 LOAD_NAME                0 (testF)
             12 LOAD_CONST               1 (3)
             15 CALL_FUNCTION            1
             18 POP_TOP             
                ・snip
                ・

一行目の def testF(.) で testF のコード・オブジェクトを引き数に MAKE FUNCTION 処理を行わせています。 Interpreter である Python では関数の宣言にも実行が伴います。

4 行目の testF(.) では、testF ファンクション・オブジェクト(への参照)と引数 3 をスタックに乗せ CALL_FUNCTION を実行しています。キーワード引数がない単純な場合の python 関数呼び出しは C での関数呼び出しと似ています。

MAKE_FUNCTION

MAKE_FUNCTION の処理について、より突っ込んで見てみましょう。MAKE FUNCTION は関数オブジェクトを生成し、それを関数のラベルに対応させます。そのとき同時にグローバル変数とデフォルト引数値の設定を行わせます。下のテスト・コードから解るように、関数の宣言だけで、すなわち関数の実行を伴わない段階で、すなわち CALL_FUNCTION を実行する前に、下のように関数 testF(.) の デフォルト引数の値のタプルやグローバル変数の辞書に値が割り振られています。関数のグローバル変数と main module のグローバル変数とが全く同じであることも分ります。main モジュールで「inGlb=5」のようにグローバル変数を扱う行が新しく出てきて、それを実行すると、testF.func_globals の中身も同時に変わって行きます。

//@@
#07.06.27
def testF(inAg = 3):
    return inGlb + inAg + 1

import pprint as pp;pp.pprint(testF.func_defaults )
print "---------------------"
import pprint as pp;pp.pprint(testF.func_globals )
print "---------------------"
inGlb = 5
import pprint as pp;pp.pprint(testF.func_globals )
print "---------------------"
# global variables in main module
import inspect as ins; import pprint as pp;pp.pprint(ins.stack()[0][0].f_globals )
//@@@
(3,)
---------------------
{'__builtins__': ,
 '__doc__': None,
 '__file__': 'temp.py',
 '__name__': '__main__',
 'pp': ,
 'testF': }
---------------------
{'__builtins__': ,
 '__doc__': None,
 '__file__': 'temp.py',
 '__name__': '__main__',
 'inGlb': 5,
 'pp': ,
 'testF': }
---------------------
{'__builtins__': ,
 '__doc__': None,
 '__file__': 'temp.py',
 '__name__': '__main__',
 'inGlb': 5,
 'ins': ,
 'pp': ,
 'testF': }

ceval.c: C ソース・コードの、MAKE_FUNCTIONの処理内容が記述されている部分を下に示します。PyFunction_New(v, f->f_globals) で関数オブジェクトを生成しています。引数 v が code_object への参照値です。data stack に積まれています。PyEval_EvalFrame(PyFrameObject *f) の引数 f が呼び出し側が使っている FrameObject へのポインタです。f->f_globals 値を PyFunction_New(.) 引数に渡すことで、新しく生成する関数オブジェクトのグローバル変数へのポインタを、関数を宣言:def しているモジュールのグローバル変数へのポイン値に設定します。ですから上の testF(.) のコード例で関数のグローバル変数:testF.func_globals とモジュールのグローバル変数:ins.stack()[0][0].f_globals が全く同じ値になった訳です。両者が同じものをポイントしているのですから。

PyObject * PyEval_EvalFrame(PyFrameObject *f)
{
            ・
            ・
        case MAKE_FUNCTION:
                v = POP(); /* code object */
                x = PyFunction_New(v, f->f_globals);
                Py_DECREF(v);
                /* XXX Maybe this should be a separate opcode? */
                # oparg にはデフォルト値付き引数の個数が入っている
                if (x != NULL && oparg > 0) {
                        v = PyTuple_New(oparg);
                        if (v == NULL) {
                                Py_DECREF(x);
                                x = NULL;
                                break;
                        }
                        while (--oparg >= 0) {
                                w = POP();
                                #define PyTuple_SET_ITEM(op, i, v)
                                # (((PyTupleObject *)(op))->ob_item[i] = v)
                                PyTuple_SET_ITEM(v, oparg, w);
                        }
                        # FunctionObject.func_defaults を設定します
                        err = PyFunction_SetDefaults(x, v);
                        Py_DECREF(v);
                }
                PUSH(x);
                break;
            ・
            ・
            ・
}

MAKE_FUNCTION は関数の宣言の所で実行される python virtual machine code です。デバッガでシングル・ステップ動作させるとき、関数の宣言の場所で一回ずつ停止します。このとき MAKE_FUNCTION が実行されています。すなわち C ソースの この部分が実行されています。

ceval.c の MAKE_FUNCTION の部分のソースを追うことで、それが実行されるときの処理内容の詳細が解ります。下のことが解ります。

PyFunction_New(.) 関数の より詳細な動作を知るためには、PyFunction_New(.) 関数のソース・コードを追跡する必要があります。Googl Code Search を使えば簡単にソースに辿り着けます。例えば PyFunction_New 関数の処理内容を見たければここ を見てください。

この C ソース・コードの中の PyFunction_New(.) の処理内容を辿ってみれば MAKE_FUNCTION で

でも、これらの C ソース・コードを追跡することは、詳細になりすぎて本稿には相応しくありません。こちらでコードの追跡を行います。(まだ C ソースに少しの解説を入れているだけです。)

関数引数のデフォルト値を func_defaults 配列に設定する

関数のデフォルト引数値は関数オブジェクトに配列として設定されます。「def testF(inAg = 3):」のようなデフォルト引数値が与えられた関数では、MAKE_FUNCTION を実行する前に、デフォルト引数値(への参照)が data stack に積まれます。その積まれた値を関数オブジェクトの func_defaults 配列メンバーに設定します。

関数を宣言するときではなく、呼び出すとき「CALL_FUNCTION 1」のように CALL_FUNCTION のオペランドには引数の個数が設定されています。(この個数はコンパイル時に定まります。) 一方で既に MAKE_FUNCTION が FunctionObject.func_defaults[] 配列にデフォルト引数値を設定してくれています。この二つを使えば、デフォルト引数も含めた引数値が定まります。

//@@
def testF(inAg = 3):
    inAt = 4
    print inAt + inAg + 5

testF()
testF(4)
import kcommon as kc;kc.dis()
import pprint as pp;pp.pprint(testF.func_defaults )
print "-------------------"
print testF.func_code.co_varnames
//@@@
python temp.py
8
9
  1           0 LOAD_CONST          0 (3)  # 注:デフォルト引数値
              3 LOAD_CONST          1 (<code object testF at 0091FC20, file "temp.py", line 1>)
              6 MAKE_FUNCTION       1      # オペランドの 1 はデフォルト引数の個数です
              9 STORE_NAME          0 (testF)

  4          12 LOAD_NAME           0 (testF)
             15 CALL_FUNCTION       0
             18 POP_TOP             

  5          19 LOAD_NAME           0 (testF)
             22 LOAD_CONST          2 (4)
             25 CALL_FUNCTION       1
             28 POP_TOP             
                ・snip
                ・
(3,)
-------------------
('inAg', 'inAt')

MAKE_FUNCTION については ceval.c の下の部分で、デフォルト引数値を保持するタプルの生成と FunctionObject.func_defaults への設定を行っています。

PyObject * PyEval_EvalFrame(PyFrameObject *f)
{
            ・
            ・
        case MAKE_FUNCTION:
                    ・
                    ・
                # oparg にはデフォルト値付き引数の個数が入っている
                if (x != NULL && oparg > 0) {
                        v = PyTuple_New(oparg);
                        if (v == NULL) {
                                Py_DECREF(x);
                                x = NULL;
                                break;
                        }
                        while (--oparg >= 0) {
                                w = POP();
                                #define PyTuple_SET_ITEM(op, i, v)
                                # (((PyTupleObject *)(op))->ob_item[i] = v)
                                PyTuple_SET_ITEM(v, oparg, w);
                        }
                        err = PyFunction_SetDefaults(x, v);
                        Py_DECREF(v);
                }
            ・
            ・

CALL_FUNCTION

今度は関数の宣言ではなく呼び出しをするときに python interpreter が行っていることの詳細を見ていきましょう。

関数を呼び出すときは CALL_FUNCTION が実行されます。CALL_FUNCTION の処理についても、MAKE FUNCTION と同様に、 ceval.c ソース・コードを追跡することで、より詳細まで突っ込んで見ることができます。

PyObject * PyEval_EvalFrame(PyFrameObject *f)
{
                    ・
                    ・
                case CALL_FUNCTION:
                {
                        PyObject **sp;
                        #define PCALL(POS) pcall[POS]++
                        #As a result, the relationship among the statistics appears to be
                        #PCALL_ALL == PCALL_FUNCTION + PCALL_METHOD - PCALL_BOUND_METHOD +
                        #             PCALL_CFUNCTION + PCALL_TYPE + PCALL_GENERATOR + PCALL_OTHER
                        # 下の PCALL(.) はデバッグ時プロファイル情報を集めるためのマクロと推測します
                        PCALL(PCALL_ALL);
                        # stack_pointer = f->f_stacktop;
                        sp = stack_pointer;
#ifdef WITH_TSC
                        x = call_function(&sp, oparg, &intr0, &intr1);
#else
                        x = call_function(&sp, oparg);
#endif
                        stack_pointer = sp;
                        PUSH(x);
                        if (x != NULL)
                                continue;
                        break;
                }
                    ・
                    ・
                    ・

call_function(.) の呼び出し先の処理で FrameObject を新たに生成します。FrameObject.f_localsplus に co_nlocals + co_stacksize のサイズのローカル変数と両方を足し合わせたスタック領域をヒープ領域より一括して確保・設定します。その新しい FrameObject は call_function(.) の戻り値である関数オブジェクトの中に含まれています。新しい FrameObject の f_locals, f_globals ポインタ値を設定します。

Python では引数もローカル変数です。co_varnames[] 配列には引数も含んだローカル変数の文字列が入っています。co_varnames[] の始めの側に、引数の順序に従った引数変数文字列が入っています。func_code.co_varnames[] に引数変数文字列が入れてあるので、python のキーワード引数呼び出しが可能になります。

call_function(.) 関数の内部から呼び出す fast_function(.) が co_nlocals + co_stacksize のサイズのローカル変数とスタックの両方を足し合わせた領域をヒープ領域より確保します。関数呼び出し側の data stack に積んである引数値と、 MAKE_FUNCTION が設定してくれた FunctionObject.func_defaults の二つから、新規に確保した FrameObject.f_localsplus に、引数のローカル変数値を設定します。

この過程の詳細を理解するには、下の C 関数の sourse を追っていかねばなりません。でもそれは詳細になりすぎるのでこちら側に C sourse と説明を書いておきます。

case CALL_FUNCTION
        ↓

static PyObject *
call_function(PyObject ***pp_stack, int oparg)
        ↓DLL 関数か否かで処理を振り分けます

static PyObject *
fast_function(PyObject *func, PyObject ***pp_stack, int n, int na, int nk)
# 関数の FrameObject を生成します
          # デフォルト引数、キーワード引数がなければ PyEval_EvalFrame(.) を呼び出します
        ↓# デフォルト引数・キーワード引数があるとき下の処理が追加で行われます

PyObject *
PyEval_EvalCodeEx(PyCodeObject *co, PyObject *globals, PyObject *locals,
           PyObject **args, int argcount, PyObject **kws, int kwcount,
           PyObject **defs, int defcount, PyObject *closure)
          # キーワード引数・キーワード引数があるときの処理

        ↓# python machine code の実行
PyObject * PyEval_EvalFrame(PyFrameObject *f)

ローカル・グローバル変数とSTORE/LOAD_FAST, STORE/LOAG_GLOBAL

python における変数のスコープの規則は LEGB ルールで説明されます。この説明の背後にある考え方を、disassembler の出力と関数の func_code 属性から伺うことができます。

変数のスコープ:LEGB ルール

O'REYLLY から出版されている「初めての python」の「13 章:変数とスコープ」に、変数の参照は LEGB ルールで解決される、即ちlocal, enclosure, global, builtin の順序で参照先を探していくと書かれてています。ここでは closure は少し面倒なので後で扱うこととし、local, global builtin の三種類だけの例に限定して python virtual machine の視点から見てみましょう。

関数内で扱われる変数の参照の詳細な解決方法は、 disassembler 出力と関数の func_code 属性から理解できます。

//@@
inGlb = 3
def testF(inAg):
    global inGlb
    inAt = inAg + inGlb + 3
    inGlb = 4
    return inAg + inGlb

import dis;dis.dis(testF)
print "------------------"
print "co_varnames:", testF.func_code.co_varnames
print "co_names:   ", testF.func_code.co_names
print "co_consts:  ", testF.func_code.co_consts
#testF(3)
//@@@
  4           0 LOAD_FAST                0 (inAg)
              3 LOAD_GLOBAL              1 (inGlb)
              6 BINARY_ADD          
              7 LOAD_CONST               1 (3)
             10 BINARY_ADD          
             11 STORE_FAST               1 (inAt)

  5          14 LOAD_CONST               2 (4)
             17 STORE_GLOBAL             1 (inGlb)

  6          20 LOAD_FAST                0 (inAg)
             23 LOAD_GLOBAL              1 (inGlb)
             26 BINARY_ADD          
             27 RETURN_VALUE        
------------------
co_varnames: ('inAg', 'inAt')
co_names:    ('inAg', 'inGlb', 'inAt')
co_consts:   (None, 3, 4)

上の disassembler 結果よりわかるようにローカル変数には LOAD_FAST/STORE_FAST が、グローバル変数には LOAD_GLOBAL/STORE_GLOBAL が使われます

LOAD_FAST/STORE_FAST と LOAD_GLOBAL/STORE_GLOBAL の二種類の命令を使い分けて、変数へのアクセス方法を変えています。LOAD_FAST/STORE_FAST は関数呼び出しごとに設けられるローカル変数の配列にインデックスを使って高速にアクセスします。一方で LOAD_GLOBAL/STORE_GLOBAL 変数文字列を使ってグローバル変数辞書をアクセスします。ローカル変数へのアクセスのほうが、文字列の比較を伴うグローバル変数へのアクセスより倍程度早くなります。

ローカル変数とグローバル変数の違い

実は global 宣言していない変数でも co_names タプル側だけに配置され co_varnames 側に配置されていない変数は、LOAD_GLOBAL/STORE_GLOBAL が割り当てられます。関数内で使われている、 global 宣言されていない変数が local/global どちらになるかは、左値になるか否かで決まります。

//@@
inGlb = 3
def testF(inAg):
    #global inGlb
    inAt = inAg + inGlb + 3
    #inGlb = 4
    return inAg + inGlb

import dis;dis.dis(testF)
print "------------------"
print "co_varnames:", testF.func_code.co_varnames
print "co_names:   ", testF.func_code.co_names
print "co_consts:  ", testF.func_code.co_consts
#testF(3)
//@@@
  4           0 LOAD_FAST                0 (inAg)
              3 LOAD_GLOBAL              1 (inGlb)
              6 BINARY_ADD          
              7 LOAD_CONST               1 (3)
             10 BINARY_ADD          
             11 STORE_FAST               1 (inAt)

  6          14 LOAD_FAST                0 (inAg)
             17 LOAD_GLOBAL              1 (inGlb)
             20 BINARY_ADD          
             21 RETURN_VALUE        
------------------
co_varnames: ('inAg', 'inAt')
co_names:    ('inAg', 'inGlb', 'inAt')
co_consts:   (None, 3)

上の python virtual machine code を見れば解るように、ローカル変数には LOAD_FAST/STORE_FAST を使います。グローバル変数には LOAD_GLOBAL/STORE_GLOBAL を使います。上のコード例では inGlb は global ん源されていません。でも左値にもなっていないので global 変数とみなされています。

なお関数内でのモジュール名(下のコード例では bisec モジュール名)はローカル変数です。bisect の STORE/LOAD は STORE_FAST/LOAD_FAST 命令を使います。

//@@
def testF():
    import bisect
    print bisect
    print var

import dis;dis.dis(testF)
//@@@
  2           0 LOAD_CONST               0 (None)
              3 IMPORT_NAME              0 (bisect)
              6 STORE_FAST               0 (bisect)

  3           9 LOAD_FAST                0 (bisect)
             12 PRINT_ITEM          
             13 PRINT_NEWLINE       
             14 LOAD_CONST               0 (None)
             17 RETURN_VALUE        
__builtins__ 変数

ビルトイン変数も interpreter からみればグローバル変数です。ビルトイン変数にどんなものがあるかは下のコードにより調べられます。

//@@
import pprint as pp
pp.pprint( vars(__builtins__) )
//@@@
{'ArithmeticError': ,
    ・snip
    ・
 '__import__': ,
 '__name__': '__builtin__',
 'abs': ,
 'apply': ,
    ・
    ・
 'tuple': ,
 'type': ,
 'unichr': ,
 'unicode': ,
 'vars': ,
 'xrange': ,
 'zip': }

ビルトイン変数がグローバル変数であることは、次のコードの「print zip」分が LOAD_GLOBA になることから解ります。Python interpreter は FrameObject.f_globals 辞書にグローバル変数文字列が見つからなかったら、ビルトイン変数辞書を探すようにしているだけです。

//@@
def testF():
    print zip       # zip built-in function object

testF()
import dis; dis.dis(testF)
//@@@
python temp.py

  2           0 LOAD_GLOBAL              0 (zip) # 文字列 "zip" への参照
              3 PRINT_ITEM          
              4 PRINT_NEWLINE       
              5 LOAD_CONST               0 (None)
              8 RETURN_VALUE        
ビルトイン変数のスコープ

グローバル変数といっても、そのスコープはモジュール内に限られます。C 言語のときのように、プログラム全体に及ぶスコープを持つ変数はありません。

関数では MAKE_FUNCTION を実行したときに、参照しているグローバル変数オブジェクトと関数内のグローバル変数ラベルとの間の対応が定まります。一方で関数オブジェクトは持ち運び出来ます。別の変数スコープの環境で実行されます。でも関数オブジェクトの持ち運びでは、グローバル変数を保持する FunctionObject.f_globals も一緒に移動します。そうでなければ、動作が保証されません。

python では全ての変数はデフォルトで public です。モジュールの global 変数や builtin 変数でさえも、ユーザーの必要に応じて勝手に変更可能です。でも、public だからといって別モジュールに影響を与えるような変更はできません。builtin 辞書変数はモジュール内スコープごとに設けられているからです。どこかのモジュールで builtin 変数を勝手に変更していても、そのモジュールのユーザーには影響しません。注意深く かつ上手く考えられています。

function.func_globals
MAKE_FUNCTION を行っただけで FunctionObject は出来上がっています。FunctionObject.fun_globals が出来上がっています。CALL_FUNCTION は関数の呼び出しごとに FrameObject を作ります。
//@@
#print testF
#inGlb = 3
def testF(inAg=3):
    inAt = 5
    print inAg + inAt + inGlb
import inspect as ins; import pprint as pp; pp.pprint(ins.getmembers(testF) )
//@@@
[('__call__', ),
 ('__class__', ),
 ('__delattr__', ),
 ('__dict__', {}),
 ('__doc__', None),
 ('__get__', ),
 ('__getattribute__', ),
 ('__hash__', ),
 ('__init__', ),
 ('__module__', '__main__'),
 ('__name__', 'testF'),
 ('__new__', ),
 ('__reduce__', ),
 ('__reduce_ex__',
  ),
 ('__repr__', ),
 ('__setattr__', ),
 ('__str__', ),
 ('func_closure', None),
 ('func_code', <code object testF at 0091FC20, file "temp.py", line 3>),
 ('func_defaults', (3,)),
 ('func_dict', {}),
 ('func_doc', None),
 ('func_globals',
  {'__builtins__': ,
   '__doc__': None,
   '__file__': 'temp.py',
   '__name__': '__main__',
   'ins': ,
   'pp': ,
   'testF': }),
 ('func_name', 'testF')]
下のように FunctionObject.func_globals だけを取り出せます
//@@
#print testF

def testF(inAg=3):
    inAt = 5
    print inAg + inAt
# print testF.func_locals   # testF dose not have a func_locals attribute.
print testF.func_globals
//@@@
python temp.py
{'__builtins__': , '__name__': '__main__', '__file__': 'temp.py', '__doc__': None, 'testF': }

下のように関数と function.func_globals と FrameObject.f_globals は同じものです。C source 上では、どちらも同じ global 変数の辞書オブジェクトを指しています。

//@@
def testF(inAg=3):
    inAt = 5
    print inAg + inAt
    #import inspect as ins; import pprint as pp;pp.pprint(ins.stack()[0][0].f_globals )
    import inspect as ins; print(ins.stack()[0][0].f_globals )
print testF.func_globals
testF()
//@@@
python temp.py
{'__builtins__': , '__name__': '__main__', '__file__': 'temp.py', '__doc__': None, 'testF': }
8
{'__builtins__': , '__name__': '__main__', '__file__': 'temp.py', '__doc__': None, 'testF': }
function.func_defaults

関数引数にデフォルト引数があると func_defaults 要素が生成されます。MAKE_FUNCTION が FunctionObject.func_defaults を生成・設定します。

//@@
#07.06.27
def testF(inAg = 3):
    return inAg + 1

print "Default Argment Value:", testF.func_defaults
//@@@
Default Argment Value: (3,)

関数のデフォルト引数の値は MAKE_FUNCTION を行ったときに FunctionObject の中に蓄えられます。後で変更されることはありません。

//@@
inGlb = 3
def testF(inAg = inGlb):
    return inAg + 1

print testF(10)

inGlb = 5
print testF(10)
//@@@
11
11
function.func_dict

下のように、たとえ関数でもクラスの時のようにメンバーを追加できます。このとき func_dict には、この追加メンバーが入っています。このようなデータ・メンバーを関数に追加することが本当に必要かは疑問です。こんな使い方を関数に対して使う人は殆どいないと思います。ただ この例は python のオブジェクトは基本的な段階で辞書オブジェクトを付加できるるように作られていることを意味しています。

//@@
#07.06.27
def testF(inAg = 3):
    return inAg + 1

testF.m_in = 4
print "testF.func_dict:", testF.func_dict
//@@@
testF.func_dict: {'m_in': 4}
STORE_FAST/LOAD_FAST

今まで説明してきたローカル/グローバル変数についての知識を前提に、ここではローカル変数を data stack に出し入れする STORE_FAST/LOAD_FAST 命令について詳しくみてみましょう。

下のコード例で示されているように、ローカル変数 inAt1, inAt2 への読み書きは「STORE_FAST i/LOAD_FAST i」命令で行われます。

//@@
def testF():
    inAt1 = 3
    inAt2 = 5
    print inAt1 + inAt2
    f_locals
import dis;dis.dis(testF)
print testF.func_code.co_varnames
//@@@
  2           0 LOAD_CONST               1 (3)
              3 STORE_FAST               1 (inAt1)

  3           6 LOAD_CONST               2 (5)
              9 STORE_FAST               0 (inAt2)

  4          12 LOAD_FAST                1 (inAt1)
             15 LOAD_FAST                0 (inAt2)
             18 BINARY_ADD          
             19 PRINT_ITEM          
             20 PRINT_NEWLINE       

  5          21 LOAD_GLOBAL              2 (f_locals)
             24 POP_TOP             
             25 LOAD_CONST               0 (None)
             28 RETURN_VALUE        
('inAt2', 'inAt1')

「STORE_FAST i/LOAD_FAST i」命令のインデックス i は testF.func_code.co_varnames:('inAt2', 'inAt1') 配列のインデックスに対応します。ただし testF.func_code.co_varnames 自体は文字列の配列にすぎません。実際にデータが読み書きされるのは、FrameObject の中に作られるローカル変数領域です。co_varnames 文字列配列はコンパイル時に 「STORE_FAST i/LOAD_FAST i」のインデックス値を決めるのに利用されるだけです。

ローカル変数領域はヒープ領域に確保され、FrameObject の中に data stack 領域と一緒に保持されます。その開始アドレスが C ソース中では FrameObject.f_localsplus メンバーに記録されています。

ローカル変数領域はヒープ領域が data stack 領域と一緒に一塊として確保されるのには強い理由はありません。ローカル変数の数とスタック・サイズはコンパイル時に定まり、それがコードオブジェクトに記録されているため、CodeObject.co_nlocals + Code.Object.co_stacksize を計算して、まとめて両方のヒープ領域を確保したほうが処理時間を少しでも短くできる程度の意味しかありません。

「STORE_FAST i/LOAD_FAST i」命令の実際の処理内容を示す ceval.c のソース・コード部分を下に示します。

#define PyTuple_SET_ITEM(op, i, v) (((PyTupleObject *)(op))->ob_item[i] = v)

#define SETLOCAL(i, value)  do { PyObject *tmp = GETLOCAL(i); \
                                     GETLOCAL(i) = value; \
                                     Py_XDECREF(tmp); } while (0)
#define GETLOCAL(i) (fastlocals[i])

        co = f->f_code;
        names = co->co_names;
        consts = co->co_consts;
        fastlocals = f->f_localsplus;
                    ・
                    ・
                if (HAS_ARG(opcode))
                        # LOAD_FAST i などの opecode を持つ命令のとき
                        # i 値を oparg 変数に入れる
                        oparg = NEXTARG();
                    ・
                    ・
                case LOAD_FAST:
                        #define GETLOCAL(i) (fastlocals[i])
                        x = GETLOCAL(oparg);
                        if (x != NULL) {
                                Py_INCREF(x);
                                PUSH(x);
                                goto fast_next_opcode; 連続して次の処理に移る
                        }
                        format_exc_check_arg(PyExc_UnboundLocalError,
                                UNBOUNDLOCAL_ERROR_MSG,
                                PyTuple_GetItem(co->co_varnames, oparg));
                        break;
                        ・
                        ・
                case STORE_FAST:
                        v = POP();
                        SETLOCAL(oparg, v);
                        goto fast_next_opcode;

oparg に「STORE_FAST i/LOAD_FAST i」命令のオペ・コード、すなわちインデックス i の値が入ります。そのインデックス値に従って FrameObject.f_localsplus から始まるオート変数の参照の配列領域に書き込み、また読み込みをしています。LOAD_FAST 命令ではレファランス・カウントのインクリメントも行わせています。

また「STORE_FAST i/LOAD_FAST i」命令のすなわちインデックスは、 変数名文字列が入っている func_code.co_varnames[] のインデックスに対応することを、再度指摘しておきます。

STORE_GLOBAL/LOAD_GLOBAL

今度はグローバル変数の読み書きについて詳細に見て見ましょう。 グローバル変数のときは、ローカル変数のときのようにコンパイル時に定まる配列からインデックス数により値を取り出す真似ができません。Python のグローバル変数は下のコード例からも解るように、プログラムによってダイナミックに追加されていくものだからです。モジュールに作られるグローバル変数辞書の key 文字列と、関数側のグローバル変数文字列よりグローバル変数の値を取り出します。

//@@
# 07.07.16 add global variable by subroutin
def addToGlobalVariable(dctAg):
    dctAg['inGlb'] = "xyz"

def testF():
    print inGlb

addToGlobalVariable(globals())
testF()
import dis;dis.dis(testF)
print testF.func_code.co_names
//@@@
C:\my\vc7\mtCm>python temp.py
xyz
  6           0 LOAD_GLOBAL              0 (inGlb)
              3 PRINT_ITEM          
              4 PRINT_NEWLINE       
              5 LOAD_CONST               0 (None)
              8 RETURN_VALUE        
('inGlb',)

上のプログラムを解説します。ビルトイン関数 globals() は、モジュールのグローバル変数の辞書を返します。その辞書を addToGlobalVariable(.) 関数の引数に渡してやり、 ('inGlb', 'xyz') の key と value の組をグローバル変数辞書に追加しています。 testF() 関数の中で、追加された inGlb 変数の値をコンソールに打ち出しています。

Disassemble 結果の「LOAD_GLOBAL 0」命令が関数側の変数文字列配列よりインデックス 0 の文字列参照値を取り出して data stack に値を乗せる処理を行います。ここで「0」 は testF.func_code.co_names より変数名文字列 'inGlB' を取り出すためのインデックス値です。このインデックス値はコンパイル時に定まります。そして f_code.co_names[0] にある変数名文字列と FrameObject.f_globals ポインタより、グローバル変数が指し示すオブジェクトを 探し出し push(X):stack に乗せます。FrameObject.f_globals に探している文字列が無いときは FrameObject.f_builtins を探します

元々 MAKE_FUNCTION 命令ががモジュールの FrameObject のグローバル変数へのポインタを、関数オブジェクトの func_globals 要素に設定していました。この値を、CALL_FUNCTION が FrameObject を生成するとき FrameObject.f_globals にコピーします。このため FrameObject のグローバル変数は関数が宣言されたときのモジュールのグローバル変数へのポインタに設定されています。この FrameObject.f_globals 辞書より f_code.co_name[i] 変数文字列をキーとする値を「LOAD_GLOBAL i」によって取り出し data stack に乗せます。

LOAD_GLOBAL     i   # testF.func_code.co_names[i] の文字列配列より "inGlb" 文字列を取り出す
                ↓
            func_code->co_names[i] --> --> "inGlb" string # 変数名文字列
                                                ↓
                       function FramObject.f_globals ≡ module FrameObject.f_globals
                                                                            ↓
                                                               # モジュールのグローバル変数
                                                               # 辞書の key() に対応する
                                                               # value を取り出す
                                                               Module global variable

下に ceval.c にある STORE_GLOBAL/LOAD_GLOBAL の処理内容の部分を示します。

                case STORE_GLOBAL:
                        w = GETITEM(names, oparg);
                        v = POP();
                        err = PyDict_SetItem(f->f_globals, w, v);
                        Py_DECREF(v);
                        if (err == 0) continue;
                        break;
                            ・
                            ・
                case LOAD_GLOBAL:
                        # co_names[] よりインデックス oparg に対応する文字列への参照を w に与える
                        w = GETITEM(names, oparg);
                        if (PyString_CheckExact(w)) {
                                # w が文字列であり、こちら側で inline に、高速に処理する
                                # ここでグローバル辞書から w 文字列キーを持つ値を取り出す
                                /* Inline the PyDict_GetItem() calls.
                                   WARNING: this is an extreme speed hack.
                                   Do not try this at home. */
                                long hash = ((PyStringObject *)w)->ob_shash;
                                if (hash != -1) {
                                        PyDictObject *d;
                                        d = (PyDictObject *)(f->f_globals);
                                        x = d->ma_lookup(d, w, hash)->me_value;
                                        if (x != NULL) {
                                                Py_INCREF(x);
                                                PUSH(x);
                                                # LOAD_GLOBAL 処理が終わったので
                                                # 次の machine code の解析に移る
                                                continue;
                                        }
                                        # f->f_globals に w が見つからなかったので
                                        # f->f_builtins から w を探す
                                        d = (PyDictObject *)(f->f_builtins);
                                        x = d->ma_lookup(d, w, hash)->me_value;
                                        if (x != NULL) {
                                                Py_INCREF(x);
                                                PUSH(x);
                                                # LOAD_GLOBAL 処理が終わったので
                                                # 次の machine code の解析に移る
                                                continue;
                                        }
                                        goto load_global_error;
                                }
                        }
                        /* This is the un-inlined version of the code above */
                        x = PyDict_GetItem(f->f_globals, w);
                        if (x == NULL) {
                                x = PyDict_GetItem(f->f_builtins, w);
                                if (x == NULL) {
                                  load_global_error:
                                        format_exc_check_arg(
                                                    PyExc_NameError,
                                                    GLOBAL_NAME_ERROR_MSG, w);
                                        break;
                                }
                        }
                        Py_INCREF(x);
                        PUSH(x);
                        continue;

LOAD_FAST の簡潔な処理と比較すると、物々しくさえ感じます。グローバル変数辞書よりキー文字列を高速に検索するため hash を使っているので、コードは複雑になってしまいます。その詳細は PyDict_SetItem(.) 関数と PyDict_GetItem(.) 関数にあります。上の C ソースのボールド体文字の部分もグローバル変数辞書から変数名文字列のキーを見つけて値を取り出している部分です。

下のように decrement 実行時間を計測してみると global/local の違いで global のほうが二倍程度多くの処理時間がかかります。辞書からキー文字列を探し出す処理とローカル変数配列からインデックスで直接取り出す処理を直感的に比較するイメージからすると、倍で済むのが以外に感じます。この処理を高速化するため hash 値をオブジェクトに持たせ一回しか計算させないなど、様々のテクニックが使われています。私の力では簡潔に説明できません。興味のある方は C ソースを追ってください。

//@@
NGlb = 100000
inGlb = NGlb
def testGlobalF():
    global inGlb
    while(inGlb > 0):
        inGlb -= 1
        inGlb = inGlb   # repeat 10 times
        inGlb = inGlb   # to avoid CPU cash effect
        inGlb = inGlb
        inGlb = inGlb
        inGlb = inGlb

        inGlb = inGlb
        inGlb = inGlb
        inGlb = inGlb
        inGlb = inGlb
        inGlb = inGlb

def testLocalF():
    inAt = NGlb
    while(inAt > 0):
        inAt -= 1
        inAt = inAt     # repeat 10 times
        inAt = inAt     # to avoid CPU cash
        inAt = inAt
        inAt = inAt
        inAt = inAt

        inAt = inAt
        inAt = inAt
        inAt = inAt
        inAt = inAt
        inAt = inAt

import time
tmBeforeAt = time.clock()
testLocalF()
print time.clock()-tmBeforeAt

tmBeforeAt = time.clock()
testGlobalF()
print time.clock()-tmBeforeAt
//@@@
python temp.py
0.0623292609314
0.112814522461

STORE_GLOBAL/STORE_NAME

Python virtual machine code を見ていると、関数でもモジュールでも殆ど同じことをしていることが解ります。でも下のコード例でも見られるように関数では STORE_GLOBAL を使っていたのに、モジュールでは STORE_NAME を使うという微妙な違いがあります。ここでは STORE_GLOBAL と STORE_NAME で どんな違いがあるのかを調べます。

//@@
def testF():
    global inGlb
    inGlb = 3

inGlb = 5
print "------- disassemble testF() --------"
import dis; dis.dis(testF)
print "------- disassemble module --------"
import kcommon as kc; kc.dis()
import inspect as ins; print(ins.stack()[0][0].f_globals )
import inspect as ins; import pprint as pp;pp.pprint(ins.stack()[0][0].f_code.co_names )
//@@@
python temp.py
5
------- disassemble testF() --------
  2           0 LOAD_CONST               1 (3)
              3 STORE_FAST               0 (inAt)

  3           6 LOAD_FAST                0 (inAt)
              9 PRINT_ITEM          
             10 PRINT_NEWLINE       

  4          11 LOAD_GLOBAL              1 (inGlb)
             14 PRINT_ITEM          
             15 PRINT_NEWLINE       
             16 LOAD_CONST               0 (None)
             19 RETURN_VALUE        
------- disassemble module --------
  1           0 LOAD_CONST               0 (<code object testF at 0091FC20, file "temp.py", line 1>)
              3 MAKE_FUNCTION            0
              6 STORE_NAME               0 (testF)

  6           9 LOAD_CONST               1 (5)
             12 STORE_NAME               1 (inGlb)

  7          15 LOAD_NAME                1 (inGlb)
             18 PRINT_ITEM          
             19 PRINT_NEWLINE       
                ・snip
                ・

STORE_GLOBAL と STORE_NAME について python のレファランス・マニュアルには下のように書いてあります。

STORE_GLOBAL    namei 
Works as STORE_NAME, but stores the name as a global

STORE_NAMEとして機能しますが、グローバルとして名前を記憶します。


STORE_NAME    namei 
Implements name = TOS. namei is the index of name in the attribute co_names
of the code object. The compiler tries to use STORE_LOCAL or STORE_GLOBAL 
if possible. 

name = TOSを実行します。nameiはコードオブジェクトの属性co_namesにおけるnameの
インデックスです。コンパイラは可能ならばSTORE_LOCALまたはSTORE_GLOBALを使おう
とします。 

この説明では「よう分からん」という方が大部分だと思います。なので ceval.c に踏み込んで見てみます。

    case STORE_GLOBAL:
            w = GETITEM(names, oparg);
            v = POP();
            err = PyDict_SetItem(f-<f_globals, w, v);
            Py_DECREF(v);
            if (err == 0) continue;
            break;

    case STORE_NAME:
            w = GETITEM(names, oparg);
            v = POP();      # value
            if ((x = f-<f_locals) != NULL) {
                    #define PyDict_CheckExact(op) ((op)->ob_type == &PyDict_Type)
                    if (PyDict_CheckExact(x))
                            # f_locals が辞書:PyDict_Type なので STORE_GLOBAL の
                            # ときと同様に、変数名文字列を使って hash 値経由で 値
                            # を設定する。そのとき f_locals 辞書への key, value の
                            # 設定も行っていく。デフォルト辞書サイズでは足りなくな
                            # ったら辞書の作り直しも行う
                            err = PyDict_SetItem(x, w, v);
                    else
                            # w の ob_ival 属性を使って local 変数値をインデックスで設定する
                            err = PyObject_SetItem(x, w, v);
                    Py_DECREF(v);
                    if (err == 0) continue;
                    break;
            }
            PyErr_Format(PyExc_SystemError,
                         "no locals found when storing %s",
                         PyObject_REPR(w));
            break;
                 ・
                 ・
    case LOAD_NAME:
            w = GETITEM(names, oparg);
            if ((v = f->f_locals) == NULL) {
                    PyErr_Format(PyExc_SystemError,
                                 "no locals when loading %s",
                                 PyObject_REPR(w));
                    break;
            }
            if (PyDict_CheckExact(v)) {
                    # 
                    x = PyDict_GetItem(v, w);
                    Py_XINCREF(x);
            }
            else {
                    # FrameObject.f_locals 辞書から ob_ival 属性を使ってインデックスで設定する
                    x = PyObject_GetItem(v, w);f_globals, w);
                    if (x == NULL) {
                            x = PyDict_GetItem(f->f_builtins, w);
                            if (x == NULL) {
                                    format_exc_check_arg(
                                                PyExc_NameError,
                                                NAME_ERROR_MSG ,w);
                                    break;
                            }
                    }
                    Py_INCREF(x);
            }
            PUSH(x);
            continue;

要はローカル変数の存在しないトップレベルの処理でも、高速化のために関数の時と同様にインデックス使ったローカル変数配列でのアクセスを使うということです。もともとモジュールのトップ・レベルではローカル変数は存在しません。でも FrameObject はあるのですから、関数のときと同様にローカル変数領域を作ってやっても問題ありません。。実際に下のようにモジュールの f_locals/f_globals をinspect してやると f_local が現実には存在します。でも f_globals と同じ値になっています。g_globasl と f_locals の両方が実行に伴って増えていきます。

//@@
if True:
    inAt = 3    # inAt is global variable

print range     # range is built in function object
import inspect as ins; import pprint as pp;pp.pprint(ins.stack()[0][0].f_locals )
print "-----------------"
import inspect as ins; import pprint as pp;pp.pprint(ins.stack()[0][0].f_globals )
print "----------------------------------"
inGlb = 5
print range     # range is built in function object
import inspect as ins; import pprint as pp;pp.pprint(ins.stack()[0][0].f_locals )
print "-----------------"
import inspect as ins; import pprint as pp;pp.pprint(ins.stack()[0][0].f_globals )
print "----------------------------------"
import dis;import inspect as ins;dis.dis(ins.stack()[0][0].f_code)
//@@@
python temp.py

{'__builtins__': ,
 '__doc__': None,
 '__file__': 'temp.py',
 '__name__': '__main__',
 'inAt': 3,
 'ins': ,
 'pp': }
-----------------
{'__builtins__': ,
 '__doc__': None,
 '__file__': 'temp.py',
 '__name__': '__main__',
 'inAt': 3,
 'ins': ,
 'pp': }
----------------------------------

{'__builtins__': ,
 '__doc__': None,
 '__file__': 'temp.py',
 '__name__': '__main__',
 'inAt': 3,
 'inGlb': 5,
 'ins': ,
 'pp': }
-----------------
{'__builtins__': ,
 '__doc__': None,
 '__file__': 'temp.py',
 '__name__': '__main__',
 'inAt': 3,
 'inGlb': 5,
 'ins': ,
 'pp': }
----------------------------------
  1           0 LOAD_NAME                0 (True)
              3 JUMP_IF_FALSE           10 (to 16)
              6 POP_TOP             

  2           7 LOAD_CONST               0 (3)
             10 STORE_NAME               1 (inAt)
             13 JUMP_FORWARD             1 (to 17)
        <<   16 POP_TOP             

  4     <<   17 LOAD_NAME                2 (range)
             20 PRINT_ITEM          
             21 PRINT_NEWLINE       
                ・snip
                ・

モジュールのトップ・レベルの変数は global 変数です。でも処理していることは関数のオート変数と殆ど同じです。違いがあるとすれば、関数の FrameObject.f_globals は CALL_FUNCTION の処理中に設定されます。でもモジュールのトップ・レベルの処理では FramObject.f_globals を設定するのは自分自身です。だからこそモジュールでは STORE_FAST/LOAD_FAST の代わりにSTORE_NAME/LOAD_NAME 命令を使うことで、関数と殆ど同じ処理で済ませながら、モジュールの FrameObject.f_globals を設定していく作業もさせるようにしているのでしょう。

なお次に述べるクラスでも STORE_NAME/LOAD_NAME を使うのですが、こちらでは f_locals 側だけを追加していきます。

inspect 経由による クラスの disassemble

クラスと関数は virtual machine code の面から見ると よく似ています。実際クラスであっても、下の python コードを実行させたとき、クラスが引数なしの関数であるかのように実行されてしまいます。「print m_inStt」が実行されて 3 がコンソールに出力されます。

//@@
class ClTest:
    m_inStt = 3
    print m_inStt

    def __init__(self, inAg =3):
        self.m_in = inAg
//@@@
python temp.py
3

クラス宣言の箇所で python interpreter がどのようなことを 行っているかは、下のように python virtual machin code を吐き出させてやることで覗き込めます。

//@@
class ClTest:
    m_inStt = 3
    print m_inStt

    def __init__(self, inAg =3):
        self.m_in = inAg

import kcommon as kc; kc.dis()
//@@@
3
  1           0 LOAD_CONST               0 ('ClTest')
              3 BUILD_TUPLE              0
              6 LOAD_CONST               1 (<code object ClTest at 0091FCA0, file "temp.py", line 1>)
              9 MAKE_FUNCTION            0
             12 CALL_FUNCTION            0
             15 BUILD_CLASS         
             16 STORE_NAME               0 (ClTest)
                ・snip
                ・

# new type class
//@@
class ClTest(object):
    m_inStt = 3
    print m_inStt

    def __init__(self, inAg =3):
        self.m_in = inAg

import kcommon as kc; kc.dis()
//@@@
3
  1           0 LOAD_CONST               0 ('ClTest')
              3 LOAD_NAME                0 (object)
              6 BUILD_TUPLE              1
              9 LOAD_CONST               1 (<code object ClTest at 0091FCA0, file "temp.py", line 1>)
             12 MAKE_FUNCTION            0
             15 CALL_FUNCTION            0
             18 BUILD_CLASS         
             19 STORE_NAME               1 (ClTest)
                ・snip
                ・

クラス名 ClTest,継承クラス object,クラスの code object を data stack に乗せてから MAKE_FUNCTION を行わせる様子は、関数宣言での処理と同じです。関数宣言との違いは MAKE_FUNCTION の直後に引数 0 で CALL_FUNCTION も実行させていることです。この CALL_FUNCTION 呼び出しによって print m_inStt などが実行されます。

クラス自体の virtual machine code は次のようになります。

//@@
class ClTest:
    m_inStt = 3
    print m_inStt

    def __init__(self, inAg =3):
        self.m_in = inAg

    import kcommon as kc; kc.dis()
//@@@
3
  1           0 LOAD_GLOBAL              0 (__name__)
              3 STORE_NAME               1 (__module__) # クラスの __module__ 属性を指定する

  2           6 LOAD_CONST               1 (3)
              9 STORE_NAME               2 (m_inStt)

  3          12 LOAD_NAME                2 (m_inStt)
             15 PRINT_ITEM          
             16 PRINT_NEWLINE       

  5          17 LOAD_CONST               1 (3)
             20 LOAD_CONST               2 (<code object __init__ at 0091FC60, file "temp.py", line 5>)
             23 MAKE_FUNCTION            1
             26 STORE_NAME               3 (__init__)
                ・snip
                ・

最初の「LOAD_GLOBAL 0 (__name__)」,「STORE_NAME 1 (__module__)」 はクラスの __module__ 属性に '__main__' を設定しています。この処理は関数ではありませんでした。その次からは STORE_FAST/LOAD_FAST の代わりに STORE_NAME/LOAD_NAME を使うことを除けば関数のときと同じです。STORE_NAME/LOAD_NAME を使っていることより「クラスはモジュールと関数の両方の処理を行わせている」と言ったほうが より的確だと解ります。

なお import dis; dis.dis(ClTest) とやっても「m_inStt = 3」などのクラスの python 文の逆アッセンブルを行ってくれません。ClTest クラスを作った後では、ClTest 自体の code オブジェクトは使い終わって捨てられているからです。この例のように ClTest を作っている最中に inspect 経由で見てやらねばなりません。

また次のように class ClTest の f_locals/f_globals を覗き込んでやると、m_inStt, m_inStt2 変数はローカル変数であり、f_locas 辞書にダイナミックに追加されていくことが確認できます。モジュールのトップ・レベルのときとは違って f_global 変数には変数の追加を行いません。

class ClTest:
    m_inStt = 3
    import inspect as ins; import pprint as pp;pp.pprint(ins.stack()[0][0].f_locals )
    print "-----------------"
    import inspect as ins; import pprint as pp;pp.pprint(ins.stack()[0][0].f_globals )

    m_inStt2 = 5
    print "----------------------------------"
    import inspect as ins; import pprint as pp;pp.pprint(ins.stack()[0][0].f_locals )
    print "-----------------"
    import inspect as ins; import pprint as pp;pp.pprint(ins.stack()[0][0].f_globals )
//@@@
python temp.py
{'__module__': '__main__',
 'ins': ,
 'm_inStt': 3,
 'pp': }
-----------------
{'__builtins__': ,
 '__doc__': None,
 '__file__': 'temp.py',
 '__name__': '__main__'}
----------------------------------
{'__module__': '__main__',
 'ins': ,
 'm_inStt': 3,
 'm_inStt2': 5,
 'pp': }
-----------------
{'__builtins__': ,
 '__doc__': None,
 '__file__': 'temp.py',
 '__name__': '__main__'}

クラスとモジュールの f_locals/f_global の振る舞いの違いは FrameObject->f_locals->ob_type の違いで制御しているのだろうと推測しています。でも全くの推測です。ここらの説明をしてある資料を御存知の方は御教えください。

LOAD_ATTRI

モジュールやクラスのメンバーのアクセスの様子について調べてみましょう。

下のように LOAD_ATTR 命令を使って、モジュールのデータ要素(への参照)を TOS:Top of Stack に乗せます。

//@@
//@@
def testF():
    import numarray as sc
    print sc.zeros
import dis as kc; kc.dis(testF)
//@@@
python temp.py
  2           0 LOAD_CONST               0 (None)
              3 IMPORT_NAME              0 (numarray)
              6 STORE_FAST               0 (sc)

  3           9 LOAD_FAST                0 (sc)
             12 LOAD_ATTR                2 (zeros)
             15 PRINT_ITEM          
             16 PRINT_NEWLINE       
             17 LOAD_CONST               0 (None)
             20 RETURN_VALUE        
                ・snip
                ・

要素名文字列 'zero' は local 変数やグローバル変数ではなく、f_code.co_names[] に入っているとして扱われています。要素名文字列が取り出せたら、モジュールのグローバル変数やクラスの属性辞書から対応する辞書を取り出すのは LOAD_GLOBAL のときと同じです。hash を使って高速に取り出します。

//@@
def testF():
    import numarray as sc
    print sc.zeros
    import inspect as ins; import pprint as pp;pp.pprint(ins.stack()[0][0].f_globals )
    print "--------------------"
    import inspect as ins; import pprint as pp;pp.pprint(ins.stack()[0][0].f_locals )
    print "--------------------"
    import inspect as ins; import pprint as pp;pp.pprint(ins.stack()[0][0].f_code.co_names )
testF()
//@@@
python temp.py

{'__builtins__': ,
 '__doc__': None,
 '__file__': 'temp.py',
 '__name__': '__main__',
 'testF': }
--------------------
{'ins': ,
 'pp': ,
 'sc': }
--------------------
('numarray',
 'sc',
 'zeros',
 'inspect',
 'ins',
 'pprint',
 'pp',
 'stack',
 'f_globals',
 'f_locals',
 'f_code',
 'co_names')



リスト

ここでは python のリストを調べてみましょう。

Python のリストは、通常のコンピュータ・サイエンスの言葉で言えば参照の配列と言ったほうが近いと考えます。C 言語などの配列とは違うのは配列への追加や挿入が可能になっている点です。この python リストの様子を ceval.c ソースから見てみましょう。

下のコード例のように pyhon compiler はリスト [1,2,3] が与えられると、要素の値をスタックに積み BUILD_LIST 命令を行わせます。既存のリストへの追加があると IMPLOACE_ADD を行わせます。 この二つの命令を ceval.c ソースから見てみましょう。

//@@
# 07.08.14 test list
lstAt = [1,2,3]
print hex(id(lstAt))
lstAt += [4,5]
import kcommon as kc; kc.dis()
//@@@
0x9256e8
  1           0 LOAD_CONST               0 (1)
              3 LOAD_CONST               1 (2)
              6 LOAD_CONST               2 (3)
              9 BUILD_LIST               3
             12 STORE_NAME               0 (lstAt)

  2          15 LOAD_NAME                1 (hex)
             18 LOAD_NAME                2 (id)
             21 LOAD_NAME                0 (lstAt)
             24 CALL_FUNCTION            1
             27 CALL_FUNCTION            1
             30 PRINT_ITEM          
             31 PRINT_NEWLINE       
  3          32 LOAD_NAME                0 (lstAt)
             35 LOAD_CONST               3 (4)
             38 LOAD_CONST               4 (5)
             41 BUILD_LIST               2
             44 INPLACE_ADD         
             45 STORE_NAME               0 (lstAt)
                ・snip
                ・

BUILD_LIST 命令のオペランドはリストのサイズです。その C ソース・コードは下のようになっています。Python のリストは参照の配列であり、配列要素のサイズとは無関係に、リスト要素の数だけで、リストに必要なメモリ・サイズは決まってしまいます。

    if (HAS_ARG(opcode))
        oparg = NEXTARG();
            ・
            ・
    case BUILD_LIST:
            # リスト・サイズ分の領域を heap 領域より確保します。
            x =  PyList_New(oparg);
            if (x != NULL) {
                    # スタック上にあるリスト要素への参照値を、新たに確保したリスト領域にコピーする
                    for (; --oparg >= 0;) {
                            w = POP();
                            PyList_SET_ITEM(x, oparg, w);
                    }
                    PUSH(x);
                    continue;
            }
            break;

上の C ソースを見ていると BUILD_LIST 命令は下のことをしているだけです。

これだけを見ていると、python のリストは配列だと言いたくなります。でも単なる配列では追加や挿入ができません。それをどのように処理しているかは、下の INPLACE_ADD の C ソースを見ることで推測できます。

    # register PyObject *x;   /* Result object -- NULL if error */
    case INPLACE_ADD:
            w = POP();
            v = TOP();
            if (PyInt_CheckExact(v) && PyInt_CheckExact(w)) {
                    /* INLINE: int + int */
                    register long a, b, i;
                    a = PyInt_AS_LONG(v);
                    b = PyInt_AS_LONG(w);
                    i = a + b;
                    if ((i^a) < 0 && (i^b) < 0)
                            goto slow_iadd;
                    x = PyInt_FromLong(i);
            }
            else if (PyString_CheckExact(v) &&
                     PyString_CheckExact(w)) {
                    x = string_concatenate(v, w, f, next_instr);
                    /* string_concatenate consumed the ref to v */
                    goto skip_decref_v;
            }
            else {
              # 整数でも文字列でもないとき、例えば list のとき
              slow_iadd:
                    x = PyNumber_InPlaceAdd(v, w);
            }
            Py_DECREF(v);
      skip_decref_v:
            Py_DECREF(w);
            SET_TOP(x);
            if (x != NULL) continue;
            break;

リストの追加では PyNumber_InPlaceAdd(v. w) 関数を呼び出しています。この関数は abstract.c に下のように実装されています。

PyObject *
PyNumber_InPlaceAdd(PyObject *v, PyObject *w)
{
    # define NB_SLOT(x) offsetof(PyNumberMethods, x)
    # 「NB_」 はたぶん New style Binary operator の意味
    PyObject *result = binary_iop1(v, w, NB_SLOT(nb_inplace_add),
                       NB_SLOT(nb_add));
    if (result == Py_NotImplemented) {
        # list のときは binary operator がなく、こちら側の処理になる。
        PySequenceMethods *m = v->ob_type->tp_as_sequence;
        Py_DECREF(result);
        if (m != NULL) {
            binaryfunc f = NULL;
            if (HASINPLACE(v))
                f = m->sq_inplace_concat;
            if (f == NULL)
                f = m->sq_concat;
            if (f != NULL)
                return (*f)(v, w);
        }
        result = binop_type_error(v, w, "+=");
    }
    return result;
}

このソースは別の関数ポインタを呼び出しており、これだけでは詳細な追跡ができません。でも「concat: concatenate 【他動】結び付ける、鎖状につなぐ」の言葉より、二つのリスト PyObject* v, PyObject* w を c の ポインタにより繋げていくと読み取れます。Python のリストのデータ構造を擬似コードで表すと下のようになるものと思われます。

struct StPythonList{
    PyObject* m_arPyObject # = PyObject[N]
    int m_indexStart;
    int m_indexEnd;
    StPythonList* m_pStPythonListNext;
}

これを図示すると下のようになります

Python のリストが上のようなデータ構造になっていることは、下のテスト・コードからも裏付けられます。 id(.) 関数はメモリ・アドレスを返してくれます。これよりリスト・オブジェクトのメモリ配置アドレスがわかります。下のテスト・コードより「lstAt += [4,5] 」を行っても lstAt の配置アドレスが同じであることが解ります。リスト [4,5] を追加して配列のサイズが変わったとき、新たな配列領域を確保して二つのリスト要素をコピーするのではなく、既存のリストにポインタを経由して追加しているのだと推測されます。

//@@
# 07.08.14 test list
lstAt = [1,2,3]
print hex(id(lstAt))
lstAt += [4,5]
print lstAt
print hex(id(lstAt))
//@@@
0x9256e8
[1, 2, 3, 4, 5]
0x9256e8

一見すると面倒な、こんな処理をするのは、リストのサイズが大きくなったとき、二つのリストの要素を全部コピーし直していては処理時間がかかりすぎるためです。ポインタで繋ぎ合わせてやることで、既存リストのコピー処理の発生を抑えるためです。

ただし lstAt += lstAt2 のようになることもあるので、式の右側のリストについてはコピー・インスタンスを生成せねばなりません。このことは、下のテスト・コードより確認できます。

//@@
# 07.08.14 test list concatination time
import time
lstAt = [x for x in range(1000000)]
lstAt2 = [x for x in range(3)]
dbStartTimeAt = time.clock()
lstAt += lstAt2
print time.clock() - dbStartTimeAt

lstAt = [x for x in range(1000000)]
lstAt2 = [x for x in range(3)]
dbStartTimeAt = time.clock()
lstAt2 += lstAt
print time.clock() - dbStartTimeAt
//@@@
4.66137207168e-006
0.0114877698847

「大きなリスト += 小さなリスト」としたときは、小さなリストのコピー・インスタンスしか生成しません。短い処理時間で済みます。でも「小さなリスト += 大きなリスト」としたときは、大きなリストの・コピー・インスタンスを生成します。長い処理時間が必要になります。

以上見てきたように python のリストは C 言語などで言う配列に近いのですが、追加・挿入での余分なコピー・インスタンスの生成をさけるためにポインタでの追加・挿入される配列とのリンクを行うポインタを備えたデーター構造になっています。




hash

LOAD_GLOBAL, LOAD_ATTRI など python interpreter では変数名文字列による辞書からの値の取出しが多用されます。ローカル変数のときのように、コンパイル時に定まる配列では、実行中に変化するグローバル変数や要素を処理しきれないからです。ダイナミックに属性を変化させる interpreter では変数名文字列による値の取り出しと設定が必須です・

でも文字列検索による辞書からの値取出しでは配列よりもアクセス速度が遅くなってしまいます。辞書(map) データからキー文字列を逐次比較して値を取り出していたのでは、配列によるアクセスの何十倍ものアクセス速度の低下になってしまいます。しかし hash を使ってやることで、アクセス速度の低下を倍程度のオーダーに抑えられます。

ここでは、変数名文字列から対応する値を取り出す・設定するとき、python interpreter で hash がどのように使われているかを調べます。

hash とは

IT 用語辞典では「ハッシュ法」は次のように説明されています。
 データ検索アルゴリズムの一種で、もっともポピュラーなものの一つ。検索対象のデー
タを一定の規則にしたがってハッシュ値と呼ばれる整数に変換し、ハッシュ値を比較して
検索を行なう方式。

 文字列検索のように個々のデータが大きい場合、データ全体を比較しながら検索するよ
りも比較にかかるコストが節約でき、高速に検索できる。ただし、検索するデータの大き
さが小さければ、ハッシュ値に変換する手間が増えるためにかえって効率が悪くなること
もある。

 元のデータをハッシュ値に変換する関数をハッシュ関数と呼ぶが、すべてのデータが異
なるハッシュ値を持つように変換できれば、探したいデータのハッシュ値と同じハッシュ
値を持つデータを探すことで、探したいデータと同じデータを見つけることが可能になる。
しかし現実にはそのようなハッシュ関数を用意することは難しいため、ハッシュ値が重複
した場合(1つのハッシュ値に複数のデータがエントリーされる場合)を処理する方法を用
意しておく必要がある。

 ハッシュ値の重複を処理する代表的な方法としては、重複した場合に空いているハッシ
ュ値にデータを振り分ける「オープンアドレス法」や、同じハッシュ値を持つデータを線
形リストとして保存する「直接連鎖法」などがある。

python では hash 値を生成することは、オブジェクトを切り刻んで(hash して)、対応する 32bit integer を生成することを意味します。

  ┌──────┐
  │文字列などの│ hash(.)  ┌───────────┐
  │Python      ├────→│扱いやすい 32bit 整数 │
  │Object      │          └─┬─────────┘
  └──────┘              │% map size          
                                ↓
                            ┌────────────────┐
                            │map[]:(key,value) への参照の配列│
                            └────────────────┘

Python ソース・コードの変数文字列に対応する値を高速に取り出すために、「オープンアドレス法」での hash 関数を以下のように使います。

このような処理を行わせる hash 関数に必要な機能は 32bit 整数値へ変換することだけです。極端な話 0 しか返さない hash 関数でも、map から変数名文字列に対応する (key,value) ペアを取り出すことはできます。hash 関数が上手く散らばった値にならないと目的の key に辿り着くまでに手間がかかるだけです。

このような hash 値を使う理由は比較の高速化のためです。変数文字列と key 文字列の二つの hash 値が異なることを調べるだけで、両者の文字列が異なることが保障されるからです。hash 値が等しくても、変数文字列と key 文字列が等しいとは限りませんが、hash 値が異なれば変数文字列と key 文字列が異なることは保障されるので、32 bit 整数値を一回比較するだけ、両者が異なることを確認できてしまいます。'abcdefgX' と 'abcdefgY' 二つの文字列を最初の文字 a から逐次手間をかけて調べる必要がないからです。

hashable == imutable ではない

python ではタプル/リスト/辞書が数値や文字列と同様な基本要素であり、オブジェクトが hashable であるか否かが峻別されます。この hashable であることがリストとタプルの文脈で説明されるとき変更できないこと:imutable と同じであるかのように説明されることがあり混乱させられます。Python で hashable であるとは hash 関数を備えることです。imutable の意味ではありません。

たしかに、リストや辞書は変更でき、また hashable ではありません。それに比較して「数値の 1」や「文字列'abc'」「タプル (1,2,3)」などは変更できず、また hashable です。

でも下のコード例でも解るように class や class インスタンスは変更可能でありながら hashable です。タプルでもリストを要素にすると hashable ではありません。

//@@
class ClTest:pass

clAt = ClTest()
print hash(ClTest)
print hash(clAt)
print hash( (1,2,[1,2,3]) )
//@@@
9562512
9590424
Traceback (most recent call last):
  File "temp.py", line 6, in ?
    print hash( (1,2,[1,2,3]) )
TypeError: list objects are unhashable

hash 関数の種類

hash 関数が具体的にどんな風に計算しているのか調べてみたのですが、途中までしかわかりませんでした。整数の hash 値は整数値そのものでした。クラスやクラス・インスタンスの hash 値は id 値と同じでした。メモリ・アドレスでした。

//@@
class ClTest:pass

clAt = ClTest()
print "----------- id --------------"
print hex(id(128))
print hex(id(ClTest))
print hex(id(clAt))
print "---------- hash -------------"
print hex(hash(128))
print hex(hash(ClTest))
print hex(hash(clAt))
//@@@
----------- id --------------
0x953cc4
0x91e9c0
0x925698
---------- hash -------------
0x80
0x91e9c0
0x925698

文字列やタプルなどは切り刻んで 32bit 整数に変換しているのは解るのですが、その具体的な切り刻み方までは掴めませんでした。御存知の方は御教えください。

でも python における hash 値は「hash 値が異なるときは、異なったオブジェクトである」が満たされれれば何でも構いません。極端な話常に 0 を返すような hash 関数でも python は動きます。動作が遅くなるだけです。

逆に hash 値が同じであるからと言って辞書キーとしては重複しません。下のコード例がそれを示します。

//@@
print hex(hash(2))
print hex(hash(2**32+1))
dctAt={}
dctAt[2]="abc"
dctAt[2**32+1]="xyz"
print dctAt
//@@@
0x2
0x2
{2: 'abc', 4294967297L: 'xyz'}

ここの所を誤解していたため、私自身が無用な回り道をさせられたので書いておきます。



generator

ここでは python の generator を python virtual machine のレベルで調べて見ましょう。generator 関数のわかりにくい点は下の二つです。

以下、上の二つの様子をを詳細に調べてみましょう。

generatorObject の生成

ここでは python interpreter が generatorObject を生成する詳細な様子を覗き込んでみましょう。

for loop

実は python virtual machine code を見ているだけでは、generator の様子はあまり見えてきません。下のコードで リスト:range(3) の代わりに generator:xrange(3) にしても range が xrange に変わるだけです。

//@@
#for x in xrange(3):
for x in range(3):
    print x
import kcommon as kc; kc.dis()
//@@@
C:\my\vc7\mtCm>python temp.py
0
1
2
  2           0 SETUP_LOOP              25 (to 28)
              3 LOAD_NAME                0 (range)
              6 LOAD_CONST               0 (3)
              9 CALL_FUNCTION            1
             12 GET_ITER            
        >>   13 FOR_ITER                11 (to 27)
             16 STORE_NAME               1 (x)

  3          19 LOAD_NAME                1 (x)
             22 PRINT_ITEM          
             23 PRINT_NEWLINE       
             24 JUMP_ABSOLUTE           13
        >>   27 POP_BLOCK           

GET_ITER 命令により、TOS にあるシーケンス(この場合は range(3) が作ったリスト)で動くイタレータを作り TOS(Top Of Stack) に乗せます。FOR_ITER により TOS にあった iterator より一つ値を stack に積ませます。その TOS 上の値をローカル変数 x に代入したあとに、ループ処理を行っていきます。xrange(3) を使ったときは、作られる iteratorObject がリスト:[1,2,3] ではなく xrange(3) によって作られるものに変わるだけです。

9 CALL_FUNCTION の実行後          12 GET_ITER  の実行後                        12 GET_ITER  の実行後
                                                                              ├─────────┤
                                                                           TOS│ 1                │
   ├────────┤           ├─────────┤                      ├─────────┤
TOS│[1,2,3] への参照│        TOS│intrator への参照 ├→ iteratorObject     │intrator への参照 │
   ├────────┤           ├─────────┤        ([1,2,3])     ├─────────┤
   │                │           │                  │                      │                  │
   └────────┘           └─────────┘or iteratorObjet      └─────────┘
                                                             (xrange(3))

generator 関数の python virtual machine code

generator 関数の python virtual machine code を見ても yield 命令が挿入される以外は普通の関数と違いは見受けられません。

//@@
# 07.08.21 test generatro
def testGnrt():
    yield 1
    return 

for x in testGnrt():
    print x

import dis; dis.dis(testGnrt)
//@@@
python temp.py
1
  3           0 LOAD_CONST               1 (1)
              3 YIELD_VALUE         

  4           4 LOAD_CONST               0 (None)
              7 RETURN_VALUE        

generator 関数と普通の関数の、関数オブジェクトにおける 違い

Python interpreter にとって、 generator 関数と普通の関数の違いは CodeObject の co_flags に CO_GENERATOR:0x0020 フラグ・ビットが立っているか否かの違いが重要です。下のように、このフラグが立っていることを確認できます。

//@@
# 07.08.21 test generatro
def testGnrt():
    yield 1
    return 

gnrtAt = testGnrt()
import inspect as ins;print hex(dict(ins.getmembers(testGnrt.func_code))['co_flags'])
//@@@
0x63

//@@
# 07.08.21 test generatro
def testGnrt():
    #yield 1
    return 

gnrtAt = testGnrt()
import inspect as ins;print hex(dict(ins.getmembers(testGnrt.func_code))['co_flags'])
//@@@
0x43

ceval.c における CO_GENERATOR フラグの扱い

Python compiler は関数が yield 文を含むことを検出すると、その CodeObject の co_flags の CO_GENERATOR フラグを 1 にセットします。これが立つと CALL_FUNCTION で関数を呼び出していくとき、呼び出された関数の実行すなわち PyEval_EvalFrame(.) を呼び出す直前で、 generator オブジェクトを生成して戻り値とし、呼び出された関数を実行せずに return します。この部分の C ソース・コードを下に示します。

case CALL_FUNCTION
        ↓
static PyObject *
call_function(PyObject ***pp_stack, int oparg)
        ↓DLL 関数か否かで処理を振り分けます
static PyObject *
fast_function(PyObject *func, PyObject ***pp_stack, int n, int na, int nk)
        ↓# CO_GENERATOR フラグがたっているときの処理も追加で行われます
PyObject *
PyEval_EvalCodeEx(PyCodeObject *co, PyObject *globals, PyObject *locals,
{
            ・
            ・
        if (co->co_flags & CO_GENERATOR) {
                /* Don't need to keep the reference to f_back, it will be set
                 * when the generator is resumed. */
                Py_XDECREF(f->f_back);
                f->f_back = NULL;

                PCALL(PCALL_GENERATOR);

                /* Create a new generator that owns the ready to run frame
                 * and return that as the value. */
                return PyGen_New(f);
        }

        retval = PyEval_EvalFrame(f);
        
}
         ↓# python machine code の実行
PyObject * PyEval_EvalFrame(PyFrameObject *f)

このように CodeObject.co_flags の CO_GENERATOR が立っていることより、generator 関数を呼び出すとgenerator 関数の実行直前の段階の状態まで処理した状態で、例えば FrameObject も用意した状態で generatorObject を返します。これが generator 関数を呼び出したときに行われることです。



yield 文と context switch

generator 関数の yield 文は関数の実行を中断します。generatorObject.next() メソッドは、中断された処理を再開します。

を思い起こせは、関数を途中で中断させたり再開させたりすることが python では容易であるとが解るはずです。

Python 関数 ≡ FunctionObject + FrameObject

FrameObject ≡ Local 変数
             + int f_lasti  /* Last instruction if called */             + etc

yield 文で FrameObject.f_lasti を次に実行する python virtual machine code の値にして戻ります。generatorObject.next() が実行されたら FrameObject.f_lasti の位置から実行を再開します。ローカル変数は generatorObject 毎に設けられる FrameObject 内に保持し続けられています。

この意味で python の関数== FunctionObject + FrameObject は generatorObject にすることで実行途中の状態を容易に実現できます。そして rane(1000000) のような大きなリストの生成をさけるために、xrange(1000000) の generatorObject の実行途中の状態で値を返させています。

generator と co-operative thread

generatorObject が、関数の実行途中の状態で中断・再開できることは co-operative thread の基本機能があることを意味します。FrameObject を thread のコンテキストにできるからです。wait(delayAg=-1) と restart() メソッドを追加してやれば co-operative な thread を実現できてしまう。

一般には co-operative な thread より pre-emptive な thread のほうが高機能だとの誤解があるが、大多数のユーザーにとっては co-operative な thread のほうが扱いやすい。pre-emptive な thread はコンテキスト・スイッチがどんなタイミングで発生してもよいようにプログラムしなければならないからです。test and set の狭間に発生する頻度の少ない解りにくいバグの危険が伴うからです。(これについては、次の thread の節で、より詳しく考察します。)

Pre-emptive な thread が必要になることもあります。 mili second 以下のクリティカルなタイミングを保障せねばならないときは thread に優先順位を設定し、他の thead の実行を中断させて優先させるべき thead を実行せねばなりません。デバイス・ドライバで multi thread 記述をするならば、多くのばあい pre-emptive な thread が必要になるでしょう。

でも Windows/Linux といったアプリケーションで multi thread 記述をするときは mili second のようなクリティカルなタイミングが要求されることはありません。巨大な OS の管理の下で動くアプリケーション・プログラムでは一秒の応答速度の保障さえされないのが普通です。イメージ満載の web page を開いているのと並行して動作するアプリケーションが一秒の動作遅れを伴ってもしょうがありません。このようなアプリケーションでの multi thread 記述では co-operative な thread を使うべきです。

ユーザーがコンテキスト・スイッチのタイミングを明示的に記述する co-operative な thread ならば、test and set の狭間に発生するバグが発生しません。Lock や Critical Section などを使うことなく並列処理を記述できます。Co-operative な thread では test and set の狭間の問題が発生しないので並列処理記述が容易になります。

このような generatorObject を使った VrfyYThrd モジュールを作っています。近々公表します。そのときは遊んでみてください。

thread

ここでは python の thread で注意すべき点と対策について python virtual machine のレベルで考えます。Python での thread 実装方法について調べたあと、python thread の問題点について検討します。

python における thread の実装

Python の thread 制御は、Giant Lock フラグを参照しながら 100 python virtual machin instruction ごとに実行する OS thread を切り替えることで実現しています。

Python の関数は FrameObject と FunctionObject を組み合わせたものを対称に、その co_code 部分を PyEval_EvalFrame(.) で逐次実行させていました。ですから python virtual machine code を逐次実行していく PyEval_EvalFrame(.) を複数の OS thread 上で並列に実行させてさせてやることで、python の multi thread が実装できます。OS thread 上での並列実行により multi CPU による並列実行も可能になります。

    FrameObject                           │   FrameObject                           │・・ 
        /* Last instruction if called */  │       /* Last instruction if called */  │・・ 
    ┌─int f_lasti;                      │   ┌─int f_lasti;                      │     
    │      ・                            │   │      ・                            │     
    │  FunctionObjct                     │   │  FunctionObjct                     │     
    │      CodeObject                    │   │      CodeObject                    │     
    │          co_code                   │   │          co_code                   │     
    │            0 LOAD_CONST            │   │           0 LOAD_FAST              │     
    │            3 UNPACK_SEQUENCE       │   │           3 LOAD_FAST              │     
    └────→  6 STORE_NAME            │   │           6 BINARY_ADD             │     
                  9 STORE_NAME            │   └────→ 7 STORE_FAST             │     
                 12 STORE_NAME            │               10 LOAD_CONST             │     
                        ・                │                       ・                │     
                        ・                │                       ・                │     
                                          │                                         │     
                                          │                                         │     
    PyEval_EvalFrame(FrameObject* f)      │   PyEval_EvalFrame(FrameObject* f)      │     
    {                                     │   {                                     │     
            ・                            │           ・                            │     
            ・                            │           ・                            │     
        if PyDECREF(x){                                                              │     
                    <------- context switch -----> Py_INCREF(x)                      │     
            delete(x)                                                                │     
        }                                 │                                         │     
            ・                            │           ・                            │     
            ・                            │           ・                            │     
    }                                     │   }                                     │・・ 

でも PyEval_EvalFrame(.) 関数は reentrant ではありません。例えば上のように複数の thread で共用される python object x の reference count を decrement して python object を消去しようとする瞬間に context switch が発生して、別の thread で reference count を increment したら、存在し続けるはずの pytho object x はなくなってしまいます。PyEval_EvalFrame(.) 関数が reentrant でないことによる問題は発生する箇所が他にも多数存在しています。

Reentrant でない PyEval_EvalFrame(.) 関数を並列に実行させるために PyEval_EvalFrame(.) 関数の中で context switch を行わせる場所を、命令処理ループの最初の箇所に限定しています。、ceval.c ソースの下の部分が python thread の context switch タイミングを制御する C プログラムです。

# Giant Interpreter Lock
static PyThread_type_lock interpreter_lock = 0; /* This is the GIL */

int _Py_CheckInterval = 100;
volatile int _Py_Ticker = 100;
PyObject *
PyEval_EvalFrame(PyFrameObject *f)
{
        PyThreadState *tstate = PyThreadState_GET();
            ・
            ・
        tstate->frame = f;
            ・
            ・
        for (;;) {
                if (--_Py_Ticker < 0) {
                            ・
                            ・
                        _Py_Ticker = _Py_CheckInterval;
                            ・
                            ・
                        if (interpreter_lock) {
                                /* Give another thread a chance */

                                if (PyThreadState_Swap(NULL) != tstate)
                                        Py_FatalError("ceval: tstate mix-up");
                                PyThread_release_lock(interpreter_lock);
                                # Giant Lock を外したので、他の
                                # PyEval_EvalFrame(.) 関数を実行できる
                                /* Other threads may run now */

                                PyThread_acquire_lock(interpreter_lock, 1);
                                if (PyThreadState_Swap(tstate) != NULL)
                                        Py_FatalError("ceval: orphan tstate");

                                /* Check for thread interrupts */

                                if (tstate->async_exc != NULL) {
                                        x = tstate->async_exc;
                                        tstate->async_exc = NULL;
                                        PyErr_SetNone(x);
                                        Py_DECREF(x);
                                        why = WHY_EXCEPTION;
                                        goto on_error;
                                }
                        }
                        ・
                        ・
                        ・
                    switch (opcode) {
                        ・
                        ・
                    }
            }
        }

/* Other threads may run now */ の箇所で、Giant Lock:OS の thread 切り替えロックを外し、別の PyEval_EvalFrame(.) 関数に実行を移せるようにしています。Context switch させるのを、この位置だけに限定することで、reentrant でない PyEval_EvalFrame(.) 関数の並列実行を可能にしています。

_PyTicker は命令の処理回数を数える static な変数です。一つの python 命令ごとに PyEval_EvalFrame(.) 関数を切り替えていては thread 切り替えのために OS CPU 処理時間が消費され python interpretor の実行が遅くなってしまうので、 _Py_Ticker:100 回ごとに PyEval_EvalFrame(.) 関数を切り替えています。

この設定回数は下のように python コードから確認できます。

//@@
# 07.08.22 test getcheckinterval()
import sys
print sys.getcheckinterval()
//@@@
100

I/O 処理の最中など PyEval_EvalFrame(.) 関数を切り替えてはまずいタイミングもあります。そのときのたにめ Giant Lock フラグ:interpreter_lock が設けられています。interpreter_lock == 0 であるかぎり、PyEval_EvalFrame(.) 関数の切り替えは発生しません。なお interpreter_lock は threading.Lock() とは別物です。ユーザーが interpreter_lock を利用するとしたら C API を使ったときに C 関数で記述した関数の threadsafe を保障したいときなどに限られます。

thread programm 記述の問題点

Python の thread は pre-emptive です。前の節で述べたように python の multi thread は _Py_Ticker:100 命令ごとに thead を切り替えます。ユーザーには thread のどこで context switch が発生するか解りません。Python Program の任意の箇所で context switch が発生しえます。プログラムの流れ次第です。100 命令ごとに別の thread を切り替える様子は time slice による thread 切り替えに似ています。

一方で複数の thread の間には共通変数が存在することが多くあります。Thread のどこでも context switch が発生しえることと共通変数が存在することが組み合わさることで、 multi thread 特有のバグが入り込んできます。再現性のない発生頻度の少ない嫌らしいバグです。

以下では、この multi thread 特有のバグを単純例で検討します。それを python の random モジュールで より具体的な形で再検討します。

thread 間の共通変数の問題

Pre-emptive な multi thread programming では複数の thread から並列してアクセスされる変数が誤動作を引き起こします。下の例で考えてみましょう。

                    denominatorGlb                                      
                       |                                                
         +-------------+-----------------+----------------  ・・・・    
         |                               |                              
    def thread_1():                 def thread_2():         ・・・・    
            ・                             ・                           
            ・                             ・                           
         denominatorGlb = 3                ・                           
            ・                             ・                           
            ・                             ・                           
            ・    ←- context switch → denominatorGlb = 0              
         someAt /= denominatorGlb       someAt *= denominatorGlb        
            ・                             ・                           
            ・                             ・                           

上のコード例で denominatorGlb が thread_1 と thread_2 の両方で読み書きされています。thead_1 で demoninatorGlb = 3 と設定しても someAt /= denominatorGlb を実行するときに denominatorGlb が 3 の値を保っている保障がありません。 someAt /= denominatorGlb を実行するまでに context switch が発生して、thread_2() が実行され denominatorGlb が 0 に書き換わっている可能性があります。

このバグは広域的です。遠く離れたところ:thread_2() の動きが絡んできます。thread_1() だけを見ていると、demoninatorGlb = 3 と設定しているので、 someAt /= denominatorGlb は正常に実行されるように見えてしまいます。実際にも、それを実行するまでの間に context switch が発生しなければ、すなわち大部分のタイミングでは正常に動作してしまいます。バグが発生するのはクリティカルなタイミングに限られます。しかも、このバグは thread_1() のコードを眺めているだけでは原因をつかめません。遠く離れているかもしれな、別のファイルにあるかもしれない thread_2() の動き方によって誤動作となります。

                    denominatorGlb                                      
                       |                                                
         +-------------+-------------------------+----------------  ・・・・    
         |                                       |                              
    def thread_1():                         def thread_2():         ・・・・    
            ・                                     ・                           
            ・                                     ・                           
         denominatorGlb = 3                      ・                      
            ・                                     ・                           
            ・                                     ・                           
            if denominatorGlb != 0:               
                  ←- context switch --------→ denominatorGlb = 0       
                someAt /= denominatorGlb
                                                someAt *= denominatorGlb        
            ・                                     ・                           
            ・                                     ・                           

上のように、割り算をする前に「if denominatorGlb != 0:」判定してやっても対策にはなりません。「if denominatorGlb != 0:」判定の後にコンテキスト・スイッチが発生したら同じバグが発生するからです。たしかに if 判定の追加によりバグの発生頻度は少なくなります。でもバグの再現性が悪くなることでもあり、より性質の悪いバグになってしまいます。

Multi thread programming でのバグは再現性が悪い、また発生頻度が少ないために厄介なバグとなりがちです。demoninatorGlb = 3 と設定しているので、 someAt /= denominatorGlb を実行するまでの間に context switch が発生しなければ正常に動作してしまうからです。

Multi thread programming で共通変数が確実に存在しないようにプログラムすることは簡単ではありません。Python では import したモジュール側に global 変数が存在しうるからです。Threadsafe モジュールであれば共通変数になりうる変数は設けません。でも python で threadsafe であると明言しているモジュールは殆どありません。

thread 間の共通変数の test and set 問題

共通変数が原理的に避けられないことも多くあります。下のように一台の LAN プリンタに複数の thread が一まとまりの文章を出力している状態を考えましょう。thread_1() が文章出力中は thread_2() は LAN プリンタに出力できません。thread_2() は LAN プリンタが空いているときしか出力できません。常に一つの thread のみが LAN プリンタに出力していることを保障するためには thread 間で共通して読み書きされる状態変数が必要になります。

一つしかないリソースを複数の thread が使うときは排他的に動作せざるをえません。一つのリソースを複数のスレッドで排他的に使うには、そのスレッドに共通する global な状態変数がどうしても必要になります。下のように blUsingGlb を設けて blUsingGlb == False の時に LAN プリンタを使うようにすれば良さそうに見えます。Thread が LAN プリンタに出力するまえに blUsingGlb = True 状態にし、LAN プリンタ に出力するのを完了した跡に blUsingGlb = False に戻してやれば排他的に動きそうです。

                    LAN_Printer
                    blUsingGlb = False
                       |                                                
         +-------------+-----------------+----------------  ・・・・    
         |                               |                              
    def thread_1():                 def thread_2():         ・・・・    
            ・                             ・                           
            ・                             ・                           
         if blUsingGlb == False:        if blUsingGlb == False:         
                        ←- context switch →                           
            blUsingGlb = True:             blUsingGlb == True:          
            for strAt open(fileName):      for strAt open(fileName):    
                print strAt,                    print strAt,            
            blUsingGlb = False:            blUsingGlb == False:         
                ・                             ・                       
                ・                             ・                       

一見しただけだと、これで良さそうに思えてしまいます。でも、これでは誤動作します。ただしプログラムの誤動作するタイミングが限られます。 if blUsingGlb == False の test をしてから blUsingGlb = True の値を set するまでの間に context switch が発生して、なおかつ、その context switch で別の thread が if blUsingGlb == False チェックを行うクリティカルなタイミングのときに限って、LAN プリンタに同時に出力する誤動作が発生します。これが Multi thread programming における「test and set の狭間の問題」です。大部分のタイミングでは正常に動作してしまうため、再現性のないバグとなる嫌らしい問題です。

この「test and set の狭間の問題」を避けるために threading.Lock() が用意されています。下のように使います。

                    LAN_Printer
                    lockGlb = threading.Lock()
                       |                                                
         +-------------+-----------------+----------------  ・・・・    
         |                               |                              
    def thread_1():                 def thread_2():         ・・・・    
            ・                             ・                           
            ・                             ・                           
        lockGlb.acquire()               lockGlb.acquire()               
        for strAt open(fileName):       for strAt open(fileName):       
           print strAt,                     print strAt,   
        lockGlb.release()               lockGlb.release()               
                ・                             ・                       
                ・                             ・                       

Lock オブジェクト lockGlb については、Lock() の最中には「test and set の狭間の問題」が発生しないように context switch を禁止するなどの対策を言語処理系の中で行っています。

問題を発生させる thread 共用変数・共用オブジェクトについて、threading.Lock() などを使ってプログラマーが排他的であることを保障せねばなりません。でも他人が作ったモジュールの中に発生する thread 共用変数までは注意し切れません。

実際に multi thread programming で一番注意しなければならないのが、この一見動いてしまえる 「test and set の狭間の問題」です。クリティカルなタイミングを除いて動作してしまうからです。プログラム自体は multi thread programming でなければ正しいプログラムです。Context switch のことを意識していない限り、つい記述してしまうプログラムです。普通の関数呼び出しでは頻繁に行っている記述だからです。条件判断が関数内でなされていたら 「test and set の狭間の問題」の発生箇所であるとさえ意識するのが難しくなります。

                    somModule
                    someModule.global 変数 1
                    someModule.global 変数 2
                            ・
                            ・
                       |                                                
         +-------------+-----------------+----------------  ・・・・    
         |                               |                              
    def thread_1():                 def thread_2():         ・・・・    
            ・                             ・                           
            ・                             ・                           
        import someModule               import someModule               
        if someModule.test():           if someModule.test():           
            someModule.set()                someModule.set()            
                ・                             ・                       
                ・                             ・                       

「test and set の狭間の問題」の嫌らしさは、バグの発生がクリティカルなタイミングに限られることです。通常のプログラム・テストでは見逃されるバグになりやすいことです。大部分のタイミングでが正常に動作してしまうことです。大規模なプログラムにおける再現性のない発生頻度の少ないバグの嫌らしさは職業プログラマーならば身にしみて理解してもらえるはずです。市場に出てからの御客のところで一ヶ月に一回程度の頻度で発生する、請求金額の桁違いなどの致命的なバグの怖さを理解してもらえるはずです。

Co-operative な thread ならば、クリティカルなタイミングに限られたバグが入り込みません。Context switch を行われるのは、wait(.) や yield などのコンテキスト・スイッチを発生させる関数や命令を実行したときに限られるからです。Co-operative な thread ではプログラマーが context switch のタイミングを明示的に記述するからです。共通変数によるバグの問題は残りますが、上のような自前の排他制御だけで対処できます。pre-emptive な thread でのバグに比較すれば、バグの頻度・再現性の面で素性の良いバグに収まります。もっと co-operative な thread の利用を考えるべきだと主張します。

random モジュールを例に使った thread 間共通変数問題の検討

Python の random モジュールを例に、thread 間共通変数の問題を より具体的に検討してみましょう。random モジュールは python モジュールでは珍しく threadsafe を明記しています。

python -m pydoc random
        ・
        ・
    * The random() method is implemented in C, executes in
      a single Python step, and is, therefore, threadsafe.
        ・

通常は random モジュールを下のように使います。

//@@
# coding=shift_jis
# 06.10.29 test random
import random as rm
print rm.seed(1)    # 再現性が必要なとき種(seed)を設定する
print rm.random()   # 一個の random value
print [rm.random() for x in range(10)]
#print [rm.random() in range(10)]   # [False] になる
//@@@
0.286764866026
[0.9478009373832299, 0.083293806704341389, 0.69287751155037047, 0.11174129451311121
, 0.94066583365119105, 0.075509802509286628, 0.48949937014030143, 0.65659243274936629
, 0.92908348489143999, 0.8888168647683683]

上のように単純な記述ができるのは random モジュールに Random クラスのインスタンスがモジュール内 global 変数 _inst として作られているからです。random モジュールの seed 関数のラベルに対応させられているのは _inst.seed の callable な関数オブジェクトだからです。

>type C:\lng\python24\Lib\random.py
    ・
    ・
_inst = Random()
seed = _inst.seed
    ・
    ・
getrandbits = _inst.getrandbits
    ・

random モジュール内で _inst グローバル変数を使っているため、下のように普通の random モジュールの使い方を multi thread programming でも行ってしまうと、thread 間共通変数 random._inst が random モジュール内部で使われてしまいます。下のように thread_1() で rm.seed(1) に設定したのに、thread_2() が動くと rm.seed(2) に変更されてしまいます。

    import random as rm

    def thread_1():                 def thread_2():         ・・・・    
            ・                             ・                           
            ・                             ・                           
        rm.seed(1)                      rm.seed(3)
        valeuAt = rm.random()           valeuAt = rm.random()
            ・                             ・                       
            ・                             ・                       

Threadsafe なプログラムを書くためには、random._inst を使わない書き方をする必要があります。次のように書く必要があります。Thread 間共通変数を発生させない書き方にする必要があります。

    import random as rm

    def thread_1():                 def thread_2():         ・・・・    
            ・                             ・                           
            ・                             ・                           
        clRandomAt = rm.Random()        clRandomAt = rm.Random()
        clRandomAt.seed(1)              clRandomAt.seed(3)
        valeuAt = clRandomAt.random()   valeuAt = clRandomAt.random()
            ・                             ・                       
            ・                             ・                       

Threadsafe をうたっている random モジュールでさえ そのソースを追うレベルで理解していないと thread 間共通変数が紛れ込んでしまいます。Threadsafe ではなくなってしまいます。Threadsafe を考慮していない一般のモジュールを利用したとき、その threadsafe を保障する責任は一般モジュールのユーザーにあります。プログラマーは利用モジュールのソースを全て追って threadsafe であることを確認した後でなければ threadsafe なプログラムを書けません。利用モジュールが呼び出している先のモジュールまで含めたソース確認が必要になるので、利用モジュールの threadsafe を保障するのは現実には不可能になることも多くなります。

私は python の thread/threading モジュールで thread safe なプログラムを書けるのは非常に限られると主張します。1000 行を超える規模の thread/threading を使った、thread safe で実用的意味のある 並列処理 thread 関数群の作成は神業に近いと主張します。





decorator 構文のハック

decorator 構文は inspect や disassemble を使ったほうが よく解ります。言葉でいくら丁寧に説明しても関数が三重にネストする理由は簡単には理解できないでしょう。逆に ここまでの disassemble/inspec/virtual machine code が解っているならば、それらを使って python の decorator 構文コードを追跡することで、@decorator 構文の意味を完全に理解できます。

Python のデコレーター構文 @dcrt は関数の置き換えと、closure の生成の二つの意味があります。この二つの詳細を以下覗き込んで行きます。

置き換え: syntax sugar

Python のデコレーター構文が関数の置き換えであることは、次のコードが吐き出す disassemble 結果を見てやれば一目瞭然です。

//@@
def dcrtF(fnAg):
    print "Now in dcrtF(.):",fnAg
    inFreeAt = 1

    def innerF(objAg):
        print inFreeAt

        import time
        print "Now innerF(.)", objAg
        dbStartTimeAt = time.clock()
        fnAg(objAg)
        print time.clock() - dbStartTimeAt

    return innerF

@dcrtF
def testF():pass
import kcommon as kc;kc.dis()
//@@@
Now in dcrtF(.): 
  1           0 LOAD_CONST               0 (<code object dcrtF at 0091FD20, file "temp.py", line 1>)
              3 MAKE_FUNCTION            0
              6 STORE_NAME               0 (dcrtF)

 16           9 LOAD_NAME                0 (dcrtF)
# testF() 関数オブジェクトを生成し data stack に乗せる
             12 LOAD_CONST               1 (<code object testF at 0091FD60, file "temp.py", line 16>)
             15 MAKE_FUNCTION            0
# data stack 上にある testF 関数オブジェクト引数にして dcrtF(.) を CALL する
             18 CALL_FUNCTION            1
             21 STORE_NAME               1 (testF)

18 バイト目の CALL_FUNCTION で testF 関数オブジェクトを引数にして dcrtF 関数を呼び出します。その戻り値がスタックに残されて、ついで testF ラベルの位置に STORE_NAME されます。testF ラベルの中身が dcrtF(.) 関数の戻り値:関数オブジェクト:closure に置き換えられます。

closure

closure とは、ネストした関数の内側から、外側の関数のローカル変数を参照しているときの内側の関数のことを言います。

           ┌──┐
def outerF(│ag1 │, ag2, ...):
  ┌──┐ └──┘
  │at1 │= 3
  └──┘
    print ag2

    def innerF()
              ┌──┐  ┌──┐
        inAt =│at1 │+ │ag1 │
              └──┘  └──┘
        print inAt

外側の関数
outerF.func_code.co_cellvars:('ag1','at1')
outerF.func_code.co_freevars:()

内側の関数
innerF.func_code.co_cellvars:()
innerF.func_code.co_freevars:('ag1','at1')

上の例のように内側の関数 innerF(.) は、外側の関数 outerF(.) のローカル変数のうち ag1, at1 の二つの変数をクロスして参照しています。(なお python では引数もローカル変数です ag1 もローカル変数です。)

ネストした関数でクロスして参照されるローカル変数は特別扱いされねばなりません。関数の実行時に FrameObject.f_localsplus の下に配列として配置されます。そのために func_code.co_cellvars, func_code.co_freevars 変数配列をコンパイル時に定め CodeObject に組み込んでおきます。co_cellvars は外側の関数からみたクロスして参照される変数です。co_freevars は内側の関数から見たクロスして参照される変数です。下のような python code で確認できます。

//@@
def outerF(ag1, ag2):
    at1 = 3
    print ag2
    def innerF():
        inAt = at1+ag1
        print inAt
    print "co_cellvars:", innerF.func_code.co_cellvars
    print "co_freevars:", innerF.func_code.co_freevars

print "co_cellvars:", outerF.func_code.co_cellvars
print "co_freevars:", outerF.func_code.co_freevars
print "----- call outerF() -------"
outerF(1,2)
//@@@
co_cellvars: ('ag1', 'at1')
co_freevars: ()
----- call outerF() -------
2
co_cellvars: ()
co_freevars: ('ag1', 'at1')

このような co_cellvars, co_freevars が必要になるのは、innerF 関数オブジェクトが参照している外側のローカル変数の値を関数オブジェクトの生成時に保存しておかねばならないからです。参照される外側のローカル変数の値を関数オブジェクトに保存しておいて、innerF 関数オブジェクトが実行されるとき、保存しておいた外側のローカル変数の値を使うことで innerF の動作を確実にする必要があるためです。

func_code.co_cellvars, func_code.co_freevars 変数配列は、クロス変数変数を外側の関数からみる/内側の関数からみるかの違いです。コンパイル時に定まっています。関数のネストが二重ネストのときは

outerF.func_code.co_cellvars == innerF.func_code.co_freevars

関数のネストが三重ネスト以上のときは

outerF.func_code.co_cellvars ⊇ innerF.func_code.co_freevars

ClosureObject ≡ FunctionObject + cell object

closure オブジェクトとは ['func_closure'] 要素を持つ FunctionObject:関数オブジェクトとも言えます。

    ClosureObject ≡ FunctionObject + cell object

実際 下のように、クロスして参照されるローカル変数の有無によって FunctionObject.func_closure が None になったり cell になったりします。

# ネストした関数の間でローカル変数のクロスした参照がある
//@@
def outerF():
    inOutAt = 3
    def innerF():
        print inOutAt

    return innerF

print outerF().func_closure
//@@@
(<cell at 0x00923C50: int object at 0x008A56F0>,)

# ネスとした関数の間でローカル変数のクロスした参照がない
//@@
def outerF():
    inOutAt = 3
    def innerF():
        # print inOutAt
        print 5

    return innerF

print outerF().func_closure
//@@@
None

この cell は func_code.co_freevars 配列に対応した値のタプルです。innerF() 関数の宣言時に作られます。デフォルト引数値の配列と同様に FunctionObject が保持します。

MAKE_CLOSURE

FunctionObject.func_closure すなわち cell タプルを作るために、クロージャー関数が宣言されている箇所では MAKE_FUNCTION の代わりに MAKE_CLOSURE を使います。下のような具合です。

//@@
def outerF():
    inOutAt = 3
    def innerF():
        print inOutAt

    return innerF

import dis;dis.dis(outerF)
//@@@
python temp.py
  2           0 LOAD_CONST               1 (3)
              3 STORE_DEREF              0 (inOutAt)

  3           6 LOAD_CLOSURE             0 (inOutAt)
              9 LOAD_CONST               2 (<code object innerF at 0091FC60, file "temp.py", line 3>)
             12 MAKE_CLOSURE             0
             15 STORE_FAST               0 (innerF)

  6          18 LOAD_FAST                0 (innerF)
             21 RETURN_VALUE        

MAKE_CLOSURE の ceval.c ソースは下のようになっています。

    case MAKE_CLOSURE:
    {
            int nfree;
            v = POP(); /* code object */
            x = PyFunction_New(v, f->f_globals);
            #define PyCode_GetNumFree(op) (PyTuple_GET_SIZE((op)->co_freevars))
            nfree = PyCode_GetNumFree((PyCodeObject *)v);
            Py_DECREF(v);
            /* XXX Maybe this should be a separate opcode? */
            if (x != NULL && nfree > 0) {
                    # cell 領域を確保します
                    v = PyTuple_New(nfree);
                    if (v == NULL) {
                            Py_DECREF(x);
                            x = NULL;
                            break;
                    }
                    while (--nfree >= 0) {
                            w = POP();
                            #define PyTuple_SET_ITEM(op, i, v)
                            # (((PyTupleObject *)(op))->ob_item[i] = v)
                            PyTuple_SET_ITEM(v, nfree, w);
                    }
                    # Cell タプルを FunctionObject に設定し ClosureObject にします
                    err = PyFunction_SetClosure(x, v);
                    Py_DECREF(v);
            }
            if (x != NULL && oparg > 0) {
                    # FunctionObject.func_defaults タプルを生成・設定します
                    v = PyTuple_New(oparg);
                    if (v == NULL) {
                            Py_DECREF(x);
                            x = NULL;
                            break;
                    }
                    while (--oparg >= 0) {
                            w = POP();
                            PyTuple_SET_ITEM(v, oparg, w);
                    }
                    err = PyFunction_SetDefaults(x, v);
                    Py_DECREF(v);
            }
            PUSH(x);
            break;
    }
MAKE_FUNCTION と比較してみれば「MAKE_CLOSURE == MAKE_FUNCTION + cell の作成」であることが解ります。ただし残念ですが cell の内容は inspect で見れません。

LOAD_DEREF と co_cellvars[h, co_freevars

ClosureObject は callable object であり、CALL_FUNCIION で処理されます。FrameObject.f_localsplus のローカル変数配列の次に、クロスして参照されている外側のローカル変数配列を設定します。配列位置は co_freevar[] 変数文字列配列と対応します。

f_localsplus┌───────────┐
            │co_varnames[] の順序に│fast_function(.)/do_call(.) は
            │並べられた local 変数 │f_localsplus の生成と設定を行っている
            ├───────────┤
            │co_freevars[] の順序に│
            │並べられた外側の local│
            │変数                  │
            ├───────────┤
            │          ↓          │
            │                      │
            │          ↑          │
            ├───────────┤
            │data stack 領域       │
            └───────────┘

下の python code で関数のネストと co_varnames/co_cellvars/co_freevars の関係が見て取れると思います。

//@@
# 07.07.18 test cell and free
def outerF():
    inOutAt = 3
    def innerF():
        inAt1 = 0
        inAt2 = 0
        innerAt = inOutAt + 3
        inAt3 = 0
        inAt4 = 0
        def inInnerF():
            inInnerAt = innerAt + 5
            print inInnerAt
        return inInnerF
    import dis;dis.dis(innerF)
    return innerF

outerF()
//@@@
python temp.py
  5           0 LOAD_CONST               1 (0)
              3 STORE_FAST               5 (inAt1)

  6           6 LOAD_CONST               1 (0)
              9 STORE_FAST               4 (inAt2)

  7          12 LOAD_DEREF               1 (inOutAt)
             15 LOAD_CONST               2 (3)
             18 BINARY_ADD          
             19 STORE_DEREF              0 (innerAt)

  8          22 LOAD_CONST               1 (0)
             25 STORE_FAST               3 (inAt3)

  9          28 LOAD_CONST               1 (0)
             31 STORE_FAST               2 (inAt4)

 10          34 LOAD_CLOSURE             0 (innerAt)
             37 LOAD_CONST               3 (<code object inInnerF at 0091FCA0, file "temp.py", line 10>)
             40 MAKE_CLOSURE             0
             43 STORE_FAST               1 (inInnerF)

 13          46 LOAD_FAST                1 (inInnerF)
             49 RETURN_VALUE        


//@@
# 07.07.18 test cell and free
def outerF():
    inOutAt = 3
    def innerF():
        inAt1 = 0
        inAt2 = 0
        innerAt = inOutAt + 3
        inAt3 = 0
        inAt4 = 0
        def inInnerF():
            inInnerAt = innerAt + 5
            print inInnerAt
        print "inInnerF.func_code.co_cellvars:",inInnerF.func_code.co_cellvars
        print "inInnerF.func_code.co_freevars:",inInnerF.func_code.co_freevars
        return inInnerF
    #import dis;dis.dis(innerF)
    print "innerF.func_code.co_varnames:",innerF.func_code.co_varnames
    print "innerF.func_code.co_cellvars:",innerF.func_code.co_cellvars
    print "innerF.func_code.co_freevars:",innerF.func_code.co_freevars
    print
    return innerF

print "outerF.func_code.co_varnames:",outerF.func_code.co_varnames
print "outerF.func_code.co_cellvars:",outerF.func_code.co_cellvars
print "outerF.func_code.co_freevars:",outerF.func_code.co_freevars
print
outerF()
//@@@
python temp.py
outerF.func_code.co_varnames: ('innerF', 'inOutAt')
outerF.func_code.co_cellvars: ('inOutAt',)
outerF.func_code.co_freevars: ()

innerF.func_code.co_varnames: ('innerAt', 'inInnerF', 'inAt4', 'inAt3', 'inAt2', 'inAt1')
innerF.func_code.co_cellvars: ('innerAt',)
innerF.func_code.co_freevars: ('inOutAt',)

上のコードでは innerF の co_varname, co_cellvars, co_freevars を主に調べています。innerF() 関数オブジェクトのローカル変数は ('innerAt', 'inInnerF', 'inAt4', 'inAt3', 'inAt2', 'inAt1') です。co_freevars は ('inOutAt',) です。ローカル変数 co_varnames と co_freevars の間には重複があります。Python virtual machine code の op-code を見ても重複を許すインデックス番号が与えられています。FrameObject.f_localsplus 以下のデーター領域は重複分も含めて確保されているようです。

上の python コードの parameter を色々と変えて動かしてみると、python が closure でやっていることが良く見えてくると思います。

クロスした LOCAL 変数の読み書きは下の LOAD_DEREF/STORE_DEREF 命令を使います。この C ソース・コードは、上のメモリ配置を前提にすれば解りやすいでしょう。またLOAD_FAST/STORE_FAST命令と良く似ています。

    case LOAD_DEREF:
            # oparg = NEXTARG(); i.e. 'i' of LOAD_CLOSURE i
            # freevars = f->f_localsplus + f->f_nlocals;
            x = freevars[oparg];
            #define PyCell_GET(op) (((PyCellObject *)(op))->ob_ref)
            w = PyCell_Get(x);
            if (w != NULL) {
                    PUSH(w);
                    continue;
            }
            # ここから下はエラー処理
            err = -1;
            /* Don't stomp existing exception */
            if (PyErr_Occurred())
                    break;
            if (oparg < f->f_ncells) {
                    v = PyTuple_GetItem(co->co_cellvars,
                                           oparg);
                   format_exc_check_arg(
                           PyExc_UnboundLocalError,
                           UNBOUNDLOCAL_ERROR_MSG,
                           v);
            } else {
                   v = PyTuple_GetItem(
                                  co->co_freevars,
                                  oparg - f->f_ncells);
                   format_exc_check_arg(
                           PyExc_NameError,
                           UNBOUNDFREE_ERROR_MSG,
                           v);
            }
            break;
            ・
            ・
    case STORE_DEREF:
            # oparg = NEXTARG(); i.e. 'i' of LOAD_CLOSURE i
            # freevars = f->f_localsplus + f->f_nlocals;
            w = POP();
            x = freevars[oparg];
            #define PyCell_SET(op, v) (((PyCellObject *)(op))->ob_ref = v)
            PyCell_Set(x, w);
            Py_DECREF(w);
            continue;

decorator

今まで説明してきたことを前提に、python のデコレーター構文の disassembler 出力を見れば、それが何をやっているのか理解できるはずです。下の「関数の実行時間を計測するデコレーター構文を使った python プログラム」の動作を python virtual machine レベルで理解できるはずです。

//@@
def dcrtF(fnAg):
    print "Now in dcrtF(.):",fnAg
    inFreeAt = 1

    def innerF(objAg):
        print inFreeAt

        import time
        print "Now innerF(.)", objAg
        dbStartTimeAt = time.clock()
        fnAg(objAg)
        print time.clock() - dbStartTimeAt

    print "innerF.func_code.co_varnames:",innerF.func_code.co_varnames
    print "innerF.func_code.co_cellvars:",innerF.func_code.co_cellvars
    print "innerF.func_code.co_freevars:",innerF.func_code.co_freevars
    print "innerF.func_code.co_stacksize:",innerF.func_code.co_stacksize
    return innerF

@dcrtF
def testF():pass
import dis;dis.dis(dcrtF)
//@@@
Now in dcrtF(.): <function testF at 0x0091F4F0>
innerF.func_code.co_varnames: ('objAg', 'dbStartTimeAt', 'time')
innerF.func_code.co_cellvars: ()
innerF.func_code.co_freevars: ('inFreeAt', 'fnAg')
innerF.func_code.co_stacksize: 2
  2           0 LOAD_CONST               1 ('Now in dcrtF(.):')
              3 PRINT_ITEM          
              4 LOAD_DEREF               0 (fnAg)
              7 PRINT_ITEM          
              8 PRINT_NEWLINE       

  3           9 LOAD_CONST               2 (1)
             12 STORE_DEREF              1 (inFreeAt)

  5          15 LOAD_CLOSURE             1 (inFreeAt)
             18 LOAD_CLOSURE             0 (fnAg)
             21 LOAD_CONST               3 (<code object innerF at 0091FCA0, file "temp.py", line 5>)
             24 MAKE_CLOSURE             0
             27 STORE_FAST               2 (innerF)

 14          30 LOAD_FAST                2 (innerF)
             33 RETURN_VALUE        

5 行目の innerF(.) 関数宣言で MAKE_CLOSURE により ClosureObject を作っています。その ClosureObject.func_closure には (inFreeAt, fnAg) のタプルが設定されています。そのような ClosureObject が data stack のトップに戻り値として積まれて dcrtF(.) の処理を終わっています。

その ClosureObject がデコレーターの syntax sugar の働きにより testF ラベルに設定されます。testF が dcrtF によって decorate された後は、元の testF(.) 関数の実行時間を計測しながら testF(.) を実行します。この ClosureObject で元の testF が実行できるのは ClosureObject.func_closure すなわち cell タプルに testF 関数オブジェクトへの参照値が設定されているからです。Callable Object でもある ClosureObject に対し CALL_FUNCTION を実行するとき、FrameObject.f_localsplus のローカル変数の配列と次に、co_freevar の順序で fnAg すなわち元の testF の参照値が入っているからです。

すなわちデコレート済みの testF を実行しようとしたとき、その FramObject.f_localspls は下のようになっているからです。

FrameObject.f_localsplus┌───────┐
                        │objAg         │co_varnames: ('objAg', 'dbStartTimeAt', 'time')
                        ├───────┤
                        │dbStartTimeAt │
                        ├───────┤
                        │time          │
                        ├───────┤
                        │inFreeAt      │co_freevars:('inFreeAt', 'fnAg')
                        ├───────┤
                        │fnAg==testF   │
                        ├───────┤
         data stack 領域│              │co_stacksize: 2
                        ├───────┤
                        │              │
                        └───────┘

最後に

Python の C ソース・コードを追跡することは意外と簡単です。モジュール、関数、クラスすべてが FrameObject と CodeObject の組み合わせで動いていることを理解しておき、 PyFrameObject 構造体を理解すれば ceval.c を追跡できます。Python virtual machine がどのように動いているのか追跡できます。

そのような追跡が可能になると python がどのように動いているのか python virtual machine code レベルで理解できるようになります。解りにくいデコレーター構文でも、解説を読むより、自分で幾つかの実験コードをテストするほうが手っ取り早く理解できるようになると思います。如何でしょうか。