Pythonの内部構造::PyObject ― CPythonの実装から内部に迫る

こんにちは、皆さん。

Python言語の実装に深く踏み込む前に、Pythonの主要な概念を知っておく必要があります。それは非常にシンプルで、全てがオブジェクトだということです。このことは、Pythonの内部構造を学習する際の最初のステップであり、この旅の入り口でもあります。

今回の主なテーマは、Pythonのオブジェクトが実装レベルでどのように扱われているかを理解することです。私たちは、Python 2.7.8のCPythonの実装について話をしていきます。

Pythonのソースをダウンロードし、解凍することを想定しているので、ソースコードへの参照は全て、ルートフォルダからの相対的な参照になります。

PyObjectとPyVarObject

Pythonでは全てがオブジェクトです。Pythonで使われている以下のものは文字通り、全てCPyObjectです。

  • 関数
  • スライス
  • ファイル
  • クラス
  • イテレータ
  • 記述子
  • シーケンス
  • 数値型

具体的に、単純なCの構造体を扱うとします。内部的には、Pythonオブジェクトは、任意のPythonオブジェクトを持つ不透明なデータ型PyObjectPyVarObjectによって表現されます。後者は、あらゆる可変長のコンテナオブジェクト(これらは可変です)に対して使われ、前者はそれ以外の全てのオブジェクト(不変)に対して使われます。

全てのビルトイン型およびユーザ定義型は、オブジェクトにラップされている限り、補助情報を自由に追加できます。Pythonも例外ではなく、全てのPythonオブジェクトは、型オブジェクトへのポインタ参照カウンタを持っています。非常に便利ですが、代償を伴います。パフォーマンスが犠牲になるのです。しかし、Pythonがスピードアップを図るために使用する幾つかの技術とアルゴリズム(文字列のインターン、適応数値乗算など)によって、オーバーヘッドを減らすことが可能です。

以下は、Python 2.7.10の公式ドキュメント(訳注:現在のバージョンは2.7.12)からの引用です。

PyObject

全てのオブジェクト型は、この型を拡張したものです。この型には、Pythonがオブジェクトへのポインタをオブジェクトとして扱うために必要な情報が含まれています。通常の”リリース”ビルドでは、オブジェクトの参照カウントおよび対応する型オブジェクトへのポインタだけが含まれます。これは、PyObject_HEADマクロ展開で定義されたフィールドと対応します。

PyVarObject

これはPyObjectを拡張してob_sizeフィールドを追加したものです。長さの概念を持つオブジェクトにのみ使用されます。この型は、Python/C APIにはあまり登場しません。これは、PyObject_VAR_HEADマクロ展開で定義されたフィールドと対応します。

上記のマクロや知らない変数名は気にしないでください。おいおい分かってきますから……。

PyObject構造体とPyVarObject構造体はどのようになっているのでしょうか? 以下は、ソースコードからの抜粋です。

..\include\object.h

...
typedef struct _object {  
    PyObject_HEAD
} PyObject;
...
typedef struct {  
    PyObject_VAR_HEAD
} PyVarObject;
...

これで全部ではありません。さらにトレースしてみましょう(分かりやすくするために、細かいところは若干省略しています)。

..\include\object.h

...
#define _PyObject_HEAD_EXTRA
#define _PyObject_EXTRA_INIT
...
#define PyObject_HEAD                   \
    _PyObject_HEAD_EXTRA                \
    Py_ssize_t ob_refcnt;               \
    struct _typeobject *ob_type;

#define PyObject_HEAD_INIT(type)        \
    _PyObject_EXTRA_INIT                \
    1, type,

#define PyVarObject_HEAD_INIT(type, size)       \
    PyObject_HEAD_INIT(type) size,
...
#define PyObject_VAR_HEAD               \
    PyObject_HEAD                       \
    Py_ssize_t ob_size;
...

マクロの一部はフィールドを定義するためのもので、その他のマクロは初期化のためのものです。

_PyObject_HEAD_EXTRAマクロと_PyObject_EXTRA_INITマクロの定義が空であることに気づきましたか? これは、どのバージョンのPythonでもデフォルトの動作です。唯一空にならないのは、Pythonの”デバッグ”ビルドをコンパイルする時です。しかし、その話は、別の機会に譲るとします。これらのフィールドは常に空で、学習用にこうなっていると思ってください。

全てのマクロが展開されるとPyObjectは以下のようになります。

typedef struct _object {  
    Py_ssize_t ob_refcnt;
    struct _typeobject *ob_type;
} PyObject;

Py_ssize_t型のことは気にせず、ただのintだと思ってください。他のフィールドは見ての通りで、参照カウンタ(ob_refcnt)と、PyTypeObject(ob_type)へのポインタです。この件については、少し後でお話しします。

次は、PyVarObjectを下に示します。

typedef struct {  
    Py_ssize_t ob_refcnt;
    struct _typeobject *ob_type;
    Py_ssize_t ob_size;
} PyVarObject;

PyObjectとほとんど同じように見えますが、フィールドが1つ追加されています。これはob_sizeで、可変長コンテナに含まれるアイテムの数を示します。

