如何验证 boost::python::object 是带有参数的函数签名

How to validated a boost::python::object is a function signature with an argument

如何验证 boost::python::object 参数是带有参数的 python 函数签名?

void subscribe_py(boost::python::object callback){
        //check callback is a function signature


    }

Boost.Python 不提供更高级别的类型来帮助执行内省。但是,可以使用 Python C-API 的 PyCallable_Check() to check if a Python object is callable, and then use a Python introspection module, such as inspect 来确定可调用对象的签名。 Boost.Python 在 C++ 和 Python 之间的互操作性使得使用 Python 模块相当无缝。

这里是一个辅助函数,require_arity(fn, n) 要求表达式 fn(a_0, a_1, ... a_n) 有效:

/// @brief Given a Python object `fn` and an arity of `n`, requires
///        that the expression `fn(a_0, a_1, ..., a_2` to be valid.
///        Raise TypeError if `fn` is not callable and `ValueError`
///        if `fn` is callable, but has the wrong arity.
void require_arity(
  std::string name,
  boost::python::object fn,
  std::size_t arity)
{
  namespace python = boost::python;

  std::stringstream error_msg;
  error_msg << name << "() must take exactly " << arity << " arguments";

  // Throw if the callback is not callable.
  if (!PyCallable_Check(fn.ptr()))
  {
    PyErr_SetString(PyExc_TypeError, error_msg.str().c_str());
    python::throw_error_already_set();
  }

  // Use the inspect module to extract the arg spec.
  // >>> import inspect
  auto inspect = python::import("inspect");
  // >>> args, varargs, keywords, defaults = inspect.getargspec(fn)
  auto arg_spec = inspect.attr("getargspec")(fn);
  python::object args = arg_spec[0];
  python::object varargs = arg_spec[1];
  python::object defaults = arg_spec[3];

  // Calculate the number of required arguments.
  auto args_count = args ? python::len(args) : 0;
  auto defaults_count = defaults ? python::len(defaults) : 0;

  // If the function is a bound method or a class method, then the
  // first argument (`self` or `cls`) will be implicitly provided.
  // >>> has_self = inspect.ismethod(fn) and fn.__self__ is not None
  if (static_cast<bool>(inspect.attr("ismethod")(fn))
      && fn.attr("__self__"))
  {
    --args_count;
  }

  // Require at least one argument.  The function should support
  // any of the following specs:
  //   >>> fn(a1)
  //   >>> fn(a1, a2=42)
  //   >>> fn(a1=42)
  //   >>> fn(*args)
  auto required_count = args_count - defaults_count;
  if (!(  (required_count == 1)                   // fn(a1), fn(a1, a2=42)
       || (args_count > 0 && required_count == 0) // fn(a1=42)
       || (varargs)                               // fn(*args)
    ))
 {
   PyErr_SetString(PyExc_ValueError, error_msg.str().c_str());
   python::throw_error_already_set();
 }
}

它的用法是:

void subscribe_py(boost::python::object callback)
{
  require_arity("callback", callback, 1); // callback(a1) is valid
  ...      
}

这是一个完整的例子demonstrating用法:

#include <boost/python.hpp>
#include <sstream>

