Cómo tratar con pares de nombre / valor de argumentos de función en MATLAB

Tengo una función que toma argumentos opcionales como pares nombre / valor.

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

Parece que hay un gran esfuerzo involucrado en la extracción de los valores apropiados (y todavía no es particularmente robusto otra vez entradas mal especificadas). ¿Hay una mejor manera de manejar estos pares de nombre / valor? ¿Hay alguna función de ayuda que viene con MATLAB para ayudar?

Prefiero usar estructuras para mis opciones. Esto le brinda una manera fácil de almacenar las opciones y una manera fácil de definirlas. Además, todo se vuelve bastante compacto.

 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 ayuda con esto. Ver Entradas de función Parse para más información.

Podría hablar durante horas sobre esto, pero aún no tengo una buena vista gestalt del manejo general de la firma Matlab. Pero aquí hay un par de consejos.

En primer lugar, adopte un enfoque de laissez fair para validar los tipos de entrada. Confíe en la persona que llama. Si realmente quieres pruebas de tipo fuerte, quieres un lenguaje estático como Java. Intenta forzar la seguridad de tipo en todas partes en Matlab, y terminarás con una buena parte de tu LOC y tiempo de ejecución dedicado a pruebas de tipo de tiempo de ejecución y coerción en el territorio del usuario, que se intercambia con mucha potencia y velocidad de desarrollo de Matlab . Aprendí esto de la manera difícil.

Para las firmas API (funciones destinadas a ser llamadas desde otras funciones, en lugar de desde las líneas de comando), considere usar un único argumento Args en lugar de varargin. Luego se puede pasar entre múltiples argumentos sin tener que convertirlo a una lista separada por comas para firmas varargin. Las estructuras, como dice Jonas, son muy convenientes. También hay un buen isomorfismo entre las estructuras y las celdas n-by-2 {name, value; …}, y puede configurar un par de funciones para convertirlas dentro de sus funciones a lo que quiera usar internamente.

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

Ya sea que use inputParser o que ejecute su propio analizador de nombre / val como estos otros buenos ejemplos, organícelo en una función estándar separada a la que llamará desde la parte superior de sus funciones que tengan firmas de nombre / val. Haga que acepte la lista de valores predeterminados en una estructura de datos que sea conveniente para escribir, y sus llamadas de análisis arg se parecerán a las declaraciones de firmas de funciones, lo que ayuda a la legibilidad, y evita copiar y pegar el código repetitivo.

Así es como se verían las llamadas de análisis.

 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 

Y aquí hay una función para implementar el análisis de nombre / val de esa manera. Podría ahuecarlo y reemplazarlo con inputParser, sus propias convenciones de tipo, etc. Creo que la convención de células n-by-2 hace que el código fuente sea muy legible; Considera mantener eso. Por lo general, las estructuras son más cómodas de tratar en el código de recepción, pero las celdas n-by-2 son más convenientes de construir usando expresiones y literales. (Las estructuras requieren la continuación “, …” en cada línea y evitan que los valores de celda se expandan a estructuras no escalares).

 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 

Es tan desafortunado que las cadenas y los datenums no son tipos de primera clase en Matlab.

Personalmente utilizo una función personalizada derivada de un método privado utilizado por muchas funciones de Statistics Toolbox (como kmeans, pca, svmtrain, ttest2, …)

Al ser una función de utilidad interna, cambió y se renombró muchas veces en las versiones. Dependiendo de su versión de MATLAB, intente buscar uno de los siguientes archivos:

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

Al igual que con cualquier función no documentada, no hay garantías y podría eliminarse de MATLAB en versiones posteriores sin previo aviso … De todos modos, creo que alguien publicó una versión anterior de ella como getargs en el intercambio de archivos.

La función procesa parámetros como pares de nombre / valor, utilizando un conjunto de nombres de parámetros válidos junto con sus valores predeterminados. Devuelve los parámetros analizados como variables de salida separadas. De forma predeterminada, los pares de nombre / valor no reconocidos generan un error, pero también podemos capturarlos silenciosamente en un resultado adicional. Aquí está la descripción de la función:

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

