3. 拡張タイプの定義:さまざまなトピック—Pythonドキュメント

提供:Dev Guides
< PythonPython/docs/3.9/extending/newtypes
移動先:案内検索

3.3。 拡張タイプの定義:さまざまなトピック

このセクションの目的は、実装できるさまざまなタイプのメソッドとその機能について簡単に説明することです。

PyTypeObject の定義は次のとおりです。デバッグビルドでのみ使用される一部のフィールドは省略されています:

typedef struct _typeobject {
    PyObject_VAR_HEAD
    const char *tp_name; /* For printing, in format "<module>.<name>" */
    Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */

    /* Methods to implement standard operations */

    destructor tp_dealloc;
    Py_ssize_t tp_vectorcall_offset;
    getattrfunc tp_getattr;
    setattrfunc tp_setattr;
    PyAsyncMethods *tp_as_async; /* formerly known as tp_compare (Python 2)
                                    or tp_reserved (Python 3) */
    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;

    /* Functions to access object as input/output buffer */
    PyBufferProcs *tp_as_buffer;

    /* Flags to define presence of optional/expanded features */
    unsigned long tp_flags;

    const char *tp_doc; /* Documentation string */

    /* call function for all accessible objects */
    traverseproc tp_traverse;

    /* delete references to contained objects */
    inquiry tp_clear;

    /* rich comparisons */
    richcmpfunc tp_richcompare;

    /* weak reference enabler */
    Py_ssize_t tp_weaklistoffset;

    /* Iterators */
    getiterfunc tp_iter;
    iternextfunc tp_iternext;

    /* Attribute descriptor and subclassing stuff */
    struct PyMethodDef *tp_methods;
    struct PyMemberDef *tp_members;
    struct PyGetSetDef *tp_getset;
    struct _typeobject *tp_base;
    PyObject *tp_dict;
    descrgetfunc tp_descr_get;
    descrsetfunc tp_descr_set;
    Py_ssize_t tp_dictoffset;
    initproc tp_init;
    allocfunc tp_alloc;
    newfunc tp_new;
    freefunc tp_free; /* Low-level free-memory routine */
    inquiry tp_is_gc; /* For PyObject_IS_GC */
    PyObject *tp_bases;
    PyObject *tp_mro; /* method resolution order */
    PyObject *tp_cache;
    PyObject *tp_subclasses;
    PyObject *tp_weaklist;
    destructor tp_del;

    /* Type attribute cache version tag. Added in version 2.6 */
    unsigned int tp_version_tag;

    destructor tp_finalize;

} PyTypeObject;

これが lot のメソッドです。 ただし、あまり心配する必要はありません。定義したいタイプがある場合は、これらのほんの一握りしか実装しない可能性が非常に高くなります。

ご想像のとおり、これについては、さまざまなハンドラーについて詳しく説明します。 フィールドの順序に影響を与える歴史的な手荷物がたくさんあるため、構造で定義されている順序では進みません。 多くの場合、必要なフィールドを含む例を見つけて、新しいタイプに合わせて値を変更するのが最も簡単です。

const char *tp_name; /* For printing */

タイプの名前–前の章で述べたように、これはさまざまな場所に表示され、ほぼ完全に診断目的で表示されます。 そのような状況で役立つものを選択してみてください!

Py_ssize_t tp_basicsize, tp_itemsize; /* For allocation */

これらのフィールドは、このタイプの新しいオブジェクトが作成されたときに割り当てるメモリの量をランタイムに通知します。 Pythonには、 tp_itemsize フィールドの出番である可変長構造(文字列、タプルなど)のサポートが組み込まれています。 これは後で処理されます。

const char *tp_doc;

ここに、Pythonスクリプトがobj.__doc__を参照してドキュメント文字列を取得するときに返される文字列(またはそのアドレス)を配置できます。

ここで、基本的な型メソッドについて説明します。ほとんどの拡張型が実装する方法です。

3.1。 ファイナライズと割り当て解除

destructor tp_dealloc;

この関数は、型のインスタンスの参照カウントがゼロに減少し、Pythonインタープリターがそれを再利用したいときに呼び出されます。 タイプに解放するメモリまたは実行するその他のクリーンアップがある場合は、ここに配置できます。 オブジェクト自体もここで解放する必要があります。 この関数の例を次に示します。

