使用 kwargs 包装一个接受可选参数结构的函数

Wrap a function that takes a struct of optional arguments using kwargs

在 C 语言中,经常会看到需要大量输入的函数,其中 many/most 是可选的,将这些输入组合在一个结构中以使开发人员的界面更清晰。 (即使你应该能够依赖编译器接受 at least 127 arguments to a function 实际上没有人愿意写那么多,特别是因为 C 没有重载或默认函数参数支持)。作为一个假设的例子,我们可以考虑以下 struct/function 对 (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 命令行选项也无济于事,因为该函数只有一个实际参数。

通常在 Python 中修改函数参数和 return 值的方法是使用装饰器。作为起点,我草拟了以下装饰器,它解决了问题:

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. 它也不会破坏直接使用单个结构参数调用函数的语法
  2. 它可以支持具有强制位置参数的函数一个充满可选参数的结构作为最后一个参数。 (虽然它目前不能对强制性非结构参数使用 kwargs 语法)

然后问题就变成了简单地将装饰器应用于 SWIG 生成的 Python 代码中的正确函数。我的计划是尽可能将它包装在尽可能简单的宏中,因为该模式在我包装很多的库中重复出现。事实证明这比我预期的要难。 (而且我显然是 not the only one)我最初尝试过:

  1. %feature("shadow") - 我很确定这会起作用,而且它确实适用于 C++ 成员函数,但由于某些我没有弄清楚的原因,它不适用于全局范围内的自由函数.
  2. %feature("autodoc")%feature("docstring") - 乐观地我希望能够稍微滥用它们,但没有快乐
  3. %pythoncode 就在 SWIG 看到 C 端的函数声明之前。生成正确的代码,但不幸的是,SWIG 立即隐藏了我们通过添加 ComplexFun = _test.ComplexFun 修饰的函数。很长一段时间都找不到解决方法。
  4. 使用%rename隐藏我们调用的真实函数,然后围绕真实函数编写一个包装器,该函数也被装饰了。这行得通,但感觉真的很不优雅,因为它基本上让编写上面的装饰器变得毫无意义,而不是仅仅将它写在新的包装器中。

我终于找到了一个更简洁的技巧来修饰自由函数。通过在函数上使用 %pythonprepend 我可以插入一些东西(任何东西,评论,pass,空字符串等)足以抑制阻止#3 工作的额外代码。

我遇到的最后一个问题是,要使它全部作为单个宏工作,并使 %pythoncode 指令的位置正确(也仍然允许 %includeing 包含的头文件声明)我必须在 %include 之前调用宏。这需要添加一个额外的 %ignore 来忽略函数 if/when 它在实际头文件中第二次出现。然而,它引入的另一个问题是我们现在将函数包装在结构之前,因此在 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. 如果需要,调整宏以支持强制位置参数。

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 特定的,但似乎工作正常并且非常非常简单.

希望对您有所帮助。