如何处理MATLAB中函数参数的名称/值对

72

我有一个函数,它接受可选参数作为名称/值对。

function example(varargin)
% Lots of set up stuff
vargs = varargin;
nargs = length(vargs);
names = vargs(1:2:nargs);
values = vargs(2:2:nargs);

validnames = {'foo', 'bar', 'baz'};    
for name = names
   validatestring(name{:}, validnames);
end

% Do something ...
foo = strmatch('foo', names);
disp(values(foo))
end

example('foo', 1:10, 'bar', 'qwerty')

看起来提取适当的值需要很多努力(而且仍然不够鲁棒,无法应对指定不良的输入)。是否有更好的处理这些名称/值对的方法?MATLAB中是否有任何辅助函数可用?


4
这是目前官方 MATLAB 解决方案的答案。 - Cris Luengo
15个回答

67

我更喜欢使用结构体来存储我的选项。这样可以轻松存储选项并定义它们,整个结构也变得更加紧凑。

function example(varargin)

%# define defaults at the beginning of the code so that you do not need to
%# scroll way down in case you want to change something or if the help is
%# incomplete
options = struct('firstparameter',1,'secondparameter',magic(3));

%# read the acceptable names
optionNames = fieldnames(options);

%# count arguments
nArgs = length(varargin);
if round(nArgs/2)~=nArgs/2
   error('EXAMPLE needs propertyName/propertyValue pairs')
end

for pair = reshape(varargin,2,[]) %# pair is {propName;propValue}
   inpName = lower(pair{1}); %# make case insensitive

   if any(strcmp(inpName,optionNames))
      %# overwrite options. If you want you can test for the right class here
      %# Also, if you find out that there is an option you keep getting wrong,
      %# you can use "if strcmp(inpName,'problemOption'),testMore,end"-statements
      options.(inpName) = pair{2};
   else
      error('%s is not a recognized parameter name',inpName)
   end
end

其实,那有点可爱...我可能得开始使用那个技巧。 - JudoWill
这个结构的想法看起来可以很好地整理事物。我可能会尝试将其抽象为一个通用的名称/值结构函数。 - Richie Cotton

45

1
@Matthew:这可能是处理事情的最佳内置方式。我对Jonas的选项结构的想法非常感兴趣,但这是一个紧随其后的选择。 - Richie Cotton
1
这是一个更好的答案,因为它符合MathWorks自己的风格和最佳实践。 - Peter M
但这对性能不利,参见 https://stackoverflow.com/questions/26634672/inputparser-vs-exist-var-vs-nargin-performance - Anh-Thi DINH

13

我可以花数小时讲述这个问题,但仍然没有对Matlab签名处理有一个好的整体观。但是这里有一些建议。

首先,对于验证输入类型,采取放任自流的方法。相信调用者。如果您真的想要强类型测试,则需要像Java这样的静态语言。试图在Matlab中到处执行类型安全性,并且您最终将会得到大量的LOC和执行时间,用于运行时类型测试和用户空间中的强制转换,从而牺牲了Matlab的很多功能和开发速度。我是通过吃亏才学会这个道理的。

对于API签名(旨在从其他函数而不是从命令行调用的函数),考虑使用单个Args参数而不是varargin。然后它可以在多个参数之间传递,而无需将其转换为逗号分隔的列表以进行varargin签名。像Jonas所说的那样,结构非常方便。结构和n-by-2 {name,value;...} cells之间存在着很好的同构性,您可以设置几个函数来在您的函数内部将它们转换成所需的格式。

function example(args)
%EXAMPLE
%
% Where args is a struct or {name,val;...} cell array

无论您使用inputParser还是自己编写名称/值解析器,都应该将其打包在一个单独的标准函数中,并从具有名称/值签名的函数顶部调用它。让它接受默认值列表,并将其放入方便编写的数据结构中,您的参数解析调用将类似于函数签名声明,这有助于可读性并避免复制和粘贴样板代码。
以下是解析调用的示例。
function out = my_example_function(varargin)
%MY_EXAMPLE_FUNCTION Example function 

% No type handling
args = parsemyargs(varargin, {
    'Stations'  {'ORD','SFO','LGA'}
    'Reading'   'Min Temp'
    'FromDate'  '1/1/2000'
    'ToDate'    today
    'Units'     'deg. C'
    });
fprintf('\nArgs:\n');
disp(args);

% With type handling
typed_args = parsemyargs(varargin, {
    'Stations'  {'ORD','SFO','LGA'}     'cellstr'
    'Reading'   'Min Temp'              []
    'FromDate'  '1/1/2000'              'datenum'
    'ToDate'    today                   'datenum'
    'Units'     'deg. C'                []
    });