CのOOP(オブジェクト指向プログラミング)

では、どうしてPyObjectPyVarObject(そして、後で見る全ての他のPythonオブジェクト)は幾つかの共通の特性、つまりフィールドob_refcntob_typeとを共有するのでしょうか?

共有により、例えば通常の整数や文字列、クラスインスタンスあるいはスライスオブジェクトなど、どんな種類のオブジェクトを処理していたとしても、本質的な型の情報を抽出して、同じやり方でオブジェクトを処理することができます。

Pythonの型の実装(PyIntObjectPyFloatObjectPyDictObject)はそれぞれ、その最初のメンバ(あるいはその最初のメンバの最初のメンバなど)としてPyObject_HEADを持っています。このメンバのサブオブジェクトは、フルオブジェクトと同じアドレスに位置することが保証されます。

PyObject_HEADはそのメンバのサブオブジェクトで参照しますが、一旦ob_typeが完全な型が何であるかという情報を得るために調べられると、完全な型にキャストされるでしょう。このやり方で、C言語にある程度のOOP(特に軽い継承)を導入しています。

PyIntObjectとPyDictObject

実際のオブジェクトPyIntObjectPyDictObjectがPythonでどのように動作するか見てみましょう。

..\Include\intobject.h

...
typedef struct {  
    PyObject_HEAD
    long ob_ival;
} PyIntObject;
... 

..\Include\intobject.h

...
typedef struct {  
    PyObject_HEAD
    long ob_ival;
} PyIntObject;
...

PyObject_HEADがまたありますね。これは「PyIntObjectは、PyObjectに幾らかの付加データ(今回の例ではlong)を付け加えたものとみなすことができる」ということを意味します。PyDictDataオブジェクトを確認してみましょう(このオブジェクトはPythonで辞書{}を表します)。

..\Include\dictobject.h

...
typedef struct _dictobject PyDictObject;  
struct _dictobject {  
    PyObject_HEAD
    Py_ssize_t ma_fill;
    Py_ssize_t ma_used;

    /* ... */
    Py_ssize_t ma_mask;

    /* ... */
    PyDictEntry *ma_table;
    PyDictEntry *(*ma_lookup)(PyDictObject *mp, PyObject *key, long hash);
    PyDictEntry ma_smalltable[PyDict_MINSIZE];
};
...

辞書の表現は少し複難ですが、PyIntObjectを処理するのと同じように、PyObjectとして扱うことは依然として有効なことです。唯一の違いは、PyDictObjectの方が、より多くの追加メンバを持っているという点です。最も重要なのは、これらの全てがPyObject_HEADセクションの後ろに必ず置かれているということです。

PyObject_HEADセクションの中身と、Pythonで特定のPy*Objectオブジェクトの扱い方の情報を全て考慮に入れると、以下のコード断片は自明のものとなるでしょう。それは、Pythonが、現在どの型で動作しているかをどのように決定するのかを示しています。

...
// "op" is of PyObject* type 
if ((op)->ob_type == &PyInt_Type) {  
    // work with numeric type
}
...
if ((op)->ob_type == &PyDict_Type) {  
    // work with dictionary type
}
...

Pythonは、コードの可読性を高めようとして、多くのマクロを定義しています。例えば、明示的にob_typeメンバを使う代わりに、PyInt_CheckあるいはPyInt_CheckExactといったマクロを使うことができます。以下に示す類似したマクロの定義が、Pythonのオブジェクトの実装のCファイルの先頭部分に、見つかるでしょう。

  • 辞書{}オブジェクトのためのPyDict_CheckPyDict_CheckExact
  • 関数オブジェクトのためのPyFunction_Check
  • タプル()オブジェクトのためのPyTuple_CheckPyTuple_CheckExact

こうして、先ほどのコードは、以下のように書き換えられます。

...
if (PyInt_CheckExact(op)) {  
    // work with numeric type
}
...
if (PyDict_CheckExact(op)) {  
    // work with dictionary type
}
...

幾つかのPythonのオブジェクト実装は、共通のものと同じように、オブジェクト自体の型と特定の型をチェックするものを持っています。全ての共通なものは、..\Include\object.hファイルに配置されます。例を以下に示します。

...
#define Py_REFCNT(ob)           (((PyObject*)(ob))->ob_refcnt)
#define Py_TYPE(ob)             (((PyObject*)(ob))->ob_type)
#define Py_SIZE(ob)             (((PyVarObject*)(ob))->ob_size)
...

PyTypeObject

PyObject関連で説明すべき最後のものは、です。Pythonにおける型は、名前(”int”や”tuple”)や適切な格納場所であるだけでなく、リンクによって一般的な特性一式を定義して使用可能にする、多くの関連項目(関数、データメンバ)でもあります。

PyObject_HEADセクションを思い起こしてみましょう。

#define PyObject_HEAD                   \
    ...                                 \
    Py_ssize_t ob_refcnt;               \
    struct _typeobject *ob_type;

PyObject_HEADセクションにあるob_typeポインタは、オブジェクトの型のインスタンスを厳密に参照します。これを詳しく見てみましょう(最も興味深いセクションを選びました)。

