Come trattare le coppie nome / valore degli argomenti delle funzioni in MATLAB

Ho una funzione che accetta argomenti opzionali come coppie nome / valore.

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

Sembra che ci sia un grande sforzo per estrarre i valori appropriati (e non è ancora particolarmente robusto con input mal specificati). C’è un modo migliore di gestire queste coppie nome / valore? Ci sono alcune funzioni di supporto fornite da MATLAB per l’assistenza?

Preferisco usare le strutture per le mie opzioni. Questo ti dà un modo semplice per memorizzare le opzioni e un modo semplice per definirle. Inoltre, il tutto diventa piuttosto compatto.

 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 

InputParser aiuta con questo. Vedere Input della funzione di analisi per ulteriori informazioni.

Potrei darci da fare per ore a riguardo, ma ancora non ho una buona visione della gestazione generale della firma di Matlab. Ma ecco un paio di consigli.

In primo luogo, adottare un approccio laissez faire per convalidare i tipi di input. Fidati del chiamante. Se vuoi davvero test di tipo forte, vuoi un linguaggio statico come Java. Prova a rafforzare la sicurezza del tipo ovunque in Matlab, e finirai con una buona parte del tuo LOC e il tempo di esecuzione dedicato ai test di tipo run time e alla coercizione in userland, che sfrutta molta della potenza e della velocità di sviluppo di Matlab . L’ho imparato nel modo più duro.

Per le firme API (funzioni destinate a essere richiamate da altre funzioni, anziché dalle righe di comando), considerare l’utilizzo di un argomento Args singolo anziché varargin. Quindi può essere passato tra più argomenti senza doverlo convertire in e da un elenco separato da virgole per le firme varargin. Le strutture, come dice Jonas, sono molto convenienti. C’è anche un bel isomorfismo tra le strutture e le celle n-by-2 {nome, valore; …}, e puoi impostare una coppia di funzioni per convertirle tra le tue funzioni e quella che preferisce usare internamente.

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

Sia che si usi inputParser sia che si carichi il proprio nome / val parser come questi altri esempi, impacchettalo in una funzione standard separata che chiamerai dalla parte superiore delle tue funzioni con firme nome / val. Accettate la lista dei valori di default in una struttura dati che è conveniente scrivere, e le vostre chiamate di analisi compariranno in modo simile alle dichiarazioni delle firme delle funzioni, il che aiuta la leggibilità ed evita il codice di copia-e-incolla.

Ecco come potrebbero apparire le chiamate di parsing.

 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 

Ed ecco una funzione per implementare il nome / val parsing in questo modo. Potresti svuotarlo e sostituirlo con inputParser, le tue convenzioni di tipo, ecc. Penso che la convenzione delle celle n-by-2 sia un codice sorgente ben leggibile; considera di tenerlo. Le strutture sono in genere più comode da gestire nel codice ricevente, ma le celle n-by-2 sono più convenienti da build usando espressioni e valori letterali. (Gli struct richiedono la continuazione “, …” su ogni riga e proteggono i valori delle celle dall’espansione alle strutture non scalari).

 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 

È così sfortunato che le stringhe e i datenum non sono tipi di prima class in Matlab.

Personalmente utilizzo una funzione personalizzata derivata da un metodo privato utilizzato da molte funzioni di Toolbox di statistiche (come kmea, pca, svmtrain, ttest2, …)

Essendo una funzione di utilità interna, è stata modificata e rinominata più volte rispetto alle versioni. A seconda della versione di MATLAB, prova a cercare uno dei seguenti file:

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

Come con qualsiasi funzione non documentata, non ci sono garanzie e potrebbe essere rimosso da MATLAB nelle versioni successive senza alcun preavviso … Comunque, credo che qualcuno abbia pubblicato una versione precedente di esso come getargs sullo scambio di file ..

La funzione elabora i parametri come coppie nome / valore, utilizzando un insieme di nomi di parametri validi insieme ai loro valori predefiniti. Restituisce i parametri analizzati come variabili di output separate. Per impostazione predefinita, le coppie nome / valore non riconosciute generano un errore, ma possiamo anche catturarle silenziosamente in un output extra. Ecco la descrizione della funzione:

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

Esempio:

 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 

Ora questa funzione di esempio potrebbe essere chiamata come uno dei seguenti modi:

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

Ecco alcune chiamate non valide e gli errori lanciati:

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

Come altri hanno già menzionato, l’approccio ufficialmente raccomandato per l’input delle funzioni di parsing è l’uso inputParser class inputParser . Supporta vari schemi come la specifica di input richiesti, argomenti posizionali opzionali e parametri nome / valore. Permette anche di eseguire la validazione sugli input (come il controllo della class / tipo e la dimensione / forma degli argomenti)

Leggi il post informativo di Loren su questo argomento. Non dimenticare di leggere la sezione dei commenti … – Vedrai che ci sono diversi approcci a questo argomento. Funzionano tutti, quindi selezionare un metodo preferito è davvero una questione di gusto personale e manutenibilità.