fprintf('\nWith type handling:\n');
disp(typed_args);

% And now in your function body, you just reference stuff like
% args.Stations
% args.FromDate

这里有一个实现名称/值解析的函数。你可以空出它并用inputParser、自己的类型约定等替换它。我认为n-by-2单元格约定使源代码易读;考虑保留它。在接收代码中,结构体通常更方便处理,但使用表达式和字面量构造n-by-2单元格更加方便。(结构体需要在每行使用",..."来继续,同时防止单元格值扩展为非标量结构体。)
function out = parsemyargs(args, defaults)
%PARSEMYARGS Arg parser helper
%
% out = parsemyargs(Args, Defaults)
%
% Parses name/value argument pairs.
%
% Args is what you pass your varargin in to. It may be
%
% ArgTypes is a list of argument names, default values, and optionally
% argument types for the inputs. It is an n-by-1, n-by-2 or n-by-3 cell in one
% of these forms forms:
%   { Name; ... }
%   { Name, DefaultValue; ... }
%   { Name, DefaultValue, Type; ... }
% You may also pass a struct, which is converted to the first form, or a
% cell row vector containing name/value pairs as 
%   { Name,DefaultValue, Name,DefaultValue,... }
% Row vectors are only supported because it's unambiguous when the 2-d form
% has at most 3 columns. If there were more columns possible, I think you'd
% have to require the 2-d form because 4-element long vectors would be
% ambiguous as to whether they were on record, or two records with two
% columns omitted.
%
% Returns struct.
%
% This is slow - don't use name/value signatures functions that will called
% in tight loops.

args = structify(args);
defaults = parse_defaults(defaults);

% You could normalize case if you want to. I recommend you don't; it's a runtime cost
% and just one more potential source of inconsistency.
%[args,defaults] = normalize_case_somehow(args, defaults);

out = merge_args(args, defaults);

%%
function out = parse_defaults(x)
%PARSE_DEFAULTS Parse the default arg spec structure
%
% Returns n-by-3 cellrec in form {Name,DefaultValue,Type;...}.

if isstruct(x)
    if ~isscalar(x)
        error('struct defaults must be scalar');
    end
    x = [fieldnames(s) struct2cell(s)];
end
if ~iscell(x)
    error('invalid defaults');
end

% Allow {name,val, name,val,...} row vectors
% Does not work for the general case of >3 columns in the 2-d form!
if size(x,1) == 1 && size(x,2) > 3
    x = reshape(x, [numel(x)/2 2]);
end

% Fill in omitted columns
if size(x,2) < 2
    x(:,2) = {[]}; % Make everything default to value []
end
if size(x,2) < 3
    x(:,3) = {[]}; % No default type conversion
end

out = x;

%%
function out = structify(x)
%STRUCTIFY Convert a struct or name/value list or record list to struct

if isempty(x)
    out = struct;
elseif iscell(x)
    % Cells can be {name,val;...} or {name,val,...}
    if (size(x,1) == 1) && size(x,2) > 2
        % Reshape {name,val, name,val, ... } list to {name,val; ... }
        x = reshape(x, [2 numel(x)/2]);
    end
    if size(x,2) ~= 2
        error('Invalid args: cells must be n-by-2 {name,val;...} or vector {name,val,...} list');
    end

    % Convert {name,val, name,val, ...} list to struct
    if ~iscellstr(x(:,1))
        error('Invalid names in name/val argument list');
    end
    % Little trick for building structs from name/vals
    % This protects cellstr arguments from expanding into nonscalar structs
    x(:,2) = num2cell(x(:,2)); 
    x = x';
    x = x(:);
    out = struct(x{:});
elseif isstruct(x)
    if ~isscalar(x)
        error('struct args must be scalar');
    end
    out = x;
end

%%
function out = merge_args(args, defaults)

out = structify(defaults(:,[1 2]));
% Apply user arguments
% You could normalize case if you wanted, but I avoid it because it's a
% runtime cost and one more chance for inconsistency.
names = fieldnames(args);
for i = 1:numel(names)
    out.(names{i}) = args.(names{i});
end
% Check and convert types
for i = 1:size(defaults,1)
    [name,defaultVal,type] = defaults{i,:};
    if ~isempty(type)
        out.(name) = needa(type, out.(name), type);
    end
end

%%
function out = needa(type, value, name)
%NEEDA Check that a value is of a given type, and convert if needed
%
% out = needa(type, value)

