Matlab函数处理工作区shenanigans

Nat*_*lan 11 matlab closures scope eval anonymous-function

简而言之:是否有一种优雅的方式来限制匿名函数的范围,或者在这个例子中是否有Matlab破坏?

我有一个函数创建一个在管网解算器中使用的函数句柄.它接受一个网络状态作为输入,其中包括有关管道及其连接的信息(如果必须,还包括边和顶点),构造一个大字符串,在函数形式时将返回一个大矩阵,并"evals"该字符串以创建句柄.

function [Jv,...] = getPipeEquations(Network)
... %// some stuff happens here

Jv_str = ['[listConnected(~endNodes,:)',...
    ' .* areaPipes(~endNodes,:);\n',...
    anotherLongString,']'];

Jv_str = sprintf(Jv_str); %// This makes debugging the string easier

eval(['Jv = @(v,f,rho)', Jv_str, ';']);
Run Code Online (Sandbox Code Playgroud)

这个函数按预期工作,但每当我需要保存包含此函数句柄的后续数据结构时,它需要一个可笑的内存量(150MB) - 巧合的是在创建此函数时整个Matlab工作区(~~) 150MB).这个函数处理的变量需要来自getPipeEquations工作区并不是特别大,但是当我检查函数句柄时,更疯狂的是:

>> f = functions(Network.jacobianFun)
f = 

     function: [1x8323 char]
         type: 'anonymous'
         file: '...\pkg\+adv\+pipe\getPipeEquations.m'
    workspace: {2x1 cell}
Run Code Online (Sandbox Code Playgroud)

...工作区字段包含getPipeEquations具有的所有内容(顺便说一句,它不是整个Matlab工作区).

如果我改为将eval语句移动到子函数以试图强制范围,则句柄将更加紧凑(~1MB):

function Jv = getJacobianHandle(Jv_str,listConnected,areaPipes,endNodes,D,L,g,dz)
eval(['Jv = @(v,f,rho)', Jv_str, ';']);
Run Code Online (Sandbox Code Playgroud)

这是预期的行为吗?是否有更优雅的方式来限制此匿名函数的范围?

作为附录,当我多次运行包含此函数的模拟时,清除工作空间变得非常缓慢,这可能与Matlab处理函数及其工作空间有关,也可能与之无关.

And*_*nke 7

我可以重现:我的匿名函数捕获封闭工作空间中所有变量的副本,而不仅仅是匿名函数表达式中引用的那些变量.

这是一个最小的复制品.

function fcn = so_many_variables()
a = 1;
b = 2;
c = 3;
fcn = @(x) a+x;
a = 42;
Run Code Online (Sandbox Code Playgroud)

实际上,它捕获了整个封闭工作区的副本.

>> f = so_many_variables;
>> f_info = functions(f);
>> f_info.workspace{1}
ans = 
    a: 1
>> f_info.workspace{2}
ans = 
    fcn: @(x)a+x
      a: 1
      b: 2
      c: 3
Run Code Online (Sandbox Code Playgroud)

起初这对我来说是一个惊喜.但是当你想到它时它才有意义:由于存在fevaleval,Matlab实际上无法在构造时知道匿名函数实际上最终会引用哪些变量.所以它必须捕获范围内的所有内容,以防它们被动态引用,就像在这个人为的例子中一样.这使用了值foo但Matlab在调用返回的函数句柄之前不会知道.

function fcn = so_many_variables()
a = 1;
b = 2;
foo = 42;
fcn = @(x) x + eval(['f' 'oo']);
Run Code Online (Sandbox Code Playgroud)

您正在进行的解决方法 - 将功能构造隔离在具有最小工作空间的单独功能中 - 听起来像是正确的修复.

这是一种通用的方法来获得受限制的工作空间来构建您的匿名函数.

function eval_with_vars_out = eval_with_vars(eval_with_vars_expr, varargin)

% Assign variables to the local workspace so they can be captured
ewvo__reserved_names = {'varargin','eval_with_vars_out','eval_with_vars_expr','ewvo__reserved_names','ewvo_i'};
for ewvo_i = 2:nargin
    if ismember(inputname(ewvo_i), ewvo__reserved_names)
        error('variable name collision: %s', inputname(ewvo_i));
    end
    eval([ inputname(ewvo_i) ' = varargin{ewvo_i-1};']);
end
clear ewvo_i ewvo__reserved_names varargin;

% And eval the expression in that context
eval_with_vars_out = eval(eval_with_vars_expr);
Run Code Online (Sandbox Code Playgroud)

这里的长变量名称会损害可读性,但会降低与调用者变量冲突的可能性.

您只需调用eval_with_vars()而不是eval(),并将所有输入变量作为附加参数传入.然后,您不必为每个匿名函数构建器键入静态函数定义.只要您事先知道实际将要引用哪些变量,这将起作用,这与使用的方法具有相同的限制getJacobianHandle.

Jv = eval_with_vars_out(['@(v,f,rho) ' Jv_str],listConnected,areaPipes,endNodes,D,L,g,dz);
Run Code Online (Sandbox Code Playgroud)