2

PyCFunction_New を中心に展開するPyCXXコード (C++ Python ラッパー)を理解するのに苦労しています。

誰かがこの機能がどのように機能するか説明できますか?

(CPythonソース コードからはわかりません。)


ここでは、私が抱えている問題について詳しく説明します。これはおそらくそれほど一般的な用途ではないため、上記の行に罫線を付けました。

尋ねる理由は、私が奇妙なコードを扱っているからです。私はキーワードメソッドハンドラ関数を持っています:

    static PyObject* keyword_handler( PyObject* _self_and_name_tuple, 
                                      PyObject* _args, 
                                      PyObject* _keywords ) { }

次のように保存されています。

PyMethodDef meth_def_ext;
meth_def_ext.ml_meth = reinterpret_cast<PyCFunction>( _handler );
meth_def.ml_flags = METH_VARARGS | METH_KEYWORDS;

次に、PyCFunction_New にバンドルされます。

        MethodDefExt<T>* method_def_ext = ...;

        Tuple args{2}; // Tuple wraps a CPython Tuple
        args[0] = Object{ this };
        args[1] = Object{ PyCapsule_New( (void*)method_def_ext, nullptr, nullptr ), true };

        PyObject* func = PyCFunction_New( & method_def_ext->meth_def, args.ptr() );

        return Object(func, true);
    }

最初のパラメーターが args (ハンドラーの _self_and_name_tuple の最初のパラメーターと一致する) である 3 パラメーター関数に、CPython が型キャストを処理すると仮定するのは正しいでしょうか?

そして、CPython は、「myFunc(7, a=1)」を解析しなければならないという事実から、実際には 3-param 関数とも呼ばれるキーワードを実際に扱っていることを知るだけでしょうか?

これは正しくないようです。

たぶん、CPython は args 1を PyMethodDef に型キャストし、.ml_flags を検査しています。

それが起こっている場合、私が作業しているコードには次のようなものがあるため、知る必要があります。

template<class T>
class MethodDefExt //: public PyMethodDef <-- I commented this out
{
    // ... Constructors ...

    PyMethodDef               meth_def;

    method_noargs_function_t  ext_noargs_function  = nullptr;
    method_varargs_function_t ext_varargs_function = nullptr;
    method_keyword_function_t ext_keyword_function = nullptr;

    Object                    py_method;
};

元の形式では、PyMethodDef の 2 つのコピーがあったに違いないと思います。

これが実際に起こっている場合、つまり、このクラスが実際に PyCFunction_New の内部によって PyMethodDef に型キャストされている場合、これは危険です。

確かに、誰かが MethodDefExt の前にメンバー変数を追加すると、型キャストが壊れます。これは薄っぺらい…


私が扱っているクラスは、将来の C++ コーダーがカスタム Python タイプを実装し、このタイプ内で Python から呼び出すことができるメソッドを実装できるようにします。

そこで彼らはMyExt : CustomExtを派生させ、メソッドを書きます:

// one of these three
MyExt::foo(){...} 
MyExt::foo(PyObject* args){...}
MyExt::foo(PyObject* args, PyObject* kw){...}

次に、これら 3 つの関数の適切な 1 つを呼び出して、このメソッドをlookupに格納する必要があります。

    typedef Object (T::*method_noargs_function_t)();
    static void add_noargs_method( const char* name, 
                                   method_noargs_function_t function ) {
        lookup()[std::string{name}] = new MethodDefExt<T> 
                                   {name,function,noargs_handler,doc};
    }

    typedef Object (T::*method_varargs_function_t)( const Tuple& args );
    static void add_varargs_method( const char* name, 
                                    method_varargs_function_t function ) {
        lookup()[std::string{name}] = new MethodDefExt<T> 
                                    {name,function,varargs_handler,doc};
    }

    typedef Object (T::*method_keyword_function_t)( const Tuple& args, const Dict& kws );
    static void add_keyword_method( const char* name, 
                                    method_keyword_function_t function ) {
        lookup()[std::string{name}] = new MethodDefExt<T> 
                                    {name,function,keyword_handler,doc};
    }