Sono un fan più grande del codice di targa della caldaia cresciuto in casa come questo:

 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 

In questo modo posso simulare l”opzione’, coppia di valori che è quasi identica a come la maggior parte delle funzioni Matlab accetta i loro argomenti.

Spero possa aiutare,

Volere

Ecco la soluzione che sto sperimentando, basata sull’idea di 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 end end 

Ispirato alla risposta di Jonas, ma più compatto:

 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 

Da anni sto usando process_options.m . È stabile, facile da usare ed è stato incluso in vari framework matlab. Non so nulla sulle prestazioni però – potrebbe essere che ci siano implementazioni più veloci.

La caratteristica che mi piace di più con process_options è il valore restituito unused_args , che può essere utilizzato per dividere gli argomenti di input in gruppi di argomenti per, ad esempio, i sottoprocessi.

E puoi facilmente definire i valori predefiniti.

process_options.m più importante: l’utilizzo di process_options.m solito porta a definizioni di opzioni leggibili e process_options.m .

Codice di esempio:

 function y = func(x, y, varargin) [u, v] = process_options(varargin, 'u', 0, 'v', 1); 
 function argtest(varargin) a = 1; for ii=1:length(varargin)/2 [~] = evalc([varargin{2*ii-1} '=''' num2str(varargin{2*ii}) '''']); end; disp(a); who 

Questo naturalmente non controlla i compiti corretti, ma è semplice e qualsiasi variabile inutile sarà ignorata comunque. Funziona anche solo per numeri, stringhe e matrici, ma non per matrici, celle o strutture.

Ho finito per scrivere questo oggi, e poi ho trovato queste citazioni. Il mio usa struct’s e struct ‘overlay’ per le opzioni. Rispecchia essenzialmente la funzionalità di setstructfields (), ad eccezione del fatto che non è ansible aggiungere nuovi parametri. Ha anche un’opzione per la ricorsione, mentre setstructfields () lo fa automaticamente. Può contenere una matrice di celle di valori accoppiati chiamando struct (args {:}).

 % Overlay default fields with input fields % Good for option management % Arguments % $opts - Default options % $optsIn - Input options % Can be struct(), cell of {name, value, ...}, or empty [] % $recurseStructs - Applies optOverlay to any existing structs, given new % value is a struct too and both are 1x1 structs % Output % $opts - Outputs with optsIn values overlayed function [opts] = optOverlay(opts, optsIn, recurseStructs) if nargin < 3 recurseStructs = false; end isValid = @(o) isstruct(o) && length(o) == 1; assert(isValid(opts), 'Existing options cannot be cell array'); assert(isValid(optsIn), 'Input options cannot be cell array'); if ~isempty(optsIn) if iscell(optsIn) optsIn = struct(optsIn{:}); end assert(isstruct(optsIn)); fields = fieldnames(optsIn); for i = 1:length(fields) field = fields{i}; assert(isfield(opts, field), 'Field does not exist: %s', field); newValue = optsIn.(field); % Apply recursion if recurseStructs curValue = opts.(field); % Both values must be proper option structs if isValid(curValue) && isValid(newValue) newValue = optOverlay(curValue, newValue, true); end end opts.(field) = newValue; end end end 

Direi che usare la convenzione di denominazione 'default' e 'nuovo' sarebbe probabilmente meglio: P

Ho creato una funzione basata su Jonas e Richie Cotton. Implementa entrambe le funzionalità (argomenti flessibili o limitati, il che significa che sono consentite solo le variabili esistenti nei valori predefiniti) e alcune altre cose come i controlli sintattici su zucchero e sanità mentale.

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

Disponibile anche come Gist .

E per coloro che sono interessati ad avere argomenti con nome reale (con una syntax simile a Python, ad esempio: myfunction (a = 1, b = ‘qwerty’), utilizzare InputParser (solo per Matlab, gli utenti di Octave dovranno attendere fino alla v4.2 a o almeno puoi provare un wrapper chiamato InputParser2 ).

Inoltre, come bonus, se non vuoi scrivere sempre argstruct.yourvar ma usi direttamente yourvar , puoi usare il seguente snippet di 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 

C’è una parsepvpairs funzione chiamata parsepvpairs che si prende cura di questo in modo parsepvpairs , a patto che tu abbia accesso agli strumenti finanziari di MATLAB. Richiede tre argomenti, nomi di campo previsti, valori di campo predefiniti e gli argomenti effettivi ricevuti.

Ad esempio, ecco una funzione che crea una figura HTML in MATLAB e può prendere le coppie di valori di campo facoltative denominate ‘url’, ‘html’ e ‘titolo’.

 function htmldlg(varargin) names = {'url','html','title'}; defaults = {[],[],'Padaco Help'}; [url, html,titleStr] = parsepvpairs(names,defaults,varargin{:}); %... code to create figure using the parsed input values end