static void
newdatatype_dealloc(newdatatypeobject *obj)
{
    free(obj->obj_UnderlyingDatatypePtr);
    Py_TYPE(obj)->tp_free(obj);
}

デアロケーター関数の重要な要件の1つは、保留中の例外をそのままにしておくことです。 インタプリタがPythonスタックを巻き戻すときに、デアロケータが頻繁に呼び出されるため、これは重要です。 (通常のリターンではなく)例外が原因でスタックが巻き戻された場合、例外がすでに設定されていることをデロケーターが認識しないように保護するために何も行われません。 追加のPythonコードが実行される可能性のある、デアロケーターが実行するアクションは、例外が設定されたことを検出する場合があります。 これは、インタプリタからの誤解を招くエラーにつながる可能性があります。 これを防ぐ適切な方法は、安全でないアクションを実行する前に保留中の例外を保存し、完了したらそれを復元することです。 これは、 PyErr_Fetch()および PyErr_Restore()関数を使用して実行できます。

static void
my_dealloc(PyObject *obj)
{
    MyObject *self = (MyObject *) obj;
    PyObject *cbresult;

    if (self->my_callback != NULL) {
        PyObject *err_type, *err_value, *err_traceback;

        /* This saves the current exception state */
        PyErr_Fetch(&err_type, &err_value, &err_traceback);

        cbresult = PyObject_CallNoArgs(self->my_callback);
        if (cbresult == NULL)
            PyErr_WriteUnraisable(self->my_callback);
        else
            Py_DECREF(cbresult);

        /* This restores the saved exception state */
        PyErr_Restore(err_type, err_value, err_traceback);

        Py_DECREF(self->my_callback);
    }
    Py_TYPE(obj)->tp_free((PyObject*)self);
}

ノート

デロケーター関数で安全に実行できることには制限があります。 まず、タイプがガベージコレクションをサポートしている場合( tp_traverse および/または tp_clear を使用)、オブジェクトのメンバーの一部は、 tp_dealloc までにクリアまたはファイナライズされている可能性があります。と呼ばれます。 次に、 tp_dealloc では、オブジェクトが不安定な状態にあります。その参照カウントはゼロです。 重要なオブジェクトまたはAPI(上記の例のように)を呼び出すと、 tp_dealloc が再度呼び出され、ダブルフリーとクラッシュが発生する可能性があります。

Python 3.4以降では、 tp_dealloc に複雑なファイナライズコードを配置せず、代わりに新しい tp_finalize タイプのメソッドを使用することをお勧めします。

も参照してください

PEP 442 は、新しいファイナライズスキームについて説明しています。


3.2。 オブジェクトのプレゼンテーション

Pythonでは、オブジェクトのテキスト表現を生成する方法が2つあります。 repr()関数と str()関数です。 ( print()関数は str()を呼び出すだけです。)これらのハンドラーはどちらもオプションです。

reprfunc tp_repr;
reprfunc tp_str;

tp_repr ハンドラーは、呼び出されたインスタンスの表現を含む文字列オブジェクトを返す必要があります。 簡単な例を次に示します。

static PyObject *
newdatatype_repr(newdatatypeobject * obj)
{
    return PyUnicode_FromFormat("Repr-ified_newdatatype{{size:%d}}",
                                obj->obj_UnderlyingDatatypePtr->size);
}

tp_repr ハンドラーが指定されていない場合、インタープリターは、型の tp_name とオブジェクトの一意の識別値を使用する表現を提供します。

tp_str ハンドラーは str()に対して、上記の tp_repr ハンドラーは repr()に対してです。 つまり、Pythonコードがオブジェクトのインスタンスで str()を呼び出すときに呼び出されます。 その実装は tp_repr 関数と非常に似ていますが、結果の文字列は人間が消費することを目的としています。 tp_str が指定されていない場合は、代わりに tp_repr ハンドラーが使用されます。

簡単な例を次に示します。

