Matlab函数句柄工作区诡计

11

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

我有一个函数,它创建一个函数句柄,用于管道网络求解器。它将Network状态作为输入,其中包括有关管道及其连接的信息(如果必须,即边缘和顶点),构造一个大字符串,该字符串在函数形式下返回大矩阵,并"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, ';']);

这个函数按预期工作,但是每当我需要保存包含这个函数句柄的后续数据结构时,它需要消耗非常大的内存(150MB),巧合的是,这正好与此函数创建时Matlab工作区的大小(~150MB)相同。从getPipeEquations工作区所需的这个函数句柄的变量并不特别大,但更糟糕的是,当我检查函数句柄时:

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

     function: [1x8323 char]
         type: 'anonymous'
         file: '...\pkg\+adv\+pipe\getPipeEquations.m'
    workspace: {2x1 cell}

工作区字段包含了getPipeEquations中的所有内容(顺便说一句,这并不是整个Matlab工作区)。

如果我将eval语句移动到一个子函数中,试图强制使用范围,则句柄将更紧凑地保存(约1MB):

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

这是预期行为吗?有没有更优雅的方法限制这个匿名函数的范围?

另外,当我多次运行包括这个函数的模拟时,清除工作区变得非常缓慢,这可能与Matlab处理该函数及其工作区有关,也可能不相关。


你尝试过 evalin('base', ...) 吗?那有什么不同吗? - John Colby
我没有,但工作区应该已经被限制在 getPipeEquations 的范围内。 - Nathan Donnellan
3个回答

7

我可以重现:对于我来说,匿名函数捕获了封闭工作区中所有变量的副本,而不仅仅是在匿名函数表达式中引用的变量。

以下是最小重现代码:

function fcn = so_many_variables()
a = 1;
b = 2;
c = 3;
fcn = @(x) a+x;
a = 42;

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

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

一开始我对此感到惊讶。但是,当你考虑到fevaleval存在时,这是有道理的:Matlab在构建匿名函数时实际上无法知道它最终将引用哪些变量。因此,它必须捕获范围内的所有内容,以防它们被动态引用,就像这个假设性的例子中一样。这里使用了foo的值,但是在调用返回的函数句柄之前,Matlab不会知道这一点。

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

你正在进行的解决方法-将函数构建隔离在一个最小化工作空间中的单独函数中-听起来像是正确的修复方法。
以下是一种通用方法,可以让受限制的工作空间构建你要创建的匿名函数。
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);

这里的长变量名虽然影响可读性,但可以减少与调用者变量的冲突可能性。
你只需调用eval_with_vars()而不是eval(),并将所有输入变量作为额外参数传递。然后,您就不必为每个匿名函数构建器键入静态函数定义。只要您预先知道实际引用的变量,这种方法就会起作用,这与使用getJacobianHandle的方法具有相同的限制。
Jv = eval_with_vars_out(['@(v,f,rho) ' Jv_str],listConnected,areaPipes,endNodes,D,L,g,dz);

你可能可以在开始时使用inputname添加一个碰撞检查,但这可能是不必要的。 - Nathan Donnellan
好主意 - 碰撞不太可能,但如果发生了,你可能会得到一个难以诊断的静默错误。我已经修改了代码,添加了碰撞检查。 - Andrew Janke

2
匿名函数会捕获其作用域中的所有内容并将它们存储在函数工作区中。详见 MATLAB 文档的 匿名函数
特别地:
“表达式主体中指定的变量。MATLAB 捕获这些变量并在函数句柄的整个生命周期中保持它们不变。
这些后面的变量必须在构建使用它们的匿名函数时被赋值。在构造时,MATLAB 捕获每个在该函数主体中指定的变量的当前值。即使值在工作区中发生变化或超出作用域,该函数也将继续将此值与变量关联。”

1
你摘录和链接的文档并没有像你在这里说的那样做出如此广泛的声明。文档说它捕获表达式主体中引用的变量,但发帖者说它不仅捕获这些变量,还捕获了封闭工作区中的所有变量。 - Andrew Janke

0
一个可行的解决方法是利用Matlab的 save 函数只保存你所需的特定变量。 我曾经遇到过save 函数保存太多数据的问题(与您非常不同的上下文),但是一些谨慎的命名约定和在变量列表中使用通配符可以解决所有问题。

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