..\include\dictobject.h

typedef struct _typeobject {  
    PyObject_VAR_HEAD
    const char *tp_name;
    ....

    /* Methods to implement standard operations */
    destructor tp_dealloc;
    printfunc tp_print;
    getattrfunc tp_getattr;
    setattrfunc tp_setattr;
    cmpfunc tp_compare;
    reprfunc tp_repr;

    /* Method suites for standard classes */
    PyNumberMethods *tp_as_number;
    PySequenceMethods *tp_as_sequence;
    PyMappingMethods *tp_as_mapping;

    /* More standard operations (here for binary compatibility) */
    hashfunc tp_hash;
    ternaryfunc tp_call;
    reprfunc tp_str;
    getattrofunc tp_getattro;
    setattrofunc tp_setattro;
    ...
} PyTypeObject;

これは、構造体全体ではありませんが、最も興味深い部分です。ご想像の通り、funcの付いた変わった修飾子は、単なるコールバックです。そして、各Pythonオブジェクトは、これをそれぞれの方法で初期化します。

例えば、cmpfunc tp_compare;の行は、明らかに、オブジェクトの比較に何らかの形で関係します。そして、PyIntObjectの比較関数の実装は、PyTupleObjectの場合とは異なります。

別の行のhashfunc tp_hash;は、型のハッシュ関数を定義します。例えば、stringは、ハッシュ関数を持っていますが、dictionaryは持っていません。なぜでしょうか?

もし、これらについての詳細が知りたければ、Python/C API Reference Manual“Object Implementation Support”“Type Objects”セクションを参照してください。

以下の関数が、PyInt_Typeオブジェクト、PyDict_Typeオブジェクト、PyTuple_Typeオブジェクトでどのように実装されているかを比較します。

/* Method suites for standard classes */
PyNumberMethods *tp_as_number;  
PySequenceMethods *tp_as_sequence;  
PyMappingMethods *tp_as_mapping;  
hashfunc tp_hash;  

最初の3つの関数と戻り値を見ると、Pythonの抽象オブジェクトレイヤを思い出すことでしょう。

抽象オブジェクトレイヤは、各Pythonオブジェクトが実装し、後で実装したプロトコルによって分類されるような、多くのプロトコルを定義します。プロトコルは、明確に定義された振る舞いをさせるためにどの関数が実装の型を決めるかという、ある種の規則です。例えば、特定の関数一式(長さ、サイズ、連結など)を実装している場合、型はシーケンスベースに分類されます。

幾つかのプロトコルがありますが、主に興味を引かれるものは、下記の通りです。

  • 数値型プロトコル - 全ての数値型(int、float、complexなど)
  • シーケンス型プロトコル - シーケンス型(str、list、tupleなど)
  • マップ型プロトコル - マップ型(dict

元の例に戻りましょう。

PyInt_Type

PyTypeObject PyInt_Type = {  
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "int",
    ...
    &int_as_number,                             /* tp_as_number */
    0,                                          /* tp_as_sequence */
    0,                                          /* tp_as_mapping */
    (hashfunc)int_hash,                         /* tp_hash */
    ...
};
  • PyInt_Typeは、数値型プロトコルを実装します。そのため、tp_as_sequence関数やtp_as_mapping関数は、nullです。
  • 全ての不変型は、独自のハッシュ関数を持っているので、PyInt_Type (int_hash)も同じくハッシュ関数を持っています。

PyDict_Type

PyTypeObject PyDict_Type = {  
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "dict",
    ...
    0,                                          /* tp_as_number */
    &dict_as_sequence,                          /* tp_as_sequence */
    &dict_as_mapping,                           /* tp_as_mapping */
    (hashfunc)PyObject_HashNotImplemented,      /* tp_hash */
    ...
};
  • 辞書型は、Pythonでは厄介なものです。これはマップ型プロトコルの唯一の完全な代表例ですが、シーケンス型プロトコル(実際には__contains__という関数一つで、hack to implement "key in dict"の一種です)の一部を実装します。
  • 辞書が可変型であれば、ハッシュ関数はありません(PyObject_HashNotImplementedという例外があるのみです)。

PyTuple_Type

PyTypeObject PyTuple_Type = {  
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "tuple",
    ...
    0,                                          /* tp_as_number */
    &tuple_as_sequence,                         /* tp_as_sequence */
    &tuple_as_mapping,                          /* tp_as_mapping */
    (hashfunc)tuplehash,                        /* tp_hash */
    ...
};
  • タプル型も厄介です。タプル型は、シーケンスベースの型ですが、シーケンス型プロトコルマップ型プロトコルの両方をほぼ完全に実装します。そのため、両方の関数(tp_as_sequencetp_as_mapping)が、空ではありません。
  • タプル型は、不変オブジェクトなので、ハッシュ関数を持っています(tuplehash)。

ここまでで、CPythonの値と山の旅を楽しんでいただけましたでしょうか。ざっと見てきただけで深くは踏み込んでいませんが、この記事の情報が、今後Pythonについてより深く学ぶ助けとなればと思います。

参考資料

  1. Python Standard Library
  2. Python/C API Reference Manual