Документация для PyCFunction_New / PyCFunction_NewEx

Я изо всех сил пытаюсь понять некоторый код PyCXX (оболочка C++ Python), который вращается вокруг PyCFunction_New.

Может кто-нибудь объяснить, как работает эта функция?

(Я не могу понять это из исходного кода 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);
    }

Прав ли я, если предположить, что CPython позаботится о том, чтобы привести его обратно к функции с тремя параметрами, где первым параметром является args (что соответствует первому параметру обработчика _self_and_name_tuple)?

И CPython будет знать только из того факта, что ему нужно выполнить синтаксический анализ: 'myFunc(7, a=1)', что он на самом деле имеет дело с ключевым словом, известным как функция 3-param?

Это не выглядит правильно.

Может быть, CPython возвращает типы 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. И первый никогда не трогали, потому что это был базовый класс.

Если это действительно происходит, т. Е. Если этот класс действительно возвращает тип обратно в PyMethodDef с помощью внутренних средств PyCFunction_New, то это хитроумно.

Конечно, кто-то может добавить переменную-член в начале MethodDefExt, и тогда приведение типов прервется. Это хрупкий...


Класс, с которым я имею дело, позволяет будущему кодеру C++ реализовывать пользовательский тип Python, а в этом типе - реализовывать методы, которые можно вызывать из Python.

Таким образом, они получают MyExt: CustomExt и пишут метод:

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

Теперь они должны сохранить этот метод в поиске, вызвав соответствующую одну из этих трех функций:

    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 извлекает функцию-обработчик, которая является одним из трех обработчиков. Однако, благодаря конструкторам MethodDefExt, все они были преобразованы по типу в объекты 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 ) { }

т.е. третий. Я читал, что 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 принимает только два аргумента!!!

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 * второй параметр должен быть PyObject* _self_and_name_tuple

И мы возвращаем это обратно в CPython. Я предполагаю, что когда CPython хочет использовать 'foo(7, a=1,b=2)', он упаковывает 7 в аргументы, a = 1, b = 2 в kwds и вызывает:

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

2 ответа

Решение

Я рискну ответить:

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

PyCFunction_New, вероятно, создает PyObject Callable-Type, заполненный функцией (в мл) и дополнительными данными (в самом себе)

Второй параметр может быть любым, на самом деле он даже не должен быть 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

Если вы до сих пор не понимаете, как работают методы, возможно, взгляд на реализацию может прояснить ситуацию. При обращении к атрибуту экземпляра, который не является атрибутом данных, выполняется поиск его класса. Если имя обозначает допустимый атрибут класса, который является объектом функции, объект метода создается путем упаковки (указателей на) объекта экземпляра и объекта функции, только что найденных вместе в абстрактном объекте: это объект метода. Когда объект метода вызывается со списком аргументов, новый список аргументов строится из объекта экземпляра и списка аргументов, а объект функции вызывается с этим новым списком аргументов.

У меня также был тот же вопрос в поисках документации по обеим функциям. Погуглив, я нашел эту ссылку: https://github.com/python/cpython/blob/main/Objects/methodobject.c

Увидев код CPython, мне стало легче понять предмет.

      PyObject *
PyCFunction_New(PyMethodDef *ml, PyObject *self)
{
    return PyCFunction_NewEx(ml, self, NULL);
}

PyObject *
PyCFunction_NewEx(PyMethodDef *ml, PyObject *self, PyObject *module)
{
    return PyCMethod_New(ml, self, module, NULL);
}

PyObject *
PyCMethod_New(PyMethodDef *ml, PyObject *self, PyObject *module, PyTypeObject *cls)
{
    /* Figure out correct vectorcall function to use */
    vectorcallfunc vectorcall;
    switch (ml->ml_flags & (METH_VARARGS | METH_FASTCALL | METH_NOARGS |
                            METH_O | METH_KEYWORDS | METH_METHOD))
    {
        case METH_VARARGS:
        case METH_VARARGS | METH_KEYWORDS:
            /* For METH_VARARGS functions, it's more efficient to use tp_call
             * instead of vectorcall. */
            vectorcall = NULL;
            break;
        case METH_FASTCALL:
            vectorcall = cfunction_vectorcall_FASTCALL;
            break;
        case METH_FASTCALL | METH_KEYWORDS:
            vectorcall = cfunction_vectorcall_FASTCALL_KEYWORDS;
            break;
        case METH_NOARGS:
            vectorcall = cfunction_vectorcall_NOARGS;
            break;
        case METH_O:
            vectorcall = cfunction_vectorcall_O;
            break;
        case METH_METHOD | METH_FASTCALL | METH_KEYWORDS:
            vectorcall = cfunction_vectorcall_FASTCALL_KEYWORDS_METHOD;
            break;
        default:
            PyErr_Format(PyExc_SystemError,
                         "%s() method: bad call flags", ml->ml_name);
            return NULL;
    }

PyCFunction_New и PyCFunction_NewEx — это просто частные случаи PyCMethod_New.

Другие вопросы по тегам