如何在Matlab中获取函数参数的名称?

19

除了解析函数文件外,有没有一种方法可以在MATLAB中获取函数的输入和输出参数的名称?

例如,给定以下函数文件:

divide.m

function [value, remain] = divide(left, right)
     value = floor(left / right);
     remain = left / right - value;
end

我想从函数外部获取一个输出参数的数组,这里是:['value', 'remain'],并且类似地获取输入参数:['left', 'right']

在Matlab中是否有一种简单的方法实现?Matlab通常支持反射。

编辑背景:

这样做的目的是在窗口中呈现函数参数供用户输入。我正在编写一种信号处理程序,并将执行这些信号操作的函数存储在子文件夹中。我已经有一个列表和每个函数的名称供用户选择,但是某些函数需要额外的参数(例如平滑函数可能会将窗口大小作为参数)。

目前,我可以向子文件夹添加新函数,程序将找到它,并且用户可以选择它来执行操作。我缺少的是让用户指定输入和输出参数,在这里我遇到了障碍,因为我找不到函数的名称。


难道这不是你使用“打开”命令的原因吗? - Rasman
在函数内部还是外部?我假设是外部,因为这样使用起来很简单。 - Gunther Struyf
@Hannesh 你的意思是想要函数声明中的变量名,就像它在实现中出现的那样吗? - Eitan T
@EitanT 是的。运行时必须知道名称才能在函数调用时创建变量,因此我想肯定有一种访问它们的方法。 - Hannesh
可能唯一的方法是解析文件。尝试使用checkcode查看是否能获取任何信息。 - sivann
6个回答

12

MATLAB提供了一种获取类元数据信息的方法(使用meta包),但这仅适用于面向对象的类而非普通函数。

一个技巧是动态编写一个类定义,其中包含您想要处理的函数的源代码,并让MATLAB处理源代码的解析(这可能很棘手,因为函数定义行跨越多行,实际定义之前有注释等等)。

因此,在您的情况下创建的临时文件将如下所示:

classdef SomeTempClassName
    methods
        function [value, remain] = divide(left, right)
            %# ...
        end
    end
end
可以将其传递给 meta.class.fromName 用于解析元数据...
这是一个快速而简单的实现方法:
function [inputNames,outputNames] = getArgNames(functionFile)
    %# get some random file name
    fname = tempname;
    [~,fname] = fileparts(fname);

    %# read input function content as string
    str = fileread(which(functionFile));

    %# build a class containing that function source, and write it to file
    fid = fopen([fname '.m'], 'w');
    fprintf(fid, 'classdef %s; methods;\n %s\n end; end', fname, str);
    fclose(fid);

    %# terminating function definition with an end statement is not
    %# always required, but now becomes required with classdef
    missingEndErrMsg = 'An END might be missing, possibly matching CLASSDEF.';
    c = checkcode([fname '.m']);     %# run mlint code analyzer on file
    if ismember(missingEndErrMsg,{c.message})
        % append "end" keyword to class file
        str = fileread([fname '.m']);
        fid = fopen([fname '.m'], 'w');
        fprintf(fid, '%s \n end', str);
        fclose(fid);
    end

    %# refresh path to force MATLAB to detect new class
    rehash

    %# introspection (deal with cases of nested/sub-function)
    m = meta.class.fromName(fname);
    idx = find(ismember({m.MethodList.Name},functionFile));
    inputNames = m.MethodList(idx).InputNames;
    outputNames = m.MethodList(idx).OutputNames;

    %# delete temp file when done
    delete([fname '.m'])
end

并且只需运行:

>> [in,out] = getArgNames('divide')
in = 
    'left'
    'right'
out = 
    'value'
    'remain'