static PyObject *
newdatatype_str(newdatatypeobject * obj)
{
    return PyUnicode_FromFormat("Stringified_newdatatype{{size:%d}}",
                                obj->obj_UnderlyingDatatypePtr->size);
}

3.3。 属性管理

属性をサポートできるすべてのオブジェクトについて、対応するタイプは、属性の解決方法を制御する関数を提供する必要があります。 属性を取得できる関数(定義されている場合)と、属性を設定する関数(属性の設定が許可されている場合)が必要です。 属性の削除は特殊なケースであり、ハンドラーに渡される新しい値はNULLです。

Pythonは、2組の属性ハンドラーをサポートしています。 属性をサポートするタイプは、1つのペアの関数を実装するだけで済みます。 違いは、一方のペアが属性の名前を char * として受け取り、もう一方のペアが PyObject * を受け入れることです。 各タイプは、実装の便宜のためにより意味のあるペアを使用できます。

getattrfunc  tp_getattr;        /* char * version */
setattrfunc  tp_setattr;
/* ... */
getattrofunc tp_getattro;       /* PyObject * version */
setattrofunc tp_setattro;

オブジェクトの属性へのアクセスが常に単純な操作である場合(これについては後で説明します)、属性管理関数の PyObject * バージョンを提供するために使用できる一般的な実装があります。 Python 2.2以降、タイプ固有の属性ハンドラーの実際の必要性はほぼ完全になくなりましたが、利用可能な新しい汎用メカニズムの一部を使用するように更新されていない例が多数あります。

3.3.1。 ジェネリック属性管理

ほとんどの拡張タイプは、 simple 属性のみを使用します。 では、何が属性を単純にするのでしょうか? 満たす必要のある条件は2つだけです。

  1. PyType_Ready()を呼び出すときは、属性の名前を知っている必要があります。
  2. 属性が検索または設定されたことを記録するために特別な処理は必要ありません。また、値に基づいてアクションを実行する必要もありません。

このリストでは、属性の値、値の計算時期、または関連データの保存方法に制限がないことに注意してください。

PyType_Ready()が呼び出されると、型オブジェクトによって参照される3つのテーブルを使用して、型オブジェクトのディクショナリに配置される記述子が作成されます。 各記述子は、インスタンスオブジェクトの1つの属性へのアクセスを制御します。 各テーブルはオプションです。 3つすべてがNULLの場合、タイプのインスタンスは基本タイプから継承された属性のみを持ち、 tp_getattro および tp_setattro フィールドNULLも同様で、基本型が属性を処理できるようにします。

テーブルは、タイプオブジェクトの3つのフィールドとして宣言されています。

struct PyMethodDef *tp_methods;
struct PyMemberDef *tp_members;
struct PyGetSetDef *tp_getset;

tp_methodsNULLでない場合は、 PyMethodDef 構造体の配列を参照する必要があります。 テーブルの各エントリは、この構造のインスタンスです。

typedef struct PyMethodDef {
    const char  *ml_name;       /* method name */
    PyCFunction  ml_meth;       /* implementation function */
    int          ml_flags;      /* flags */
    const char  *ml_doc;        /* docstring */
} PyMethodDef;

タイプによって提供されるメソッドごとに1つのエントリを定義する必要があります。 基本型から継承されたメソッドにはエントリは必要ありません。 最後に1つの追加エントリが必要です。 これは、配列の終わりを示す番兵です。 番兵のml_nameフィールドはNULLである必要があります。

2番目のテーブルは、インスタンスに格納されているデータに直接マップする属性を定義するために使用されます。 さまざまなプリミティブCタイプがサポートされており、アクセスは読み取り専用または読み取り/書き込みの場合があります。 表の構造は次のように定義されています。

typedef struct PyMemberDef {
    const char *name;
    int         type;
    int         offset;
    int         flags;
    const char *doc;
} PyMemberDef;

テーブルのエントリごとに、記述子が作成され、インスタンス構造から値を抽出できるタイプに追加されます。 type フィールドには、structmember.hヘッダーで定義されているタイプコードの1つが含まれている必要があります。 この値は、Python値をC値に変換する方法とC値から変換する方法を決定するために使用されます。 flagsフィールドは、属性へのアクセス方法を制御するフラグを格納するために使用されます。

