2016年8月30日
Pythonの内部構造::PyObject ― CPythonの実装から内部に迫る
(2015-07-07)by Sergei Danielian
本記事は、原著者の許諾のもとに翻訳・掲載しております。
こんにちは、皆さん。
Python言語の実装に深く踏み込む前に、Pythonの主要な概念を知っておく必要があります。それは非常にシンプルで、 全てがオブジェクトだ ということです。このことは、Pythonの内部構造を学習する際の最初のステップであり、この旅の入り口でもあります。
今回の主なテーマは、Pythonのオブジェクトが実装レベルでどのように扱われているかを理解することです。私たちは、 Python 2.7.8 のCPythonの実装について話をしていきます。
Pythonのソースをダウンロードし、解凍することを想定しているので、ソースコードへの参照は全て、ルートフォルダからの相対的な参照になります。
PyObjectとPyVarObject
Pythonでは全てがオブジェクトです。Pythonで使われている以下のものは文字通り、全て C
の PyObject
です。
- 関数
- スライス
- ファイル
- クラス
- イテレータ
- 記述子
- シーケンス
- 数値型
具体的に、単純なCの構造体を扱うとします。内部的には、Pythonオブジェクトは、任意のPythonオブジェクトを持つ不透明なデータ型 PyObject
と PyVarObject
によって表現されます。後者は、あらゆる可変長のコンテナオブジェクト(これらは 可変 です)に対して使われ、前者はそれ以外の全てのオブジェクト( 不変 )に対して使われます。
全てのビルトイン型およびユーザ定義型は、オブジェクトにラップされている限り、補助情報を自由に追加できます。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(オブジェクト指向プログラミング)
では、どうして PyObject
と PyVarObject
(そして、後で見る全ての他のPythonオブジェクト)は幾つかの共通の特性、つまりフィールド ob_refcnt
と ob_type
とを共有するのでしょうか?
共有により、例えば通常の整数や文字列、クラスインスタンスあるいはスライスオブジェクトなど、どんな種類のオブジェクトを処理していたとしても、本質的な型の情報を抽出して、同じやり方でオブジェクトを処理することができます。
Pythonの型の実装( PyIntObject
や PyFloatObject
、 PyDictObject
)はそれぞれ、その最初のメンバ(あるいはその最初のメンバの最初のメンバなど)として PyObject_HEAD
を持っています。このメンバのサブオブジェクトは、フルオブジェクトと同じアドレスに位置することが保証されます。
PyObject_HEAD
はそのメンバのサブオブジェクトで参照しますが、一旦 ob_type
が完全な型が何であるかという情報を得るために調べられると、完全な型にキャストされるでしょう。このやり方で、 C
言語にある程度のOOP(特に軽い継承)を導入しています。
PyIntObjectとPyDictObject
実際のオブジェクト PyIntObject
と PyDictObject
が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_Check
とPyDict_CheckExact
- 関数オブジェクトのための
PyFunction_Check
- タプル
()
オブジェクトのためのPyTuple_Check
とPyTuple_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_sequence
とtp_as_mapping
)が、空ではありません。 - タプル型は、不変オブジェクトなので、ハッシュ関数を持っています(
tuplehash
)。
ここまでで、CPythonの値と山の旅を楽しんでいただけましたでしょうか。ざっと見てきただけで深くは踏み込んでいませんが、この記事の情報が、今後Pythonについてより深く学ぶ助けとなればと思います。
参考資料
株式会社リクルート プロダクト統括本部 プロダクト開発統括室 グループマネジャー 株式会社ニジボックス デベロップメント室 室長 Node.js 日本ユーザーグループ代表
- Twitter: @yosuke_furukawa
- Github: yosuke-furukawa