それぞれに関連付けられたハンドラ関数があることに注意してください。これらのハンドラ関数は、CustomExtの静的メソッドです。静的メソッドへのポインタは CPython から呼び出すことができるためです。つまり、これは単なる標準 C スタイルの関数ポインタです。

したがって、Python がこのfoo関数のポインターを必要とする場合、ここでインターセプトします。

    // turn a name into function object
    virtual Object getattr_methods( const char* _name )
    {
        std::string name{ _name };

        // see if name exists and get entry with method
        auto i = lookup().find( name );

        DBG_LINE( "packaging relevant C++ method and extension object instance into PyCFunction" );

        // assume name was found in the method map
        MethodDefExt<T>* method_def_ext = i->second;

        // this must be the _self_and_name_tuple that gets received
        //   as the first parameter by the handler
        Tuple args{2};

        args[0] = Object{ this };
        args[1] = Object{ PyCapsule_New( (void*)method_def_ext, nullptr, nullptr ), true };

このメソッドのハンドラーを呼び出す Python 関数を作成します (このオブジェクト args[0] にメソッド自体の詳細 args 1を渡します)。ハンドラーは、エラーをトラップしながらメソッドを実行します。

この時点ではハンドラーを実行しないことに注意してください。代わりに、この Python 関数を Python ランタイムに戻します。Python コーダーは、関数を実行することを望まず、関数へのポインターを取得したかっただけかもしれません: fp = MyExt.func;

        PyObject* func = PyCFunction_New( & method_def_ext->meth_def, args.ptr() );

X (下記参照) & method_def_ext->meth_def は、3 つのハンドラーの 1 つであるハンドラー関数を引き出します。ただし、MethodDefExt s コンストラクターのおかげで、それらはすべて PyCFunction オブジェクトに型キャストされています。これは、キーワード ハンドラーのパラメーター リストが間違っていることを意味します。

        return Object(func, true);
    }

(SOのフォーマッタがコメントをコードコメントとして処理していなかったため、コメントを分割する必要がありました)

私が苦労しているのはこれです: fooがキーワードを取る関数であるとしましょう。したがって、その署名は次のようになります。

MyExt::foo(PyObject* args, PyObject* kw)

一致するハンドラーは次のようになります。

    static PyObject* noargs_handler( PyObject* _self_and_name_tuple, 
                                     PyObject*  ) { }

    static PyObject* varargs_handler( PyObject* _self_and_name_tuple, 
                                      PyObject* _args ) { }

    static PyObject* keyword_handler( PyObject* _self_and_name_tuple, 
                                      PyObject* _args, 
                                      PyObject* _keywords ) { }

つまり、3番目のもの。Python が追加の最初の_self_and_name_tupleパラメータを提供していることを読みました。

foo をルックアップに登録するときは、次のハンドラーを提供します。

    typedef                               Object (T::*method_keyword_function_t)( const Tuple& args, const Dict& kws );
    static void add_keyword_method( const char* name, method_keyword_function_t function ) {
        methods()[std::string{name}] = new MethodDefExt<T> {name,function,keyword_handler,doc};
    }

MethodDefExtの特定のコンストラクターを見ると、

    // VARARGS + KEYWORD
    MethodDefExt (
        const char* _name,
        method_keyword_function_t _function,
        method_keyword_call_handler_t _handler
    )
    {
        meth_def.ml_name = const_cast<char *>( _name );
        meth_def.ml_doc  = nullptr;
        meth_def.ml_meth = reinterpret_cast<PyCFunction>( _handler );
        meth_def.ml_flags = METH_VARARGS | METH_KEYWORDS;

        ext_noargs_function = nullptr;
        ext_varargs_function = nullptr;
        ext_keyword_function = _function;
    }

... このハンドラーをPyCFunctionに型キャストすることがわかります

しかし、PyCFunctionは 2 つの引数しか取りません!!!