structmember.hでは次のフラグ定数が定義されています。 それらはビットごとのORを使用して組み合わせることができます。

絶え間ない 意味
READONLY 決して書き込み可能ではありません。
READ_RESTRICTED 制限付きモードでは読み取りできません。
WRITE_RESTRICTED 制限付きモードでは書き込みできません。
RESTRICTED 制限付きモードでは読み取りまたは書き込みができません。

tp_members テーブルを使用して実行時に使用される記述子を作成することの興味深い利点は、この方法で定義された属性は、テーブルにテキストを提供するだけで、関連付けられたドキュメント文字列を持つことができることです。 アプリケーションは、イントロスペクションAPIを使用してクラスオブジェクトから記述子を取得し、__doc__属性を使用してドキュメント文字列を取得できます。

tp_methods テーブルと同様に、nameの値がNULLのセンチネルエントリが必要です。


3.3.2。 タイプ固有の属性管理

簡単にするために、ここでは char * バージョンのみを示します。 nameパラメーターのタイプは、インターフェースの char * フレーバーと PyObject * フレーバーの唯一の違いです。 この例は、上記の一般的な例と効果的に同じことを行いますが、Python2.2で追加された一般的なサポートを使用していません。 ハンドラー関数がどのように呼び出されるかを説明しているので、それらの機能を拡張する必要がある場合は、何をする必要があるかを理解できます。

tp_getattr ハンドラーは、オブジェクトが属性ルックアップを必要とするときに呼び出されます。 これは、クラスの__getattr__()メソッドが呼び出されるのと同じ状況で呼び出されます。

次に例を示します。

static PyObject *
newdatatype_getattr(newdatatypeobject *obj, char *name)
{
    if (strcmp(name, "data") == 0)
    {
        return PyLong_FromLong(obj->data);
    }

    PyErr_Format(PyExc_AttributeError,
                 "'%.50s' object has no attribute '%.400s'",
                 tp->tp_name, name);
    return NULL;
}

tp_setattr ハンドラーは、クラスインスタンスの__setattr__()または__delattr__()メソッドが呼び出されるときに呼び出されます。 属性を削除する必要がある場合、3番目のパラメーターはNULLになります。 これは単に例外を発生させる例です。 これが本当に必要なすべてである場合は、 tp_setattr ハンドラーをNULLに設定する必要があります。

static int
newdatatype_setattr(newdatatypeobject *obj, char *name, PyObject *v)
{
    PyErr_Format(PyExc_RuntimeError, "Read-only attribute: %s", name);
    return -1;
}

3.4。 オブジェクトの比較

richcmpfunc tp_richcompare;

tp_richcompare ハンドラーは、比較が必要なときに呼び出されます。 これは、__lt__()のようなリッチ比較メソッドに類似しており、 PyObject_RichCompare()および PyObject_RichCompareBool()によっても呼び出されます。

この関数は、2つのPythonオブジェクトと演算子を引数として呼び出されます。演算子は、Py_EQPy_NEPy_LEPy_GTPy_LTまたはPy_GT。 指定された演算子に関して2つのオブジェクトを比較し、比較が成功した場合はPy_TrueまたはPy_Falseを返し、比較が実装されていないことを示すためにPy_NotImplementedを返し、他のオブジェクトは比較方法を試すか、例外が設定されている場合はNULLを試してください。

内部ポインタのサイズが等しい場合に等しいと見なされるデータ型のサンプル実装を次に示します。

static PyObject *
newdatatype_richcmp(PyObject *obj1, PyObject *obj2, int op)
{
    PyObject *result;
    int c, size1, size2;

    /* code to make sure that both arguments are of type
       newdatatype omitted */

    size1 = obj1->obj_UnderlyingDatatypePtr->size;
    size2 = obj2->obj_UnderlyingDatatypePtr->size;

    switch (op) {
    case Py_LT: c = size1 <  size2; break;
    case Py_LE: c = size1 <= size2; break;
    case Py_EQ: c = size1 == size2; break;
    case Py_NE: c = size1 != size2; break;
    case Py_GT: c = size1 >  size2; break;
    case Py_GE: c = size1 >= size2; break;
    }
    result = c ? Py_True : Py_False;
    Py_INCREF(result);
    return result;
 }