/// @brief Given a Python object `fn` and an arity of `n`, requires
///        that the expression `fn(a_0, a_1, ..., a_2` to be valid.
///        Raise TypeError if `fn` is not callable and `ValueError`
///        if `fn` is callable, but has the wrong arity.
void require_arity(
  std::string name,
  boost::python::object fn,
  std::size_t arity)
{
  namespace python = boost::python;

  std::stringstream error_msg;
  error_msg << name << "() must take exactly " << arity << " arguments";

  // Throw if the callback is not callable.
  if (!PyCallable_Check(fn.ptr()))
  {
    PyErr_SetString(PyExc_TypeError, error_msg.str().c_str());
    python::throw_error_already_set();
  }

  // Use the inspect module to extract the arg spec.
  // >>> import inspect
  auto inspect = python::import("inspect");
  // >>> args, varargs, keywords, defaults = inspect.getargspec(fn)
  auto arg_spec = inspect.attr("getargspec")(fn);
  python::object args = arg_spec[0];
  python::object varargs = arg_spec[1];
  python::object defaults = arg_spec[3];

  // Calculate the number of required arguments.
  auto args_count = args ? python::len(args) : 0;
  auto defaults_count = defaults ? python::len(defaults) : 0;

  // If the function is a bound method or a class method, then the
  // first argument (`self` or `cls`) will be implicitly provided.
  // >>> has_self = inspect.ismethod(fn) and fn.__self__ is not None
  if (static_cast<bool>(inspect.attr("ismethod")(fn))
      && fn.attr("__self__"))
  {
    --args_count;
  }

  // Require at least one argument.  The function should support
  // any of the following specs:
  //   >>> fn(a1)
  //   >>> fn(a1, a2=42)
  //   >>> fn(a1=42)
  //   >>> fn(*args)
  auto required_count = args_count - defaults_count;
  if (!(  (required_count == 1)                   // fn(a1), fn(a1, a2=42)
       || (args_count > 0 && required_count == 0) // fn(a1=42)
       || (varargs)                               // fn(*args)
    ))
 {
   PyErr_SetString(PyExc_ValueError, error_msg.str().c_str());
   python::throw_error_already_set();
 }
}

void perform(
  boost::python::object callback,
  boost::python::object arg1)
{
  require_arity("callback", callback, 1);
  callback(arg1);
}

BOOST_PYTHON_MODULE(example)
{
  namespace python = boost::python;
  python::def("perform", &perform);
}

交互使用:

>>> import example
>>> def test(fn, a1, expect=None):
...     try:
...         example.perform(fn, a1)
...         assert(expect is None)
...     except Exception as e:
...         assert(isinstance(e, expect))
... 
>>> test(lambda x: 42, None)
>>> test(lambda x, y=2: 42, None)
>>> test(lambda x=1, y=2: 42, None)
>>> test(lambda *args: None, None)
>>> test(lambda: 42, None, ValueError)
>>> test(lambda x, y: 42, None, ValueError)
>>> 
>>> class Mock:
...     def method_no_arg(self): pass
...     def method_with_arg(self, x): pass
...     def method_default_arg(self, x=1): pass
...     @classmethod
...     def cls_no_arg(cls): pass
...     @classmethod
...     def cls_with_arg(cls, x): pass
...     @classmethod
...     def cls_with_default_arg(cls, x=1): pass
... 
>>> mock = Mock()
>>> test(Mock.method_no_arg, mock)
>>> test(mock.method_no_arg, mock, ValueError)
>>> test(Mock.method_with_arg, mock, ValueError)
>>> test(mock.method_with_arg, mock)
>>> test(Mock.method_default_arg, mock)
>>> test(mock.method_default_arg, mock)
>>> test(Mock.cls_no_arg, mock, ValueError)
>>> test(mock.cls_no_arg, mock, ValueError)
>>> test(Mock.cls_with_arg, mock)
>>> test(mock.cls_with_arg, mock)
>>> test(Mock.cls_with_default_arg, mock)
>>> test(mock.cls_with_default_arg, mock)

函数类型的严格检查可以说是非 Pythonic,并且由于可调用对象的各种类型(绑定方法、未绑定方法、类方法、函数等)而变得复杂。在应用严格类型检查之前,可能值得评估是否需要严格类型检查,或者替代检查(例如 Abstract Base Classes 是否足够)。例如,如果 callback 仿函数将在 Python 线程中调用,那么不执行类型检查可能是值得的,并允许在调用时引发 Python 异常。另一方面,如果将从非 Python 线程中调用 callback 仿函数,则启动函数中的类型检查可以允许在调用 Python 中抛出异常线程。