How to deal with name/value pairs of function arguments in MATLAB

后端 未结 14 1023
忘掉有多难
忘掉有多难 2020-11-28 19:28

I have a function that takes optional arguments as name/value pairs.

function example(varargin)
% Lots of set up stuff
vargs = varargin;
nargs = length(vargs         


        
相关标签:
14条回答
  • 2020-11-28 19:48

    I have made a function based on Jonas and Richie Cotton. It implements both functionalities (flexible arguments or restricted, meaning that only variables existing in the defaults are allowed), and a few other things like syntactic sugar and sanity checks.

    function argStruct = getnargs(varargin, defaults, restrict_flag)
    %GETNARGS Converts name/value pairs to a struct (this allows to process named optional arguments).
    % 
    % ARGSTRUCT = GETNARGS(VARARGIN, DEFAULTS, restrict_flag) converts
    % name/value pairs to a struct, with defaults.  The function expects an
    % even number of arguments in VARARGIN, alternating NAME then VALUE.
    % (Each NAME should be a valid variable name and is case sensitive.)
    % Also VARARGIN should be a cell, and defaults should be a struct().
    % Optionally: you can set restrict_flag to true if you want that only arguments names specified in defaults be allowed. Also, if restrict_flag = 2, arguments that aren't in the defaults will just be ignored.
    % After calling this function, you can access your arguments using: argstruct.your_argument_name
    %
    % Examples: 
    %
    % No defaults
    % getnargs( {'foo', 123, 'bar', 'qwerty'} )
    %
    % With defaults
    % getnargs( {'foo', 123, 'bar', 'qwerty'} , ...
    %               struct('foo', 987, 'bar', magic(3)) )
    %
    % See also: inputParser
    %
    % Authors: Jonas, Richie Cotton and LRQ3000
    %
    
        % Extract the arguments if it's inside a sub-struct (happens on Octave), because anyway it's impossible that the number of argument be 1 (you need at least a couple, thus two)
        if (numel(varargin) == 1)
            varargin = varargin{:};
        end
    
        % Sanity check: we need a multiple of couples, if we get an odd number of arguments then that's wrong (probably missing a value somewhere)
        nArgs = length(varargin);
        if rem(nArgs, 2) ~= 0
            error('NameValuePairToStruct:NotNameValuePairs', ...
                'Inputs were not name/value pairs');
        end
    
        % Sanity check: if defaults is not supplied, it's by default an empty struct
        if ~exist('defaults', 'var')
            defaults = struct;
        end
        if ~exist('restrict_flag', 'var')
            restrict_flag = false;
        end
    
        % Syntactic sugar: if defaults is also a cell instead of a struct, we convert it on-the-fly
        if iscell(defaults)
            defaults = struct(defaults{:});
        end
    
        optionNames = fieldnames(defaults); % extract all default arguments names (useful for restrict_flag)
    
        argStruct = defaults; % copy over the defaults: by default, all arguments will have the default value.After we will simply overwrite the defaults with the user specified values.
        for i = 1:2:nArgs % iterate over couples of argument/value
            varname = varargin{i}; % make case insensitive
            % check that the supplied name is a valid variable identifier (it does not check if the variable is allowed/declared in defaults, just that it's a possible variable name!)
            if ~isvarname(varname)
              error('NameValuePairToStruct:InvalidName', ...
                 'A variable name was not valid: %s position %i', varname, i);
            % if options are restricted, check that the argument's name exists in the supplied defaults, else we throw an error. With this we can allow only a restricted range of arguments by specifying in the defaults.
            elseif restrict_flag && ~isempty(defaults) && ~any(strmatch(varname, optionNames))
                if restrict_flag ~= 2 % restrict_flag = 2 means that we just ignore this argument, else we show an error
                    error('%s is not a recognized argument name', varname);
                end
            % else alright, we replace the default value for this argument with the user supplied one (or we create the variable if it wasn't in the defaults and there's no restrict_flag)
            else
                argStruct = setfield(argStruct, varname, varargin{i + 1});  %#ok<SFLD>
            end
        end
    
    end
    

    Also available as a Gist.

    And for those interested in having real named arguments (with a syntax similar to Python, eg: myfunction(a=1, b='qwerty'), use InputParser (only for Matlab, Octave users will have to wait until v4.2 at least or you can try a wrapper called InputParser2).

    Also as a bonus, if you don't want to always have to type argstruct.yourvar but directly use yourvar, you can use the following snippet by Jason S:

    function varspull(s)
    % Import variables in a structures into the local namespace/workspace
    % eg: s = struct('foo', 1, 'bar', 'qwerty'); varspull(s); disp(foo); disp(bar);
    % Will print: 1 and qwerty
    % 
    %
    % Author: Jason S
    %
        for n = fieldnames(s)'
            name = n{1};
            value = s.(name);
            assignin('caller',name,value);
        end
    end
    
    0 讨论(0)
  • 2020-11-28 19:49

    Here's the solution I'm trialling, based upon Jonas' idea.

    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
    
    0 讨论(0)
  • 2020-11-28 19:50

    Inspired by Jonas' answer, but more compact:

    function example(varargin)
      defaults = struct('A',1, 'B',magic(3));  %define default values
    
      params = struct(varargin{:});
      for f = fieldnames(defaults)',
        if ~isfield(params, f{1}),
          params.(f{1}) = defaults.(f{1});
        end
      end
    
      %now just access them as params.A, params.B
    
    0 讨论(0)
  • 2020-11-28 19:52

    Personally I use a custom function derived from a private method used by many Statistics Toolbox functions (like kmeans, pca, svmtrain, ttest2, ...)

    Being an internal utility function, it changed and was renamed many times over the releases. Depending on your MATLAB version, try looking for one of the following files:

    %# 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
    

    As with any undocumented function, there are no guarantees and it could be removed from MATLAB in subsequent releases without any notice... Anyways, I believe someone posted an old version of it as getargs on the File Exchange..

    The function processes parameters as name/value pairs, using a set of valid parameter names along with their default values. It returns the parsed parameters as separate output variables. By default, unrecognized name/value pairs raise an error, but we could also silently capture them in an extra output. Here is the function description:

    $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.
    

    Example:

    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
    

    Now this example function could be called as any of the following ways:

    >> 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
    

    Here are some invalid calls and the errors thrown:

    %# 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:

    As others have mentioned, the officially recommended approach to parsing functions inputs is to use inputParser class. It supports various schemes such as specifying required inputs, optional positional arguments, and name/value parameters. It also allows to perform validation on the inputs (such as checking the class/type and the size/shape of the arguments)

    0 讨论(0)
  • 2020-11-28 19:52

    MathWorks has revived this beaten horse, but with very useful functionality that answers this need, directly. It's called Function Argument Validation (a phrase one can and should search for in the documentation) and comes with release R2019b+. MathWorks created a video about it, also. Validation works much like the "tricks" people have come up with over the years. Here is an example:

    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
    end
    

    Here's the unpacking:

    The arguments block must come before any code (OK after help block) and must follow the positional order defined in the function definition, and I believe every argument requires a mention. Required arguments go first, followed by optional arguments, followed by name-value pairs. MathWorks also recommends to no longer use the varargin keyword, but nargin and nargout are still useful.

    • Class requirements can be custom classes, such as projector, in this case.
    • Required arguments may not have a default value; are known because they don't have a default value.
    • Optional arguments must have a default value; are known because they have a default value.
    • Default values must be able to pass the same argument validation. In other words, a default value of zeros(3) won't work as a default value for an argument that's supposed to be a character vector.
    • Name-value pairs are stored in the options struct (hinting at us that we can use structs to pass keyword arguments, like kwargs in Python).
    • Very nicely, name-value arguments will now show up as argument hints when you hit tab in a function call.

    So in the example, inputDir is a required argument because it's given no default value. It also must be a 1xN character vector. As if to contradict that statement, note that MATLAB will try to convert the supplied argument to see if the converted argument passes. If you pass 12 as inputDir, for example, it will pass and inputDir == '12'. Conversely, zeros(3) won't work, but on account of its being an array. And forget about making strings fail when you specify characters, and vice versa. MATLAB knows what you want and converts them. You'd need to dig deeper to circumvent this "flexibility."

    'foo' specifies a name-value pair whose value may be only 'bar' or 'baz'.

    MATLAB has a number of mustBe... validation functions (start typing mustBe and hit tab to see what's available), and it's easy enough to create your own. If you create your own, the validation function must provide an error if the input doesn't match, unlike, say, uigetdir, which returns a 0 if the user cancels the dialog. Personally, I follow MATLAB's convention and call my validation functions mustBe..., so I have functions like mustBeNatural for natural numbers, and mustBeFile to ensure I passed a file that actually exists.

    'Angle' specifies a name-value pair whose value must be a scalar double or integer, so, for example, example(pwd, 'foo', 'baz', 'Angle' [30 70]) won't work since you passed a vector.

    You get the idea. There is a lot of flexibility with the arguments block -- too much, and too little, I think -- but for simple functions, it's fast and easy. You still might rely on one or more of inputParser, validateattributes, assert, and so on for addressing greater validation complexity, but I always try to stuff things into an arguments block, first. If it's becoming unsightly, I'll move to the other options.

    0 讨论(0)
  • 2020-11-28 19:53
    function argtest(varargin)
    
    a = 1;
    
    for ii=1:length(varargin)/2
        [~] = evalc([varargin{2*ii-1} '=''' num2str(varargin{2*ii}) '''']);
    end;
    
    disp(a);
    who
    

    This does of course not check for correct assignments, but it's simple and any useless variable will be ignored anyway. It also only works for numerics, strings and arrays, but not for matrices, cells or structures.

    0 讨论(0)
提交回复
热议问题