3.5。 抽象プロトコルのサポート

Pythonは、さまざまな abstract 'プロトコル;'をサポートしています。 これらのインターフェースを使用するために提供される特定のインターフェースは、 Abstract Objects Layer に記載されています。

これらの抽象的なインターフェースの多くは、Python実装の開発の初期に定義されました。 特に、数、マッピング、およびシーケンスプロトコルは、当初からPythonの一部でした。 他のプロトコルは時間の経過とともに追加されてきました。 型の実装からのいくつかのハンドラールーチンに依存するプロトコルの場合、古いプロトコルは、型オブジェクトによって参照されるハンドラーのオプションのブロックとして定義されています。 新しいプロトコルの場合、メインタイプオブジェクトに追加のスロットがあり、スロットが存在することを示すフラグビットが設定されており、インタプリタがチェックする必要があります。 (フラグビットは、スロット値がNULL以外であることを示していません。 フラグはスロットの存在を示すために設定できますが、スロットはまだ埋められていない可能性があります。)

PyNumberMethods   *tp_as_number;
PySequenceMethods *tp_as_sequence;
PyMappingMethods  *tp_as_mapping;

オブジェクトを数値、シーケンス、またはマッピングオブジェクトのように機能させたい場合は、Cタイプ PyNumberMethodsPySequenceMethods を実装する構造体のアドレスを配置します。 、または PyMappingMethods 。 この構造に適切な値を入力するのはあなた次第です。 これらのそれぞれの使用例は、PythonソースディストリビューションのObjectsディレクトリにあります。

hashfunc tp_hash;

この関数を提供することを選択した場合、データ型のインスタンスのハッシュ番号を返す必要があります。 簡単な例を次に示します。

static Py_hash_t
newdatatype_hash(newdatatypeobject *obj)
{
    Py_hash_t result;
    result = obj->some_size + 32767 * obj->some_number;
    if (result == -1)
       result = -2;
    return result;
}

Py_hash_tは、プラットフォームが変化する幅の符号付き整数型です。 tp_hash から-1を返すとエラーが発生するため、上記のように、ハッシュ計算が成功したときに返さないように注意する必要があります。

ternaryfunc tp_call;

この関数は、データ型のインスタンスが「呼び出された」ときに呼び出されます。たとえば、obj1がデータ型のインスタンスであり、Pythonスクリプトにobj1('hello')が含まれている場合、 tp_call ハンドラーが呼び出されます。

この関数は3つの引数を取ります。

  1. self は、呼び出しの対象となるデータ型のインスタンスです。 呼び出しがobj1('hello')の場合、 selfobj1です。
  2. args は、呼び出しへの引数を含むタプルです。 PyArg_ParseTuple()を使用して引数を抽出できます。
  3. kwds は、渡されたキーワード引数の辞書です。 これがNULL以外で、キーワード引数をサポートしている場合は、 PyArg_ParseTupleAndKeywords()を使用して引数を抽出します。 キーワード引数をサポートしたくなく、これがNULL以外の場合は、 TypeError を発生させて、キーワード引数がサポートされていないことを示すメッセージを表示します。

おもちゃのtp_callの実装は次のとおりです。

static PyObject *
newdatatype_call(newdatatypeobject *self, PyObject *args, PyObject *kwds)
{
    PyObject *result;
    const char *arg1;
    const char *arg2;
    const char *arg3;

    if (!PyArg_ParseTuple(args, "sss:call", &arg1, &arg2, &arg3)) {
        return NULL;
    }
    result = PyUnicode_FromFormat(
        "Returning -- value: [%d] arg1: [%s] arg2: [%s] arg3: [%s]\n",
        obj->obj_UnderlyingDatatypePtr->size,
        arg1, arg2, arg3);
    return result;
}
/* Iterators */
getiterfunc tp_iter;
iternextfunc tp_iternext;

これらの関数は、イテレータプロトコルのサポートを提供します。 両方のハンドラーは、呼び出されているインスタンスという1つのパラメーターを受け取り、新しい参照を返します。 エラーが発生した場合は、例外を設定してNULLを返す必要があります。 tp_iter はPython __iter__()メソッドに対応し、 tp_iternext はPython __ next __()メソッドに対応します。