Ejemplo:

 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 

Ahora esta función de ejemplo podría llamarse de cualquiera de las siguientes maneras:

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

Estas son algunas llamadas no válidas y los errores arrojados:

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

Como otros han mencionado, el enfoque recomendado oficialmente para las entradas de funciones de análisis es utilizar la clase inputParser . Es compatible con varios esquemas, como la especificación de las entradas necesarias, los argumentos posicionales opcionales y los parámetros de nombre / valor. También permite realizar la validación en las entradas (como verificar la clase / tipo y el tamaño / forma de los argumentos)

Lea la publicación informativa de Loren sobre este tema. No olvides leer la sección de comentarios … – Verás que hay bastantes enfoques diferentes para este tema. Todos funcionan, por lo que seleccionar un método preferido es realmente una cuestión de gusto personal y facilidad de mantenimiento.

Soy un gran admirador del código de la placa de la caldera de fabricación propia como este:

 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 

De esta forma puedo simular la ‘opción’, par de valores que es casi idéntico a cómo la mayoría de las funciones de Matlab toman sus argumentos.

Espero que ayude,

Será

Aquí está la solución que estoy probando, basada en la idea de 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 

Inspirado por la respuesta de Jonas, pero más compacto:

 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 

Desde hace mucho tiempo estoy usando process_options.m . Es estable, fácil de usar y ha sido incluido en varios frameworks de matlab. Aunque no sé nada sobre el rendimiento, es posible que haya implementaciones más rápidas.

La característica que más me gusta con process_options es el valor de retorno unused_args , que se puede usar para dividir args de entrada en grupos de argumentos para, por ejemplo, subprocesos.

Y puede definir fácilmente valores predeterminados.

Lo más importante: el uso de process_options.m generalmente da como resultado definiciones de opciones legibles y mantenibles .

Código de ejemplo:

 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 

Por supuesto, esto no comprueba las asignaciones correctas, pero es simple y cualquier variable inútil será ignorada de todos modos. También solo funciona para números, cadenas y matrices, pero no para matrices, celdas o estructuras.

Terminé escribiendo esto hoy, y luego encontré estas menciones. El mío usa struct y struct ‘overlays’ para las opciones. Esencialmente refleja la funcionalidad de setstructfields () excepto que no se pueden agregar nuevos parámetros. También tiene una opción para recurrir, mientras que setstructfields () lo hace automáticamente. Puede incluir una matriz de celdas de valores emparejados llamando a 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 

Yo diría que usar la convención de nomenclatura 'por defecto' y 'nuevo' probablemente sería mejor: P

Hice una función basada en Jonas y Richie Cotton. Implementa ambas funcionalidades (argumentos flexibles o restringidos, lo que significa que solo se permiten las variables existentes en los valores predeterminados), y algunas otras cosas como controles sintácticos de azúcar y cordura.

 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 

También disponible como Gist .

Y para aquellos interesados ​​en tener argumentos con nombres reales (con una syntax similar a Python, por ejemplo: myfunction (a = 1, b = ‘qwerty’), use InputParser (solo para Matlab, los usuarios de Octave tendrán que esperar hasta v4.2 en menos o puedes probar un contenedor llamado InputParser2 ).

Además, como argstruct.yourvar adicional, si no desea tener que escribir siempre argstruct.yourvar pero usa directamente yourvar , puede usar el siguiente fragmento de 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 

Hay una función ingeniosa llamada parsepvpairs que se encarga de esto, siempre que tenga acceso a la caja de herramientas de finanzas de MATLAB. Toma tres argumentos, nombres de campo esperados, valores de campo predeterminados y los argumentos reales recibidos.

Por ejemplo, aquí hay una función que crea una figura de HTML en MATLAB y puede tomar los pares de valores de campo opcionales llamados ‘url’, ‘html’ y ‘title’.

 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