这看起来很有趣。我得试着玩一下。 - gnovice
@Amro 如果函数使用可变输入/输出参数声明,即使用 varargin 和/或 varargout,该怎么办? - Eitan T
@EitanT:它将简单地返回 varargin 和/或 varargout。另一种方法是在每个函数中编写“文档注释”,这些注释易于区分(类似于 javadocs 的 @param@return),并使用正则表达式解析这些注释,就像 gnovice 在他的答案中展示的那样。 - Amro

11
如果您的问题仅涉及简单情况,即您想解析文件中主要函数的function declaration line(即您不会处理local functionsnested functionsanonymous functions),则可以使用一些标准字符串操作和regular expressions提取输入和输出参数名称,如它们在文件中出现的那样。函数声明行具有标准格式,但您必须考虑到由于以下几个原因而产生的一些变化:

(结果证明处理块注释是最棘手的部分...)

我编写了一个名为get_arg_names的函数来处理上述所有内容。如果您给它一个函数文件的路径,它将返回包含输入和输出参数字符串的两个单元格数组(如果没有则为空单元格数组)。请注意,具有可变输入或输出列表的函数将简单地列出'varargin''varargout',分别用于变量名称。这是该函数:

function [inputNames, outputNames] = get_arg_names(filePath)

    % Open the file:
    fid = fopen(filePath);

    % Skip leading comments and empty lines:
    defLine = '';
    while all(isspace(defLine))
        defLine = strip_comments(fgets(fid));
    end

    % Collect all lines if the definition is on multiple lines:
    index = strfind(defLine, '...');
    while ~isempty(index)
        defLine = [defLine(1:index-1) strip_comments(fgets(fid))];
        index = strfind(defLine, '...');
    end

    % Close the file:
    fclose(fid);

    % Create the regular expression to match:
    matchStr = '\s*function\s+';
    if any(defLine == '=')
        matchStr = strcat(matchStr, '\[?(?<outArgs>[\w, ]*)\]?\s*=\s*');
    end
    matchStr = strcat(matchStr, '\w+\s*\(?(?<inArgs>[\w, ]*)\)?');

    % Parse the definition line (case insensitive):
    argStruct = regexpi(defLine, matchStr, 'names');

    % Format the input argument names:
    if isfield(argStruct, 'inArgs') && ~isempty(argStruct.inArgs)
        inputNames = strtrim(textscan(argStruct.inArgs, '%s', ...
                                      'Delimiter', ','));
    else
        inputNames = {};
    end

    % Format the output argument names:
    if isfield(argStruct, 'outArgs') && ~isempty(argStruct.outArgs)
        outputNames = strtrim(textscan(argStruct.outArgs, '%s', ...
                                       'Delimiter', ','));
    else
        outputNames = {};
    end

% Nested functions:

    function str = strip_comments(str)
        if strcmp(strtrim(str), '%{')
            strip_comment_block;
            str = strip_comments(fgets(fid));
        else
            str = strtok([' ' str], '%');
        end
    end

    function strip_comment_block
        str = strtrim(fgets(fid));
        while ~strcmp(str, '%}')
            if strcmp(str, '%{')
                strip_comment_block;
            end
            str = strtrim(fgets(fid));
        end
    end

end

这个一般来说有几个原因为什么不会起作用:首先是你自己提到的那个问题。其次,标题之前可能有空格。解析的函数可能在另一个函数文件中(甚至嵌套的)。或者是一个匿名函数,没有输出参数名称。但是确实很好的尝试,我也曾经遇到过以上的问题:p - Gunther Struyf
1
@GuntherStruyf:我纠正了许多限制。它仍然只能在主要函数上运行,但我认为这并不是太大的问题,因为你无法从外部文件调用子函数或嵌套函数(除非你开始玩弄函数句柄)。 - gnovice
@EitanT:这种方法有一个缺点:如果函数被重载了,调用WHICH可能会选择错误的函数,所以更安全的做法是先获取路径(使用带参数的WHICH或其他方法),然后再将其传递给上述函数。 - gnovice
1
@gnovice 我认为如果which选择默认函数(如果它被重载)是期望的行为。如果没有,你就必须指定一个更准确的名称。例如,convgf/conv重载,所以如果你想解析后者,你必须指定gf/conf而不仅仅是conv。在我看来,这不是一个问题。 - Eitan T
1
如果OP可以控制函数,那么在注释中使用自定义分隔符并以易于解析的方式包含输入/输出变量可能更简单。例如 %#!@ input: foo bar output: baz @!#,其中 #!@ 表示开始,@!#(反向)表示结束。输入和输出标记以 : 结尾,中间用空格分隔的字符串是变量... - user616736
显示剩余6条评论

