使用kwargs包装一个带有可选参数结构的函数

时间:2016-09-11 13:32:49

标签: python c swig

在C中,看到一个需要大量输入的函数并不罕见,其中许多/大多数都是可选组,这些在结构中可以使开发人员的界面更加清晰。 (即使您应该能够依赖编译器接受at least 127 arguments to a function,但实际上没有人想要编写那么多,特别是当C没有重载或默认函数参数支持时。作为一个假设的例子,我们可以考虑以下结构/函数对(test.h)来说明问题:

#include <stdbool.h>

typedef struct {
  const char *name;
  void *stuff;
  int max_size;
  char flags;
  _Bool swizzle;
  double frobination;
  //...
} ComplexArgs;

void ComplexFun(const ComplexArgs *arg) {}

使用SWIG进行包装时,我们可以使用以下方法快速完成工作:

%module test

%{
#include "test.h"
%}

typedef bool _Bool;

%include "test.h"

这有效,我们可以按如下方式使用它:

import test

args=test.ComplexArgs()
args.flags=100;
args.swizzle=True
test.ComplexFun(args)

但这并不完全是Pythonic。 Python开发人员更习惯于看到kwargs用于支持这种调用:

import test

# Not legal in the interface currently:
test.ComplexFun(flags=100, swizzle=True)

我们怎样才能做到这一点? SWIG -keyword命令行选项也没有帮助,因为该函数只有一个实际参数。

2 个答案:

答案 0 :(得分:5)

通常在Python中,修改函数参数和返回值的方法是使用装饰器。作为一个起点,我勾勒出以下装饰器,它解决了这个问题:

def StructArgs(ty):
  def wrap(f):
    def _wrapper(*args, **kwargs):
      arg=(ty(),) if len(kwargs) else tuple()
      for it in kwargs.iteritems():
        setattr(arg[0], *it)
      return f(*(args+arg))
    return _wrapper
  return wrap

当这样编写时,它有一些更整洁的属性:

  1. 它也不会破坏使用单个struct参数
  2. 直接调用函数的语法
  3. 它可以支持具有强制位置参数的函数一个充满可选参数的结构作为最后一个参数。 (虽然目前不能将kwargs语法用于强制性非结构参数)
  4. 然后问题变成简单地将该装饰器应用于SWIG生成的Python代码中的正确函数。我的计划是用尽可能简单的宏来包装它,因为模式在库中重复我正在包装很多。事实证明这比我预期的要难。 (我显然not the only one)我最初尝试过:

    1. %feature("shadow") - 我很确定它会起作用,事实上它确实适用于C ++成员函数,但由于某些我无法理解的原因,它不适用于全局范围内的自由函数。 / LI>
    2. %feature("autodoc")%feature("docstring") - 乐观地说,我希望能够轻微地滥用它们,但没有快乐
    3. 在SWIG看到C侧的函数声明之前,
    4. %pythoncode。生成正确的代码,但不幸的是,SWIG通过添加ComplexFun = _test.ComplexFun立即隐藏了我们装饰的功能。很长一段时间没能找到解决方法。
    5. 使用%rename隐藏我们调用的真实函数,然后围绕实际函数编写一个包装器,该函数也被装饰。这很有效,但感觉非常不优雅,因为它基本上使得编写上面的装饰器毫无意义,而不是仅仅在新包装器中编写它。
    6. 最后,我发现了一个更简洁的技巧来装饰自由功能。通过在函数上使用%pythonprepend,我可以插入一些内容(任何内容,注释,pass,空字符串等),这足以抑制阻止#3工作的额外代码。

      我遇到的最后一个问题是,使它全部作为单个宏工作并获得%pythoncode指令的位置(还允许%include包含声明的头文件我必须在%include之前调用宏。这需要添加一个额外的%ignore来忽略该函数if / when它在实际头文件中第二次出现时。然而,它引入的另一个问题是我们现在将函数包装在struct之前,因此在Python模块中我们需要装饰器填充的结构类型在我们调用装饰器时尚不可知。通过将字符串传递给装饰器而不是类型并稍后在module globals()中查找它,可以很容易地解决这个问题。

      所以说,包装它的完整的工作界面变为:

      %module test
      
      %pythoncode %{
      def StructArgs(type_name):
        def wrap(f):
          def _wrapper(*args, **kwargs):
            ty=globals()[type_name]
            arg=(ty(),) if kwargs else tuple()
            for it in kwargs.iteritems():
              setattr(arg[0], *it)
            return f(*(args+arg))
          return _wrapper
        return wrap
      %}
      
      %define %StructArgs(func, ret, type)
      %pythoncode %{ @StructArgs(#type) %} // *very* position sensitive
      %pythonprepend func %{ %} // Hack to workaround problem with #3
      ret func(const type*);
      %ignore func;
      %enddef
      
      %{
      #include "test.h"
      %}
      
      typedef bool _Bool;
      
      %StructArgs(ComplexFun, void, ComplexArgs)
      
      %include "test.h"
      

      这足以使用以下Python代码:

      import test
      
      args=test.ComplexArgs()
      args.flags=100;
      args.swizzle=True
      test.ComplexFun(args)
      
      test.ComplexFun(flags=100, swizzle=True)
      

      在真正使用它之前你可能想要做的事情:

      1. 使用这个装饰器和kwargs当前写的很难得到任何类型的TypeError。可能你的C函数有一种指示无效输入组合的方法。将这些转换为Python用户的TypeError异常。
      2. 根据需要调整宏以支持强制位置参数。

答案 1 :(得分:1)

Flexo的装饰令人印象深刻。我自己遇到了这个问题,不愿提出我的解决方案,只是它有一个节省的余地:简单。另外,我的解决方案是针对C ++的,但您也许可以针对C对其进行修改。

我这样声明我的OptArgs结构:

struct OptArgs {
  int oa_a {2},
  double oa_b {22.0/7.0};
  OptArgs& a(int n)    { a = n; return *this; }
  OptArgs& b(double n) { b = n; return *this; }
}

例如,打算使用MyClass(required_arg, OptArgs().b(2.71))从C ++调用构造函数。

现在,我在.i文件中使用以下内容将SWIG生成的构造函数移开并解压缩关键字参数:

%include "myclass.h"
%extend MyClass {
    %pythoncode %{
        SWIG__init__ = __init__
        def __init__(self, *args, **kwargs):
            if len(kwargs) != 0:
                optargs = OptArgs()
                for arg in kwargs:
                    set_method = getattr(optargs, arg, None)
                    # Deliberately let an error happen here if the argument is bogus
                    set_method(kwargs[arg])
                args += (optargs,)
            MyClass.SWIG__init__(self, *args)
    %}
};

这不是完美的:它依赖于由SWIG生成的__init__声明后发生的扩展,它是python特定的,但似乎可以正常运行,并且非常非常简单。

我希望这会有所帮助。