iterable オブジェクトは、 tp_iter ハンドラーを実装する必要があります。このハンドラーは、 iterator オブジェクトを返す必要があります。 ここでは、Pythonクラスの場合と同じガイドラインが適用されます。

  • 複数の独立したイテレータをサポートできるコレクション(リストやタプルなど)の場合、 tp_iter を呼び出すたびに、新しいイテレータを作成して返す必要があります。
  • 1回しか反復できないオブジェクト(通常、ファイルオブジェクトなど、反復の副作用のため)は、自身への新しい参照を返すことで tp_iter を実装できます。したがって、 tp_iternext [も実装する必要があります。 X241X]ハンドラー。

iterator オブジェクトは、 tp_itertp_iternext の両方を実装する必要があります。 イテレータの tp_iter ハンドラは、イテレータへの新しい参照を返す必要があります。 その tp_iternext ハンドラーは、反復内の次のオブジェクト(存在する場合)への新しい参照を返す必要があります。 反復が終了した場合、 tp_iternext は例外を設定せずにNULLを返すか、 StopIteration に加えてをを返すように設定します。 X181X] ; 例外を回避すると、パフォーマンスがわずかに向上する可能性があります。 実際のエラーが発生した場合、 tp_iternext は常に例外を設定し、NULLを返す必要があります。


3.6。 弱参照サポート

Pythonの弱参照実装の目標の1つは、パフォーマンスが重要なオブジェクト(数値など)にオーバーヘッドを発生させることなく、任意の型が弱参照メカニズムに参加できるようにすることです。

も参照してください

weakref モジュールのドキュメント。


オブジェクトが弱く参照可能であるためには、拡張タイプは2つのことをしなければなりません:

  1. 弱参照メカニズム専用のCオブジェクト構造に PyObject * フィールドを含めます。 オブジェクトのコンストラクターは、オブジェクトをNULLのままにする必要があります(これは、デフォルトの tp_alloc を使用する場合は自動です)。
  2. tp_weaklistoffset タイプのメンバーをCオブジェクト構造の前述のフィールドのオフセットに設定して、インタープリターがそのフィールドにアクセスして変更する方法を認識できるようにします。

具体的には、簡単なオブジェクト構造が必須フィールドでどのように拡張されるかを次に示します。

typedef struct {
    PyObject_HEAD
    PyObject *weakreflist;  /* List of weak references */
} TrivialObject;

そして、静的に宣言された型オブジェクトの対応するメンバー:

static PyTypeObject TrivialType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    /* ... other members omitted for brevity ... */
    .tp_weaklistoffset = offsetof(TrivialObject, weakreflist),
};

さらに唯一の追加は、フィールドがNULL以外の場合、tp_deallocは(PyObject_ClearWeakRefs()を呼び出すことによって)弱い参照をクリアする必要があることです。

static void
Trivial_dealloc(TrivialObject *self)
{
    /* Clear weakrefs first before calling any destructors */
    if (self->weakreflist != NULL)
        PyObject_ClearWeakRefs((PyObject *) self);
    /* ... remainder of destruction code omitted for brevity ... */
    Py_TYPE(self)->tp_free((PyObject *) self);
}

3.7。 その他の提案

新しいデータ型に特定のメソッドを実装する方法を学ぶには、 CPython ソースコードを入手してください。 Objectsディレクトリに移動し、Cソースファイルでtp_と必要な機能(tp_richcompareなど)を検索します。 実装したい関数の例があります。

オブジェクトが実装しているタイプの具象インスタンスであることを確認する必要がある場合は、 PyObject_TypeCheck()関数を使用してください。 その使用例は次のようになります。

if (!PyObject_TypeCheck(some_object, &MyType)) {
    PyErr_SetString(PyExc_TypeError, "arg #1 not a mything");
    return NULL;
}

も参照してください

CPythonソースリリースをダウンロードします。
https://www.python.org/downloads/source/
CPythonソースコードが開発されているGitHubのCPythonプロジェクト。
https://github.com/python/cpython