4
这对于通用函数(比如varargin等)来说非常难做到(可以说是不可能的)。而且一般来说,依赖变量名作为文档的形式可能并不是你想要的。我会建议采取不同的方法。
既然您控制程序,那么能否在每个模块中除了m-file外再指定一个表格条目,以提供额外信息。您可以记录额外的参数、函数本身,标记选项为布尔值,并将其表示为复选框等。
那么这些内容应该放在哪里呢?我建议主m-file函数返回结构体,作为模块加载步骤,带有指向执行实际工作的子函数(或嵌套函数)的函数句柄。这样保留了单个文件设置,同时为您的模块提供了更多配置选项。
function module = divide_load()
    module.fn = @my_divide;
    module.name = 'Divide';
    module.description = 'Divide two signals';
    module.param(1).name = 'left';
    module.param(1).description = 'left signal';
    module.param(1).required_shape = 'columnvector';
    % Etc, etc.

    function [value, remain] = my_divide(left, right)
         value = floor(left / right);
         remain = left / right - value;
    end
end

1

当你无法从编程语言中获取有关其内容的信息(例如,“反射”),你必须走出语言的范畴。

另一位发帖者建议使用“正则表达式”,但是当应用于解析真实程序时,正则表达式总是失败的,因为正则表达式无法解析上下文无关语言。

要可靠地完成这项工作,您需要一个真正的M语言解析器,它将为您提供访问解析树的权限。然后这就相当容易了。

我们的DMS软件重构工具包提供了一个M语言解析器,并且可以完成此任务。


我同意正则表达式通常在尝试解析整个程序时会失败,但 OP 只想解析函数定义行,这有一个标准格式和有限的变化数量。标准字符串操作和正则表达式的组合可以很好地处理这种情况。 - gnovice
1
也许吧。关键字“function”肯定是一个可以帮助他的信标,如果他不介意有时搞错,那可能就是他需要搜索的全部内容了。如果他想更加小心,他必须担心包含看起来像函数头的注释和字符串(理论上可能性较小但仍有可能),以及嵌套的函数头(假设他不想要这些)以及注释和换行符卡在函数头内部的各种随机位置的问题。简单的正则表达式无法解决问题;他必须构建大多数词法分析器,以确保他不会迷失方向。 - Ira Baxter
如果他对方法参数感兴趣,他就必须跟踪他所在的类以及该方法是否已重载。也许他的问题被定义为简单的。 - Ira Baxter

0

你考虑过使用映射容器吗?

你可以按照以下方式编写你的函数...

function [outMAP] = divide(inMAP)
     outMAP = containers.Map();
     outMAP('value') = floor(inMAP('left') / inMAP('right'));
     outMAP('remain') = inMAP('left') / inMAP('right') - outMAP('value');
end

...并且像这样调用它们...

inMAP  = containers.Map({'left', 'right'}, {4, 5});
outMAP = divide(inMAP);

...然后只需使用以下语法检查变量名...

>> keys(inMAP)

ans = 

    'left'    'right'

-2

5
这是实际参数名(在调用方的上下文中),他似乎只想要形式参数名。 - Ben Voigt
1
@Ben Voigt 是的,你说得对。我想从函数外部获取参数的名称,就像在函数定义中写的那样。 - Hannesh
1
有没有类似这样的东西,但是用于输出参数?从调用者那里获取参数的名称? - tim

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