2.2。 新しいタイプの定義
前の章で述べたように、Pythonを使用すると、拡張モジュールの作成者は、コアPythonの文字列やリストのように、Pythonコードから操作できる新しい型を定義できます。
これは難しいことではありません。 すべての拡張タイプのコードはパターンに従いますが、開始する前に理解する必要のある詳細がいくつかあります。
ノート
Python 2.2では、新しい型の定義方法が劇的に(そしてより良く)変更されました。 このドキュメントでは、Python2.2以降の新しい型を定義する方法について説明します。 古いバージョンのPythonをサポートする必要がある場合は、このドキュメントの古いバージョンを参照する必要があります。
2.1。 基礎
Pythonランタイムは、すべてのPythonオブジェクトをPyObject*
型の変数として認識します。 PyObject は、それほど壮大なオブジェクトではありません。refcountと、オブジェクトの「タイプオブジェクト」へのポインタが含まれているだけです。 これがアクションの場所です。 タイプオブジェクトは、たとえば、属性がオブジェクトで検索されたとき、または別のオブジェクトで乗算されたときに呼び出される(C)関数を決定します。 これらのC関数は「タイプメソッド」と呼ばれます。
したがって、新しいオブジェクトタイプを定義する場合は、新しいタイプのオブジェクトを作成する必要があります。
この種のことは例によってのみ説明できるので、ここに新しいタイプを定義する最小限の、しかし完全なモジュールがあります:
さて、それは一度に理解するのにかなりの量ですが、うまくいけば、ビットは前の章からおなじみのように見えるでしょう。
新しい最初のビットは次のとおりです。
これは、Noddyオブジェクトに含まれるものです。この場合、すべてのPythonオブジェクトに含まれるのは、refcountと型オブジェクトへのポインターだけです。 これらは、PyObject_HEAD
マクロが取り込むフィールドです。 マクロの理由は、レイアウトを標準化し、デバッグビルドで特別なデバッグフィールドを有効にするためです。 PyObject_HEAD
マクロの後にセミコロンがないことに注意してください。 1つはマクロ定義に含まれています。 誤って追加することには注意してください。 習慣から行うのは簡単で、コンパイラは文句を言わないかもしれませんが、他の誰かがおそらく文句を言うでしょう! (Windowsでは、MSVCはこれをエラーと呼び、コードのコンパイルを拒否することが知られています。)
対照的に、標準のPython整数の対応する定義を見てみましょう。
次に進むと、型オブジェクトという問題が発生します。
ここで、object.h
の PyTypeObject の定義を調べると、上記の定義よりも多くのフィールドがあることがわかります。 残りのフィールドはCコンパイラによってゼロで埋められます。必要がない限り、明示的に指定しないのが一般的な方法です。
これは非常に重要なので、その上部をさらに分解します。
この行は少し疣贅です。 私たちが書きたいのは:
型オブジェクトの型は「type」ですが、これは厳密にCに準拠しておらず、一部のコンパイラは文句を言います。 幸い、このメンバーは PyType_Ready()によって入力されます。
私たちのタイプの名前。 これは、オブジェクトのデフォルトのテキスト表現といくつかのエラーメッセージに表示されます。次に例を示します。
名前は、モジュール名とモジュール内のタイプの名前の両方を含む点線の名前であることに注意してください。 この場合のモジュールはnoddy
で、タイプはNoddy
なので、タイプ名をnoddy.Noddy
に設定します。 ドットのない名前を使用することの1つの副作用は、pydocドキュメントツールがモジュールドキュメントに新しいタイプをリストしないことです。
これは、 PyObject_New()を呼び出したときにPythonが割り当てるメモリの量を認識できるようにするためです。
ノート
型をPythonからサブクラス化可能にし、型の tp_basicsize が基本型と同じである場合、多重継承で問題が発生する可能性があります。 タイプのPythonサブクラスは、最初にタイプを __ bases __ にリストする必要があります。そうしないと、エラーが発生せずにタイプの__new__()
メソッドを呼び出すことができません。 タイプの tp_basicsize の値が基本タイプよりも大きいことを確認することで、この問題を回避できます。 ほとんどの場合、これはとにかく当てはまります。これは、基本タイプがオブジェクトになるか、データメンバーを基本タイプに追加して、そのサイズを大きくするためです。
これは、リストや文字列などの可変長オブジェクトと関係があります。 今はこれを無視してください。
提供していない多くの型メソッドをスキップして、クラスフラグを Py_TPFLAGS_DEFAULT に設定します。
すべてのタイプは、フラグにこの定数を含める必要があります。 これにより、現在のバージョンのPythonで定義されているすべてのメンバーが有効になります。
tp_doc のタイプのドキュメント文字列を提供します。
次に、タイプメソッド、つまりオブジェクトを他のオブジェクトとは異なるものにするものについて説明します。 このバージョンのモジュールでは、これらのいずれも実装しません。 この例を後で拡張して、より興味深い動作を実現します。
今のところ、私たちができることは、新しいNoddy
オブジェクトを作成することだけです。 オブジェクトの作成を有効にするには、 tp_new 実装を提供する必要があります。 この場合、API関数 PyType_GenericNew()によって提供されるデフォルトの実装を使用できます。 これを tp_new スロットに割り当てたいのですが、移植性のために、一部のプラットフォームまたはコンパイラでは、別のプラットフォームで定義された関数で構造体メンバーを静的に初期化できません。 Cモジュールなので、代わりに、 PyType_Ready()を呼び出す直前に、モジュール初期化関数で tp_new スロットを割り当てます。
他のすべての型メソッドは NULL なので、後で説明します—これは後のセクションで説明します。
initnoddy()
の一部のコードを除いて、ファイル内の他のすべてはなじみのあるものでなければなりません。
これにより、Noddy
タイプが初期化され、最初に NULL に設定したob_type
を含む多数のメンバーがファイリングされます。
これにより、タイプがモジュールディクショナリに追加されます。 これにより、Noddy
クラスを呼び出すことにより、Noddy
インスタンスを作成できます。
それでおしまい! 残っているのはそれを構築することだけです。 上記のコードをnoddy.c
というファイルに入れて
setup.py
というファイル内。 次に入力します
$ python setup.py build
シェルでは、サブディレクトリにファイルnoddy.so
を生成する必要があります。 そのディレクトリに移動してPythonを起動します— import noddy
を実行して、Noddyオブジェクトを試すことができるはずです。
それほど難しくはありませんでしたね。
もちろん、現在のNoddyタイプはかなり面白くありません。 データがなく、何もしません。 サブクラス化することもできません。
2.1.1。 基本的な例へのデータとメソッドの追加
基本的な例を拡張して、いくつかのデータとメソッドを追加しましょう。 型も基本クラスとして使えるようにしましょう。 次の機能を追加する新しいモジュールnoddy2
を作成します。
このバージョンのモジュールには、いくつかの変更があります。
追加のインクルードを追加しました:
これには、後で説明するように、属性を処理するために使用する宣言が含まれます。
Noddy
オブジェクト構造の名前がNoddy
に短縮されました。 タイプオブジェクト名はNoddyType
に短縮されました。
Noddy
タイプには、 first 、 last 、および number の3つのデータ属性があります。 first 変数と last 変数は、名前と名前を含むPython文字列です。 number 属性は整数です。
オブジェクト構造はそれに応じて更新されます。
管理するデータができたので、オブジェクトの割り当てと割り当て解除にもっと注意する必要があります。 少なくとも、割り当て解除方法が必要です。
これは tp_dealloc メンバーに割り当てられています:
このメソッドは、2つのPython属性の参照カウントを減らします。 first
およびlast
メンバーは NULL である可能性があるため、ここでは Py_XDECREF()を使用します。 次に、オブジェクトのタイプの tp_free メンバーを呼び出して、オブジェクトのメモリを解放します。 オブジェクトはサブクラスのインスタンスである可能性があるため、オブジェクトのタイプはNoddyType
ではない可能性があることに注意してください。
名前と名前が空の文字列に初期化されていることを確認したいので、新しいメソッドを提供します。
tp_new メンバーにインストールします。
新しいメンバーは、そのタイプのオブジェクトを(初期化するのではなく)作成する責任があります。 Pythonでは__new__()
メソッドとして公開されています。 __new__()
メソッドの詳細については、「Pythonでの型とクラスの統合」というタイトルの論文を参照してください。 新しいメソッドを実装する理由の1つは、インスタンス変数の初期値を保証することです。 この場合、新しいメソッドを使用して、メンバーfirst
およびlast
の初期値が NULL でないことを確認します。 初期値が NULL であるかどうかを気にしない場合は、以前と同様に、 PyType_GenericNew()を新しいメソッドとして使用できます。 PyType_GenericNew()は、すべてのインスタンス変数メンバーを NULL に初期化します。
newメソッドは、インスタンス化される型と型が呼び出されたときに渡される引数が渡される静的メソッドであり、作成された新しいオブジェクトを返します。 新しいメソッドは常に位置引数とキーワード引数を受け入れますが、多くの場合、引数を無視し、引数の処理を初期化メソッドに任せます。 タイプがサブクラス化をサポートしている場合、渡されるタイプは定義されているタイプではない可能性があることに注意してください。 新しいメソッドは、tp_allocスロットを呼び出してメモリを割り当てます。 tp_alloc スロットを自分で埋めることはありません。 むしろ、 PyType_Ready()は、基本クラス(デフォルトではオブジェクト)から継承することで、それを埋めます。 ほとんどのタイプはデフォルトの割り当てを使用します。
ノート
協同組合 tp_new (基本タイプの tp_new または__new__()
を呼び出すもの)を作成する場合は、しないを決定する必要があります。実行時にメソッド解決順序を使用して呼び出すメソッド。 呼び出すタイプを常に静的に決定し、その tp_new を直接、またはtype->tp_base->tp_new
を介して呼び出します。 これを行わないと、他のPython定義クラスからも継承するタイプのPythonサブクラスが正しく機能しない可能性があります。 (具体的には、TypeError
を取得しないと、このようなサブクラスのインスタンスを作成できない場合があります。)
初期化関数を提供します:
tp_init スロットを埋めることによって。
tp_init スロットは、Pythonでは__init__()
メソッドとして公開されています。 作成後にオブジェクトを初期化するために使用されます。 新しいメソッドとは異なり、イニシャライザが呼び出されることを保証することはできません。 イニシャライザは、オブジェクトの選択を解除するときに呼び出されず、オーバーライドできます。 イニシャライザは引数を受け入れて、インスタンスの初期値を提供します。 イニシャライザーは常に位置引数とキーワード引数を受け入れます。
イニシャライザーは複数回呼び出すことができます。 オブジェクトに対して誰でも__init__()
メソッドを呼び出すことができます。 このため、新しい値を割り当てるときは特に注意する必要があります。 たとえば、first
メンバーを次のように割り当てたいと思うかもしれません。
しかし、これは危険です。 私たちのタイプはfirst
メンバーのタイプを制限しないので、どんな種類のオブジェクトでもかまいません。 first
メンバーにアクセスしようとするコードを実行させるデストラクタが含まれている可能性があります。 偏執的であり、この可能性から身を守るために、ほとんどの場合、参照数を減らす前にメンバーを再割り当てします。 いつこれをしなければならないのですか?
- 参照カウントが1より大きいことが絶対にわかっている場合
- オブジェクト 1 の割り当て解除によって、型のコードへの呼び出しが発生しないことがわかっている場合
- ガベージコレクションがサポートされていないときに tp_dealloc ハンドラーで参照カウントをデクリメントする場合 2
インスタンス変数を属性として公開したいと思います。 これを行うにはいくつかの方法があります。 最も簡単な方法は、メンバー定義を定義することです。
定義を tp_members スロットに配置します。
各メンバー定義には、メンバー名、タイプ、オフセット、アクセスフラグ、およびドキュメント文字列があります。 詳細については、以下の汎用属性管理セクションを参照してください。
このアプローチの欠点は、Python属性に割り当てることができるオブジェクトのタイプを制限する方法が提供されないことです。 姓と名は文字列である必要がありますが、任意のPythonオブジェクトを割り当てることができます。 さらに、Cポインタを NULL に設定して、属性を削除することができます。 メンバーが NULL 以外の値に初期化されていることを確認できますが、属性が削除されている場合は、メンバーを NULL に設定できます。
オブジェクト名を名前と名前の連結として出力する単一のメソッドname()
を定義します。
このメソッドは、Noddy
(またはNoddy
サブクラス)インスタンスを最初の引数として取るC関数として実装されます。 メソッドは常に最初の引数としてインスタンスを取ります。 多くの場合、メソッドは位置引数とキーワード引数も取りますが、この場合は何も取りませんし、位置引数タプルまたはキーワード引数辞書を受け入れる必要もありません。 このメソッドは、Pythonメソッドと同等です。
first
およびlast
メンバーが NULL である可能性を確認する必要があることに注意してください。 これは、削除できるため、 NULL に設定されているためです。 これらの属性の削除を防ぎ、属性値を文字列に制限することをお勧めします。 次のセクションでその方法を説明します。
メソッドを定義したので、メソッド定義の配列を作成する必要があります。
そしてそれらを tp_methods スロットに割り当てます。
METH_NOARGS フラグを使用して、メソッドに引数が渡されないことを示していることに注意してください。
最後に、型を基本クラスとして使用できるようにします。 これまでのところ、作成または使用されているオブジェクトのタイプについて何も想定しないようにメソッドを注意深く記述しているため、必要なのは Py_TPFLAGS_BASETYPE をクラスフラグに追加することだけです。意味:
initnoddy()
の名前をinitnoddy2()
に変更し、 Py_InitModule3()に渡されたモジュール名を更新します。
最後に、setup.py
ファイルを更新して、新しいモジュールをビルドします。
2.1.2。 データ属性をより細かく制御できます
このセクションでは、Noddy
の例でfirst
およびlast
属性を設定する方法をより細かく制御します。 以前のバージョンのモジュールでは、インスタンス変数first
およびlast
を文字列以外の値に設定したり、削除したりすることができました。 これらの属性に常に文字列が含まれていることを確認する必要があります。
first
属性とlast
属性をより細かく制御するために、カスタムのゲッター関数とセッター関数を使用します。 first
属性を取得および設定するための関数は次のとおりです。
getter関数には、Noddy
オブジェクトと、voidポインターである「クロージャ」が渡されます。 この場合、クロージャは無視されます。 (クロージャーは、定義データがゲッターとセッターに渡される高度な使用法をサポートします。 これは、たとえば、クロージャー内のデータに基づいて属性を取得または設定することを決定するゲッター関数とセッター関数の単一のセットを許可するために使用できます。)
セッター関数には、Noddy
オブジェクト、新しい値、およびクロージャーが渡されます。 新しい値は NULL である可能性があり、その場合、属性は削除されます。 セッターでは、属性が削除された場合、または属性値が文字列でない場合にエラーが発生します。
PyGetSetDef 構造体の配列を作成します。
tp_getset スロットに登録します。
属性ゲッターとセッターを登録します。
PyGetSetDef 構造体の最後の項目は、上記のクロージャーです。 この場合、クロージャーを使用していないため、 NULL を渡すだけです。
これらの属性のメンバー定義も削除します。
また、 tp_init ハンドラーを更新して、文字列 3 のみを渡すことができるようにする必要があります。
これらの変更により、first
およびlast
メンバーが NULL になることはないため、ほとんどすべての場合に NULL 値のチェックを削除できます。 。 これは、ほとんどの Py_XDECREF()呼び出しを Py_DECREF()呼び出しに変換できることを意味します。 これらの呼び出しを変更できない唯一の場所は、コンストラクターでこれらのメンバーの初期化が失敗した可能性があるデアロケーターです。
また、以前と同じように、初期化関数のモジュール初期化関数とモジュール名の名前を変更し、setup.py
ファイルに追加の定義を追加します。
2.1.3。 周期的なガベージコレクションのサポート
Pythonには、参照カウントがゼロでない場合でも不要なオブジェクトを識別できる循環ガベージコレクターがあります。 これは、オブジェクトがサイクルに関与している場合に発生する可能性があります。 たとえば、次のことを考慮してください。
この例では、それ自体を含むリストを作成します。 削除しても、それ自体からの参照が残っています。 その参照カウントはゼロになりません。 幸い、Pythonのサイクリックガベージコレクターは、最終的にリストがガベージであることを認識し、それを解放します。
Noddy
の例の2番目のバージョンでは、あらゆる種類のオブジェクトをfirst
またはlast
属性 4 に格納できるようにしました。 これは、Noddy
オブジェクトがサイクルに参加できることを意味します。
これはかなりばかげていますが、Noddy
の例にサイクリックガベージコレクターのサポートを追加する言い訳になります。 循環ガベージコレクションをサポートするには、タイプは2つのスロットを埋め、これらのスロットを有効にするクラスフラグを設定する必要があります。
トラバーサルメソッドは、サイクルに参加する可能性のあるサブオブジェクトへのアクセスを提供します。
サイクルに参加できるサブオブジェクトごとに、トラバーサルメソッドに渡されるvisit()
関数を呼び出す必要があります。 visit()
関数は、引数としてサブオブジェクトと、トラバーサルメソッドに渡された追加の引数 arg を取ります。 ゼロ以外の場合に返される必要のある整数値を返します。
Python 2.4以降では、訪問関数の呼び出しを自動化する Py_VISIT()マクロが提供されています。 Py_VISIT()を使用すると、Noddy_traverse()
を簡略化できます。
ノート
tp_traverse 実装では、 Py_VISIT()を使用するために、引数に正確に visit および arg という名前を付ける必要があることに注意してください。 これは、これらの退屈な実装全体の均一性を促進するためです。
また、サイクルに参加できるサブオブジェクトをクリアするためのメソッドを提供する必要があります。
Noddy_clear()
で一時変数が使用されていることに注意してください。 一時変数を使用して、参照カウントをデクリメントする前に各メンバーを NULL に設定できるようにします。 これを行うのは、前に説明したように、参照カウントがゼロに低下すると、オブジェクトを呼び出すコードが実行される可能性があるためです。 さらに、ガベージコレクションをサポートするようになったため、ガベージコレクションをトリガーするコードの実行についても心配する必要があります。 ガベージコレクションが実行されると、 tp_traverse ハンドラーが呼び出される可能性があります。 メンバーの参照カウントがゼロになり、その値が NULL に設定されていない場合、Noddy_traverse()
が呼び出される可能性はありません。
Python 2.4以降では、参照カウントの注意深いデクリメントを自動化する Py_CLEAR()が提供されています。 Py_CLEAR()を使用すると、Noddy_clear()
関数を簡略化できます。
Noddy_dealloc()
は、__del__
メソッドまたはweakrefコールバックを介して任意の関数を呼び出す場合があることに注意してください。 これは、関数内で循環GCをトリガーできることを意味します。 GCは参照カウントがゼロではないと想定しているため、メンバーをクリアする前に PyObject_GC_UnTrack()を呼び出してGCからオブジェクトを追跡解除する必要があります。 これは、 PyObject_GC_UnTrack()とNoddy_clear()
を使用する再実装されたデアロケーターです。
最後に、 Py_TPFLAGS_HAVE_GC フラグをクラスフラグに追加します。
それはほとんどそれです。 カスタムの tp_alloc または tp_free スロットを作成した場合は、循環ガベージコレクション用にそれらを変更する必要があります。 ほとんどの拡張機能は、自動的に提供されるバージョンを使用します。
2.1.4。 他のタイプのサブクラス化
既存のタイプから派生した新しい拡張タイプを作成することが可能です。 拡張機能は必要なPyTypeObject
を簡単に使用できるため、組み込み型から継承するのが最も簡単です。 これらのPyTypeObject
構造を拡張モジュール間で共有するのは難しい場合があります。
この例では、組み込みのlist
タイプを継承するShoddy
タイプを作成します。 新しいタイプは通常のリストと完全に互換性がありますが、内部カウンターを増やす追加のincrement()
メソッドがあります。
ご覧のとおり、ソースコードは前のセクションのNoddy
の例によく似ています。 それらの主な違いを分析します。
派生型オブジェクトの主な違いは、基本型のオブジェクト構造が最初の値でなければならないことです。 基本タイプには、構造の先頭に PyObject_HEAD()がすでに含まれています。
PythonオブジェクトがShoddy
インスタンスの場合、その PyObject * ポインターは PyListObject * と Shoddy * の両方に安全にキャストできます。
この型の__init__
メソッドでは、基本型の__init__
メソッドを呼び出す方法を確認できます。
このパターンは、カスタム new およびdealloc
メソッドを使用して型を作成する場合に重要です。 new メソッドは、 tp_alloc を使用してオブジェクトのメモリを実際に作成するべきではありません。これは、 tp_new を呼び出すときに基本クラスによって処理されます。
Shoddy
タイプの PyTypeObject()に入力すると、tp_base()
用のスロットが表示されます。 クロスプラットフォームコンパイラの問題により、そのフィールドに PyList_Type()を直接入力することはできません。 後でモジュールのinit()
関数で実行できます。
PyType_Ready()を呼び出す前に、型構造体に tp_base スロットを埋める必要があります。 新しいタイプを導出する場合、 tp_alloc スロットに PyType_GenericNew()を入力する必要はありません。基本タイプからのallocate関数が継承されます。
その後、 PyType_Ready()を呼び出して、型オブジェクトをモジュールに追加することは、基本的なNoddy
の例と同じです。
2.2。 タイプメソッド
このセクションの目的は、実装できるさまざまなタイプのメソッドとその機能について簡単に説明することです。
PyTypeObject の定義は次のとおりです。デバッグビルドでのみ使用される一部のフィールドは省略されています:
これが lot のメソッドです。 ただし、あまり心配する必要はありません。定義したい型がある場合は、これらのほんの一握りしか実装しない可能性が非常に高くなります。
ご想像のとおり、これについては、さまざまなハンドラーについて詳しく説明します。 フィールドの順序に影響を与える歴史的な手荷物がたくさんあるため、構造で定義されている順序では進みません。 タイプの初期化により、フィールドが正しい順序に保たれるようにしてください。 多くの場合、必要なすべてのフィールドを含む例を見つけて(0
に初期化されている場合でも)、新しいタイプに合わせて値を変更するのが最も簡単です。
タイプの名前-前のセクションで述べたように、これはさまざまな場所に表示され、ほぼ完全に診断目的で表示されます。 そのような状況で役立つものを選択してみてください!
これらのフィールドは、このタイプの新しいオブジェクトが作成されたときに割り当てるメモリの量をランタイムに通知します。 Pythonには、 tp_itemsize フィールドの出番である可変長構造(文字列、リストなど)のサポートが組み込まれています。 これは後で処理されます。
ここに、Pythonスクリプトがobj.__doc__
を参照してドキュメント文字列を取得するときに返される文字列(またはそのアドレス)を配置できます。
ここで、基本的な型メソッド、つまりほとんどの拡張型が実装するメソッドについて説明します。
2.2.1。 ファイナライズと割り当て解除
この関数は、型のインスタンスの参照カウントがゼロに減少し、Pythonインタープリターがそれを再利用したいときに呼び出されます。 タイプに解放するメモリまたは実行するその他のクリーンアップがある場合は、ここに配置できます。 オブジェクト自体もここで解放する必要があります。 この関数の例を次に示します。
デアロケーター関数の重要な要件の1つは、保留中の例外をそのままにしておくことです。 インタプリタがPythonスタックを巻き戻すときに、デアロケータが頻繁に呼び出されるため、これは重要です。 (通常のリターンではなく)例外が原因でスタックが巻き戻された場合、例外がすでに設定されていることをデロケーターが認識しないように保護するために何も行われません。 追加のPythonコードが実行される可能性のある、デアロケーターが実行するアクションは、例外が設定されたことを検出する場合があります。 これは、インタプリタからの誤解を招くエラーにつながる可能性があります。 これを防ぐ適切な方法は、安全でないアクションを実行する前に保留中の例外を保存し、完了したらそれを復元することです。 これは、 PyErr_Fetch()および PyErr_Restore()関数を使用して実行できます。
2.2.2。 オブジェクトのプレゼンテーション
Pythonでは、オブジェクトのテキスト表現を生成する3つの方法があります。 repr()関数(または同等のバックティック構文)、 str()関数、および print ステートメント。 ほとんどのオブジェクトでは、 print ステートメントは str()関数と同等ですが、必要に応じてFILE*
への特殊な印刷が可能です。 これは、効率が問題として識別され、ファイルに書き込む一時的な文字列オブジェクトの作成にコストがかかりすぎることがプロファイリングによって示唆されている場合にのみ実行する必要があります。
これらのハンドラーはすべてオプションであり、ほとんどのタイプはせいぜい tp_str および tp_repr ハンドラーを実装する必要があります。
tp_repr ハンドラーは、呼び出されたインスタンスの表現を含む文字列オブジェクトを返す必要があります。 簡単な例を次に示します。
tp_repr ハンドラーが指定されていない場合、インタープリターは、型の tp_name とオブジェクトの一意の識別値を使用する表現を提供します。
tp_str ハンドラーは str()に対して、上記の tp_repr ハンドラーは repr()に対してです。 つまり、Pythonコードがオブジェクトのインスタンスで str()を呼び出すときに呼び出されます。 その実装は tp_repr 関数と非常に似ていますが、結果の文字列は人間が消費することを目的としています。 tp_str が指定されていない場合は、代わりに tp_repr ハンドラーが使用されます。
簡単な例を次に示します。
Pythonが型のインスタンスを「印刷」する必要があるときはいつでも、print関数が呼び出されます。 たとえば、「node」がタイプTreeNodeのインスタンスである場合、Pythonコードが呼び出すときにprint関数が呼び出されます。
flags引数と1つのフラグPy_PRINT_RAW
があり、文字列引用符なしで、場合によってはエスケープシーケンスを解釈せずに印刷することをお勧めします。
print関数は、引数としてファイルオブジェクトを受け取ります。 そのファイルオブジェクトに書き込みたいと思うでしょう。
印刷機能の例を次に示します。
2.2.3。 属性管理
属性をサポートできるすべてのオブジェクトについて、対応するタイプは、属性の解決方法を制御する関数を提供する必要があります。 属性を取得できる関数(定義されている場合)と、属性を設定する関数(属性の設定が許可されている場合)が必要です。 属性の削除は特殊なケースであり、ハンドラーに渡される新しい値は NULL です。
Pythonは、2組の属性ハンドラーをサポートしています。 属性をサポートするタイプは、1つのペアの関数を実装するだけで済みます。 違いは、一方のペアが属性の名前をchar*
として受け取り、もう一方のペアがPyObject*
を受け入れることです。 各タイプは、実装の便宜のためにより意味のあるペアを使用できます。
オブジェクトの属性へのアクセスが常に単純な操作である場合(これについては後で説明します)、属性管理関数のPyObject*
バージョンを提供するために使用できる一般的な実装があります。 Python 2.2以降、タイプ固有の属性ハンドラーの実際の必要性はほぼ完全になくなりましたが、利用可能な新しい汎用メカニズムの一部を使用するように更新されていない例が多数あります。
2.2.3.1。 ジェネリック属性管理
バージョン2.2の新機能。
ほとんどの拡張タイプは、 simple 属性のみを使用します。 では、何が属性を単純にするのでしょうか? 満たす必要のある条件は2つだけです。
- PyType_Ready()を呼び出すときは、属性の名前を知っている必要があります。
- 属性が検索または設定されたことを記録するために特別な処理は必要ありません。また、値に基づいてアクションを実行する必要もありません。
このリストでは、属性の値、値の計算時期、または関連データの保存方法に制限がないことに注意してください。
PyType_Ready()が呼び出されると、型オブジェクトによって参照される3つのテーブルを使用して、型オブジェクトのディクショナリに配置される記述子が作成されます。 各記述子は、インスタンスオブジェクトの1つの属性へのアクセスを制御します。 各テーブルはオプションです。 3つすべてが NULL の場合、型のインスタンスは基本型から継承された属性のみを持ち、 tp_getattro および tp_setattro フィールドを残す必要があります。 ] NULL も同様に、基本型が属性を処理できるようにします。
テーブルは、タイプオブジェクトの3つのフィールドとして宣言されています。
tp_methods が NULL でない場合は、 PyMethodDef 構造体の配列を参照する必要があります。 テーブルの各エントリは、この構造のインスタンスです。
タイプによって提供されるメソッドごとに1つのエントリを定義する必要があります。 基本型から継承されたメソッドにはエントリは必要ありません。 最後に1つの追加エントリが必要です。 これは、配列の終わりを示す番兵です。 番兵のml_name
フィールドは NULL である必要があります。
XXX次のセクションで共有される、構造フィールドの統一された議論を参照する必要があります。
2番目のテーブルは、インスタンスに格納されているデータに直接マップする属性を定義するために使用されます。 さまざまなプリミティブCタイプがサポートされており、アクセスは読み取り専用または読み取り/書き込みの場合があります。 表の構造は次のように定義されています。
テーブルのエントリごとに、記述子が作成され、インスタンス構造から値を抽出できるタイプに追加されます。 type フィールドには、structmember.h
ヘッダーで定義されているタイプコードの1つが含まれている必要があります。 この値は、Python値をC値に変換する方法とC値から変換する方法を決定するために使用されます。 flags
フィールドは、属性へのアクセス方法を制御するフラグを格納するために使用されます。
XXXこれの一部を共有セクションに移動する必要があります!
structmember.h
では次のフラグ定数が定義されています。 それらはビットごとのORを使用して組み合わせることができます。
絶え間ない | 意味 |
---|---|
READONLY
|
決して書き込み可能ではありません。 |
RO
|
READONLY の省略形。
|
READ_RESTRICTED
|
制限付きモードでは読み取りできません。 |
WRITE_RESTRICTED
|
制限付きモードでは書き込みできません。 |
RESTRICTED
|
制限付きモードでは読み取りまたは書き込みができません。 |
tp_members テーブルを使用して実行時に使用される記述子を作成することの興味深い利点は、この方法で定義された属性は、テーブルにテキストを提供するだけで、関連付けられたドキュメント文字列を持つことができることです。 アプリケーションは、イントロスペクションAPIを使用してクラスオブジェクトから記述子を取得し、__doc__
属性を使用してドキュメント文字列を取得できます。
tp_methods テーブルと同様に、name
値が NULL のセンチネルエントリが必要です。
2.2.3.2。 タイプ固有の属性管理
簡単にするために、ここではchar*
バージョンのみを示します。 nameパラメータのタイプは、インターフェイスのchar*
フレーバーとPyObject*
フレーバーの唯一の違いです。 この例は、上記の一般的な例と効果的に同じことを行いますが、Python2.2で追加された一般的なサポートを使用していません。 これを示すことの価値は2つあります。古いバージョンのPythonに移植可能な方法で基本的な属性管理を行う方法を示し、ハンドラー関数が呼び出される方法を説明します。そのため、必要に応じて機能を拡張する必要があります。 、何をする必要があるかを理解できます。
tp_getattr ハンドラーは、オブジェクトが属性ルックアップを必要とするときに呼び出されます。 これは、クラスの__getattr__()
メソッドが呼び出されるのと同じ状況で呼び出されます。
これを処理する可能性のある方法は、(1)一連の関数(以下の例のnewdatatype_getSize()
やnewdatatype_setSize()
など)を実装すること、(2)これらの関数をリストするメソッドテーブルを提供すること、および(3)そのテーブルでのルックアップの結果を返すgetattr関数を提供します。 メソッドテーブルは、タイプオブジェクトの tp_methods フィールドと同じ構造を使用します。
次に例を示します。
tp_setattr ハンドラーは、クラスインスタンスの__setattr__()
または__delattr__()
メソッドが呼び出されるときに呼び出されます。 属性を削除する必要がある場合、3番目のパラメーターは NULL になります。 これは単に例外を発生させる例です。 これが本当に必要なすべてである場合は、 tp_setattr ハンドラーを NULL に設定する必要があります。
2.2.4。 オブジェクトの比較
tp_compare ハンドラーは、比較が必要であり、オブジェクトが要求された比較に一致する特定のリッチ比較メソッドを実装していない場合に呼び出されます。 (定義され、 PyObject_Compare()または PyObject_Cmp()関数が使用されている場合、または cmp()がPythonから使用されている場合は、常に使用されます。) __cmp__()
メソッドに類似しています。 この関数は、 obj1 が obj2 より小さい場合は-1
を返し、等しい場合は0
を返し、の場合は1
を返す必要があります。 ] obj1 が obj2 より大きい。 (以前は、それぞれより小さいおよびより大きい任意の負または正の整数を返すことが許可されていました。Python2.2以降、これは許可されなくなりました。 将来的には、他の戻り値に別の意味が割り当てられる可能性があります。)
tp_compare ハンドラーで例外が発生する場合があります。 この場合、負の値を返す必要があります。 呼び出し元は、 PyErr_Occurred()を使用して例外をテストする必要があります。
実装例は次のとおりです。
2.2.5。 抽象プロトコルのサポート
Pythonは、さまざまな abstract 'プロトコル;'をサポートしています。 これらのインターフェースを使用するために提供される特定のインターフェースは、 Abstract Objects Layer に記載されています。
これらの抽象的なインターフェースの多くは、Python実装の開発の初期に定義されました。 特に、数、マッピング、およびシーケンスプロトコルは、当初からPythonの一部でした。 他のプロトコルは時間の経過とともに追加されてきました。 型の実装からのいくつかのハンドラールーチンに依存するプロトコルの場合、古いプロトコルは、型オブジェクトによって参照されるハンドラーのオプションのブロックとして定義されています。 新しいプロトコルの場合、メインタイプオブジェクトに追加のスロットがあり、スロットが存在することを示すフラグビットが設定されており、インタプリタがチェックする必要があります。 (フラグビットは、スロット値が非 NULL であることを示していません。 フラグはスロットの存在を示すために設定できますが、スロットはまだ埋められていない可能性があります。)
オブジェクトを数値、シーケンス、またはマッピングオブジェクトのように機能させたい場合は、Cタイプ PyNumberMethods 、 PySequenceMethods を実装する構造体のアドレスを配置します。 、または PyMappingMethods 。 この構造に適切な値を入力するのはあなた次第です。 これらのそれぞれの使用例は、PythonソースディストリビューションのObjects
ディレクトリにあります。
この関数を提供することを選択した場合、データ型のインスタンスのハッシュ番号を返す必要があります。 これは適度に無意味な例です:
この関数は、データ型のインスタンスが「呼び出された」ときに呼び出されます。たとえば、obj1
がデータ型のインスタンスであり、Pythonスクリプトにobj1('hello')
が含まれている場合、 tp_call ハンドラーが呼び出されます。
この関数は3つの引数を取ります。
- arg1 は、呼び出しの対象となるデータ型のインスタンスです。 呼び出しが
obj1('hello')
の場合、 arg1 はobj1
です。 - arg2 は、呼び出しへの引数を含むタプルです。 PyArg_ParseTuple()を使用して引数を抽出できます。
- arg3 は、渡されたキーワード引数の辞書です。 これが NULL 以外で、キーワード引数をサポートしている場合は、 PyArg_ParseTupleAndKeywords()を使用して引数を抽出します。 キーワード引数をサポートしたくなく、これが NULL 以外の場合は、
TypeError
を発生させて、キーワード引数がサポートされていないことを示すメッセージを表示します。
これは、呼び出し関数の実装の卑劣な例です。
XXXここにいくつかのフィールドを追加する必要があります…
これらの関数は、イテレータプロトコルのサポートを提供します。 コンテンツ(反復中に生成される可能性がある)の反復をサポートするオブジェクトは、tp_iter
ハンドラーを実装する必要があります。 tp_iter
ハンドラーによって返されるオブジェクトは、tp_iter
ハンドラーとtp_iternext
ハンドラーの両方を実装する必要があります。 両方のハンドラーは、呼び出されているインスタンスという1つのパラメーターを受け取り、新しい参照を返します。 エラーの場合は、例外を設定して NULL を返す必要があります。
反復可能なコレクションを表すオブジェクトの場合、tp_iter
ハンドラーはイテレーターオブジェクトを返す必要があります。 イテレータオブジェクトは、反復の状態を維持する役割を果たします。 (リストやタプルのように)互いに干渉しない複数のイテレーターをサポートできるコレクションの場合は、新しいイテレーターを作成して返す必要があります。 (通常は反復の副作用のために)1回だけ反復できるオブジェクトは、自身への新しい参照を返すことによってこのハンドラーを実装する必要があり、tp_iternext
ハンドラーも実装する必要があります。 ファイルオブジェクトは、そのようなイテレータの例です。
イテレータオブジェクトは両方のハンドラを実装する必要があります。 tp_iter
ハンドラーは、イテレーターへの新しい参照を返す必要があります(これは、破壊的にのみ反復できるオブジェクトのtp_iter
ハンドラーと同じです)。 tp_iternext
ハンドラーは、反復内の次のオブジェクトがある場合は、そのオブジェクトへの新しい参照を返す必要があります。 反復が終了に達した場合、例外を設定せずに NULL を返すか、StopIteration
を設定する場合があります。 例外を回避すると、パフォーマンスがわずかに向上する可能性があります。 実際のエラーが発生した場合は、例外を設定して NULL を返す必要があります。
2.2.6。 弱参照サポート
Pythonの弱参照実装の目標の1つは、弱参照の恩恵を受けないオブジェクト(数値など)にオーバーヘッドを発生させることなく、任意の型が弱参照メカニズムに参加できるようにすることです。
オブジェクトを弱参照可能にするには、拡張で弱参照メカニズムを使用するためにインスタンス構造にPyObject*
フィールドを含める必要があります。 オブジェクトのコンストラクターによって NULL に初期化する必要があります。 また、対応するタイプオブジェクトの tp_weaklistoffset フィールドをフィールドのオフセットに設定する必要があります。 たとえば、インスタンスタイプは次の構造で定義されます。
インスタンスの静的に宣言された型オブジェクトは、次のように定義されます。
型コンストラクターは、弱参照リストを NULL に初期化する役割を果たします。
さらに唯一の追加は、デストラクタが弱参照マネージャーを呼び出して弱参照をクリアする必要があることです。 これは、弱参照リストが NULL 以外の場合にのみ必要です。
2.2.7。 その他の提案
これらの関数のほとんどは省略できることに注意してください。その場合、値として0
を指定します。 提供する必要のある関数ごとにタイプ定義があります。 これらは、Pythonのソースディストリビューションに付属するPythonインクルードディレクトリのobject.h
にあります。
新しいデータ型に特定のメソッドを実装する方法を学習するには、次の手順を実行します。Pythonソースディストリビューションをダウンロードして解凍します。 Objects
ディレクトリに移動し、Cソースファイルでtp_
と必要な機能(たとえば、tp_print
またはtp_compare
)を検索します。 実装したい関数の例があります。
オブジェクトが実装しているタイプのインスタンスであることを確認する必要がある場合は、 PyObject_TypeCheck()関数を使用してください。 その使用例は次のようになります。
脚注
- 1
- これは、オブジェクトが文字列や浮動小数点数などの基本型であることがわかっている場合に当てはまります。
- 2
- この例では、 tp_dealloc ハンドラーでこれに依存しました。これは、このタイプがガベージコレクションをサポートしていないためです。 タイプがガベージコレクションをサポートしている場合でも、ガベージコレクションからオブジェクトを「追跡解除」するための呼び出しがありますが、これらの呼び出しは高度であり、ここでは説明しません。
- 3
- 最初と最後のメンバーが文字列であることがわかったので、参照カウントを減らすことにあまり注意を払わないかもしれませんが、文字列サブクラスのインスタンスを受け入れます。 通常の文字列の割り当てを解除してもオブジェクトが呼び出されない場合でも、文字列サブクラスのインスタンスの割り当てを解除してもオブジェクトが呼び出されないことを保証することはできません。
- 4
- 3番目のバージョンでも、サイクルを回避することは保証されていません。 文字列サブクラスのインスタンスが許可され、通常の文字列では許可されていない場合でも、文字列サブクラスでサイクルが許可される可能性があります。