% HACK to support common 'pseudotypes' that aren't real Matlab types
switch type
    case 'cellstr'
        isThatType = iscellstr(value);
    case 'datenum'
        isThatType = isnumeric(value);
    otherwise
        isThatType = isa(value, type);
end

if isThatType
    out = value;
else
    % Here you can auto-convert if you're feeling brave. Assumes that the
    % conversion constructor form of all type names works.
    % Unfortunately this ends up with bad results if you try converting
    % between string and number (you get Unicode encoding/decoding). Use
    % at your discretion.
    % If you don't want to try autoconverting, just throw an error instead,
    % with:
    % error('Argument %s must be a %s; got a %s', name, type, class(value));
    try
        out = feval(type, value);
    catch err
        error('Failed converting argument %s from %s to %s: %s',...
            name, class(value), type, err.message);
    end
end

很遗憾,在Matlab中,字符串和日期数不是一流类型。


1
@Andrew:这里有一些好建议。我同意所有这些样板代码都应该隐藏在一个函数中,你可能对输入检查不要过于控制也是正确的。 - Richie Cotton

13
MathWorks已经重新振作了这匹被打败的马,但我认为它已经足够好了,我们可以给它加上鞍子。它被称为函数参数验证(一个可以和应该在文档中搜索的短语),并且随着R2019b+版本一起发布。MathWorks也制作了相关视频。验证的工作方式类似于人们多年来想出的“技巧”。以下是一个示例:
function ret = example( inputDir, proj, options )
%EXAMPLE An example.
% Do it like this.
% See THEOTHEREXAMPLE.

    arguments
        inputDir (1, :) char
        proj (1, 1) projector
        options.foo char {mustBeMember(options.foo, {'bar' 'baz'})} = 'bar'
        options.Angle (1, 1) {double, integer} = 45
        options.Plot (1, 1) logical = false
    end

    % Code always follows 'arguments' block.
    ret = [];
    switch options.foo
        case 'bar'
            ret = sind(options.Angle);
        case 'baz'
            ret = cosd(options.Angle);
    end

    if options.Plot
        plot(proj.x, proj.y)
    end

end

这里是解包的部分: arguments块必须在任何代码之前(在帮助块后面也可以)。 在块内,函数定义中的每个参数都要按位置顺序给予处理。必需参数放在前面,后面是可选参数,最后是名称-值对。MathWorks还建议不再使用varargin关键字,但narginnargout仍然很有用。
  • 类需求可以是自定义类,例如projector
  • 必需参数可能没有默认值(即它们之所以不具有默认值是因为它们已知)。
  • 可选参数必须具有默认值(即它们之所以已知是因为它们具有默认值)。
    • 默认值必须能够通过相同的参数验证。 换句话说,zeros(3)的默认值不能作为一个应该是字符向量的参数的默认值。
  • 名称-值对存储在一个参数中,该参数在内部转换为结构体,我在这里称之为options(提示我们可以使用结构体传递关键字参数,如Python中的kwargs)。
  • 非常好的是,当您在函数调用中按Tab键时,名称-值参数现在将显示为参数提示。(如果完成提示对您感兴趣,我鼓励您还要查找MATLAB的functionSignatures.json功能。)

在这个例子中,inputDir 是一个必需的参数,因为它没有默认值。它也必须是一个1xN字符向量。然而需要注意的是,MATLAB会尝试转换所提供的参数以查看是否转换后的参数有效。例如,如果您将97:122传递给 inputDir ,那么它会成功,因为 inputDir == char(97:122)是一个字符向量(即 inputDir =='abcdefghijklmnopqrstuvwxyz')。

关于这个“灵活性”的说明: 当你传递字符时,字符串类型不会失败;当函数定义说它们应该是uint8时,以double形式传递的参数也不会失败等。它们将被转换。要避免这些类型转换,你需要深入研究。

接下来,'foo'指定一个名称-值对,其值只能是'bar''baz'

'Angle' 指定一个名称-值对,其值必须是标量 double 或整数,因此,例如,example(pwd, 'foo', 'baz', 'Angle', [30 70]) 不起作用,因为您传递了一个向量作为 Angle 参数。

MATLAB 有许多 mustBe... 验证函数(输入 mustBe 并按 Tab 键查看可用内容),并且很容易创建自己的验证函数。如果您创建自己的验证函数,则验证函数必须在输入不匹配时给出错误,而不像 uigetdir 返回 0 如果用户取消对话框。个人而言,我遵循 MATLAB 的惯例,并将我的验证函数称为 mustBe...,因此我有诸如 mustBeNatural 用于自然数,以及 mustBeFile 来确保我指定了实际存在的文件。