typedef PyObject *(*PyCFunction)(PyObject *, PyObject *);

これにハンドラーを型キャストしています。そして、これらのハンドラーには 2 つまたは 3 つのパラメーターがあります。

これは本当に間違っているように見えます。

そして、上記のように、CPython が foo を実行したい場合、このmeth_def.ml_methを取得してPyCFunction_Newにフィードします。

        Tuple args{2};

        args[0] = Object{ this };
        args[1] = Object{ PyCapsule_New( (void*)method_def_ext, nullptr, nullptr ), true };

        PyObject* func = PyCFunction_New( & method_def_ext->meth_def, args.ptr() ); // https://github.com/python/cpython/blob/master/Objects/methodobject.c#L19-L48

だから私は推測することができます: * PyCFunction_New の最初のパラメーターは PyCFunction 関数ポインターでなければなりません * 2 番目のパラメーターは PyObject でなければなりません* _self_and_name_tuple

そして、これを CPython にフィードバックしています。私の推測では、CPython が 'foo(7, a=1,b=2)' を使用したい場合、7 を args にパッケージ化し、a=1,b=2 を kwds にパッケージ化して呼び出します。 :

[the PyCFunction function pointer](_self_and_name_tuple, args, kwds)
4

2 に答える 2

4

私は答えを危険にさらします:

PyObject* PyCFunction_New(PyMethodDef* ml, PyObject* data)

PyCFunction_New はおそらく Callable-Type PyObject を作成し、関数 (ml でラップ) と追加データ (self でラップ) でプライミングされます。

2 番目のパラメーターは何でもかまいません。実際、PyObject* である必要さえありません。Python が ml 内にパッケージ化された関数を実行すると、これが最初の引数になります。以下に詳述するように、後続の引数は ml->ml_flags に依存します。

最初のパラメーターは、関数をカプセル化するために使用できる PyMethodDef オブジェクトです。

struct PyMethodDef {
    const char  *ml_name;   /* The name of the built-in function/method */
    PyCFunction ml_meth;    /* The C function that implements it */
    int         ml_flags;   /* Combination of METH_xxx flags, which mostly
                               describe the args expected by the C func */
    const char  *ml_doc;    /* The __doc__ attribute, or NULL */
};
typedef struct PyMethodDef PyMethodDef;

したがって、(特定の)関数ポインターが含まれています。

typedef PyObject *(*PyCFunction)(PyObject*, PyObject*);

...そして旗、

/* Flag passed to newmethodobject */
/* #define METH_OLDARGS  0x0000   -- unsupported now */
#define METH_VARARGS  0x0001
#define METH_KEYWORDS 0x0002
/* METH_NOARGS and METH_O must not be combined with the flags above. */
#define METH_NOARGS   0x0004
#define METH_O        0x0008

https://docs.python.org/3.4/c-api/structures.html

この方法で 3 種類の関数を Python に渡すことができます。

PyObject*foo( PyObject* data )                                 // ml_meth=METH_NOARGS
PyObject*foo( PyObject* data, PyObject* args )                 // ml_meth=METH_VARARGS
PyObject*foo( PyObject* data, PyObject* args, PyObject* kwds ) // ml_meth=METH_KEYWORDS

編集: https://docs.python.org/3/tutorial/classes.html#method-objects

メソッドがどのように機能するかをまだ理解していない場合は、実装を確認すると問題が明確になる可能性があります。データ属性ではないインスタンス属性が参照されると、そのクラスが検索されます。名前が関数オブジェクトである有効なクラス属性を示す場合、メソッド オブジェクトは、インスタンス オブジェクトと関数オブジェクトをまとめて抽象オブジェクトにまとめることによって作成されます (これがメソッド オブジェクトです)。引数リストを指定してメソッド オブジェクトを呼び出すと、インスタンス オブジェクトと引数リストから新しい引数リストが作成され、この新しい引数リストを使用して関数オブジェクトが呼び出されます。

于 2014-11-03T18:16:55.183 に答える