POSTD PRODUCED BY NIJIBOX

POSTD PRODUCED BY NIJIBOX

ニジボックスが運営する
エンジニアに向けた
キュレーションメディア

Sergei Danielian

本記事は、原著者の許諾のもとに翻訳・掲載しております。

こんにちは、皆さん。

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