你明白了。在arguments块中有很多灵活性——我认为太多或太少了——但对于简单的函数来说,它快速且容易。您仍然可能依赖于inputParservalidateattributesassert等一个或多个工具来处理更大的验证复杂性,但我总是首先尝试将事物塞入arguments块中。如果它变得不美观,也许我会做一些断言等arguments块以外的操作。


6

就我个人而言,我使用的是一个定制函数,它源自于许多统计工具箱函数(如kmeans、pca、svmtrain、ttest2等)使用的私有方法。

作为一个内部实用程序函数,它在不同版本中经常更改和重命名。根据你的MATLAB版本,尝试查找以下文件之一:

%# old versions
which -all statgetargs
which -all internal.stats.getargs
which -all internal.stats.parseArgs

%# current one, as of R2014a
which -all statslib.internal.parseArgs

与任何未记录的函数一样,没有保证并且它可能在后续版本的MATLAB中被删除而没有任何通知... 无论如何,我相信有人将其旧版本发布为getargs在文件交换中。
该函数使用一组有效的参数名称及其默认值作为名称/值对处理参数。它将解析后的参数作为单独的输出变量返回。默认情况下,无法识别的名称/值对会引发错误,但我们也可以在额外的输出中静默地捕获它们。以下是函数描述:

$MATLABROOT\toolbox\stats\stats\+internal\+stats\parseArgs.m

function varargout = parseArgs(pnames, dflts, varargin)
%
% [A,B,...] = parseArgs(PNAMES, DFLTS, 'NAME1',VAL1, 'NAME2',VAL2, ...)
%   PNAMES   : cell array of N valid parameter names.
%   DFLTS    : cell array of N default values for these parameters.
%   varargin : Remaining arguments as name/value pairs to be parsed.
%   [A,B,...]: N outputs assigned in the same order as the names in PNAMES.
%
% [A,B,...,SETFLAG] = parseArgs(...)
%   SETFLAG  : structure of N fields for each parameter, indicates whether
%              the value was parsed from input, or taken from the defaults.
%
% [A,B,...,SETFLAG,EXTRA] = parseArgs(...)
%   EXTRA    : cell array containing name/value parameters pairs not
%              specified in PNAMES.

例子:

function my_plot(x, varargin)
    %# valid parameters, and their default values
    pnames = {'Color', 'LineWidth', 'LineStyle', 'Title'};
    dflts  = {    'r',           2,        '--',      []};

    %# parse function arguments
    [clr,lw,ls,txt] = internal.stats.parseArgs(pnames, dflts, varargin{:});

    %# use the processed values: clr, lw, ls, txt
    %# corresponding to the specified parameters
    %# ...
end

现在这个例子函数可以通过以下任何一种方式进行调用:
>> my_plot(data)                                %# use the defaults
>> my_plot(data, 'linestyle','-', 'Color','b')  %# any order, case insensitive
>> my_plot(data, 'Col',[0.5 0.5 0.5])           %# partial name match

以下是一些无效的调用和抛出的错误:

%# unrecognized parameter
>> my_plot(x, 'width',0)
Error using [...]
Invalid parameter name: width.

%# bad parameter
>> my_plot(x, 1,2)
Error using [...]
Parameter name must be text.

%# wrong number of arguments
>> my_plot(x, 'invalid')
Error using [...]
Wrong number of arguments.

%# ambiguous partial match
>> my_plot(x, 'line','-')
Error using [...]
Ambiguous parameter name: line.

inputParser:

众所周知,官方推荐的解析函数输入方法是使用inputParser类。它支持各种方案,例如指定必需输入、可选位置参数和名称/值参数。它还允许对输入进行验证(例如检查参数的类/类型、大小/形状)。


@Amro:非常有趣,实际上与我刚刚自己编写的解决方案非常相似。https://dev59.com/5nE85IYBdhLWcg3wbTD2#2780043 - Richie Cotton
@OlegKomarov:感谢您的编辑。我更新了帖子以反映最新MATLAB版本中的更改,并展示了该函数的实际操作。 - Amro

5

阅读Loren发布的信息,了解此问题。不要忘记阅读评论部分——您会发现有很多不同的方法来解决这个问题。它们都有效,因此选择首选方法实际上是个人口味和可维护性的问题。


3

我更喜欢像这样的自制样板代码:

function TestExample(req1, req2, varargin)
for i = 1:2:length(varargin)
    if strcmpi(varargin{i}, 'alphabet')
        ALPHA = varargin{i+1};

    elseif strcmpi(varargin{i}, 'cutoff')
        CUTOFF = varargin{i+1};
        %we need to remove these so seqlogo doesn't get confused
        rm_inds = [rm_inds i, i+1]; %#ok<*AGROW>

    elseif strcmpi(varargin{i}, 'colors')
        colors = varargin{i+1};
        rm_inds = [rm_inds i, i+1]; 
    elseif strcmpi(varargin{i}, 'axes_handle')
        handle = varargin{i+1};
        rm_inds = [rm_inds i, i+1]; 
    elseif strcmpi(varargin{i}, 'top-n')
        TOPN = varargin{i+1};
        rm_inds = [rm_inds i, i+1];
    elseif strcmpi(varargin{i}, 'inds')
        npos = varargin{i+1};
        rm_inds = [rm_inds i, i+1];
    elseif strcmpi(varargin{i}, 'letterfile')
        LETTERFILE = varargin{i+1};
        rm_inds = [rm_inds i, i+1];
    elseif strcmpi(varargin{i}, 'letterstruct')
        lo = varargin{i+1};
        rm_inds = [rm_inds i, i+1];
    end
end

这样我就可以模拟“选项”和“值”对,这几乎与大多数Matlab函数接受其参数的方式完全相同。希望这能有所帮助。 祝好, 威尔

2
@JudoWill:谢谢。使用switch语句会更加简洁,而不是使用大量的elseif子句,同时添加一个else/otherwise子句可以很好地捕获无法识别的输入。 - Richie Cotton
+1,对于简单情况,我也绝对更喜欢这样做。不过使用 switch 也可以。 - zelanix

1
如果您正在使用MATLAB 2019b或更高版本,处理函数中的名称-值对的最佳方法是使用“声明函数参数验证”。
function result = myFunction(NameValueArgs)
arguments
    NameValueArgs.Name1
    NameValueArgs.Name2
end

% Function code
result = NameValueArgs.Name1 * NameValueArgs.Name2;

end

请参见: https://www.mathworks.com/help/matlab/ref/arguments.html


新的参数块已经在这个答案中讨论过了。 - Cris Luengo

1

这是我正在试验的解决方案,基于Jonas的想法。

function argStruct = NameValuePairToStruct(defaults, varargin)
%NAMEVALUEPAIRTOSTRUCT Converts name/value pairs to a struct.
% 
% ARGSTRUCT = NAMEVALUEPAIRTOSTRUCT(DEFAULTS, VARARGIN) converts
% name/value pairs to a struct, with defaults.  The function expects an
% even number of arguments to VARARGIN, alternating NAME then VALUE.
% (Each NAME should be a valid variable name.)
% 
% Examples: 
% 
% No defaults
% NameValuePairToStruct(struct, ...
%    'foo', 123, ...
%    'bar', 'qwerty', ...
%    'baz', magic(3))
% 
% With defaults
% NameValuePairToStruct( ...
%    struct('bar', 'dvorak', 'quux', eye(3)), ...
%    'foo', 123, ...
%    'bar', 'qwerty', ...
%    'baz', magic(3))
% 
% See also: inputParser

nArgs = length(varargin);
if rem(nArgs, 2) ~= 0
   error('NameValuePairToStruct:NotNameValuePairs', ...
      'Inputs were not name/value pairs');
end

argStruct = defaults;
for i = 1:2:nArgs
   name = varargin{i};
   if ~isvarname(name)
      error('NameValuePairToStruct:InvalidName', ...
         'A variable name was not valid');
   end
   argStruct = setfield(argStruct, name, varargin{i + 1});  %#ok<SFLD>
end

end

谢谢,你的解决方案也很有趣,但它与Jonas的方案有所不同:你的方案接受任何参数名称,并仅检查其是否为语法上有效的变量名,而Jonas的代码确实将参数名称限制为选项中指定的名称。没有更好的代码,只是我认为澄清这种差异很重要。 - gaborous

1

我使用process_options.m已经有很长时间了。它非常稳定,易于使用,并已包含在各种 matlab 框架中。虽然不知道性能如何-可能有更快的实现。

我最喜欢的process_options功能是返回值unused_args,它可用于将输入参数分组为例如子进程的参数组。

你还可以轻松定义默认值。

最重要的是:使用process_options.m通常会导致可读和可维护的选项定义。

示例代码:

function y = func(x, y, varargin)

    [u, v] = process_options(varargin,
                             'u', 0,
                             'v', 1);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接