2016-03-21 49 views

回答

1

Boost.Python的不提供更高級別的類型,幫助進行反省。但是,可以使用Python C-API的PyCallable_Check()來檢查Python對象是否可調用,然後使用Python自檢模塊(例如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) 

函數類型的嚴格的檢查可以認爲作爲非Python化和可能會變得複雜,由於各種類型的可調用(結合方法中,未結合的方法,類方法,功能等的)。在應用嚴格類型檢查之前,可能需要評估是否需要進行嚴格類型檢查,或者是否需要其他檢查(如Abstract Base Classes)就足夠了。例如,如果將在Python線程中調用callback仿函數,那麼可能不值得執行類型檢查,並允許在調用時引發Python異常。另一方面,如果將從非Python線程中調用callback仿函數,那麼在啓動函數內鍵入檢查可以允許在調用Python線程中引發異常。