在Mathematica中防止运行时错误的雪崩效应

13

当笔记本超过几个函数时,我通常会遇到这样的情况-- 我评估了一个表达式,但是没有得到正确答案,而是得到了Beep声,然后是数十个无用的警告,最后是“进一步的输出...将被抑制”

我发现有一个有用的方法--在函数内使用类似Python的"assert"来强制执行内部一致性。还有其他建议吗?

Assert[expr_, msg_] := If[Not[expr], Print[msg]; Abort[], None]

编辑 11/14 警告大量出现的一般原因是子表达式计算出了“错误”的值。这导致父表达式计算出了一个“错误”的值,而这种“错误”一直传播到根部。一路上计算的内置函数会注意到这些错误并产生警告。“错误”可能意味着具有错误Head的表达式、元素数量错误的列表、负定性矩阵而不是正定性矩阵等等。通常这些都与父表达式的语义不符。

处理这个问题的一种方法是重新定义所有的函数在“无效输入”时返回未计算的值。这将解决大多数内置函数产生的消息。像“Part”这样做结构操作的内置函数仍然会尝试评估你的值并可能产生警告。

将调试器设置为“中断消息”可以防止错误的大量发生,但似乎总是开启它有点过头了。


3
我一直以为只有我这样 :) - Dr. belisarius
1
只需禁用蜂鸣声 - Simon
1
我收藏了这个,不是因为我期望从这个主题中获得一些启发,而是出于共情的共鸣。 - Dr. belisarius
3
@belisarius,你应该更加乐观。我一直都这么告诉你。 - Dr. belisarius
1
在Mma8中,现在有一个内置的assert函数。如果您将$AssertFunction设置为Abort[],则它基本上具有与您问题中的函数相同的行为。 - Simon
看起来很有用(但我正在等待8.0.1来解决不可避免的新版本问题)。 - Yaroslav Bulatov
5个回答

10

正如其他人指出的那样,有三种方法可以以一致的方式处理错误:

  1. 正确输入参数并设置函数运行条件,
  2. 正确且一致地处理生成的错误,以及
  3. 简化您的方法以应用这些步骤。

正如Samsdram所指出的,正确输入函数可以帮助很多。不要忘记使用 Pattern 的冒号形式,因为有时候在这种形式下表达某些模式更容易,例如 x:{{_, _} ..}。显然,当这不足够时,就需要使用 PatternTest (?) 和 Condition (/;)。Samsdram 已经涵盖了这方面的内容,但我想强调一点:您可以通过纯函数创建自己的模式测试,例如 f[x_?(Head[#]===List&)] 等同于 f[x_List]。注意,在使用 & 形式的纯函数时需要使用括号。

处理生成的错误最简单的方法显然是使用 Off,或更局部地使用Quiet。大多数情况下,我们都认为完全关闭不需要的消息是个坏主意,但是在您知道正在执行某些可能会产生警告但仍然正确的操作时,Quiet 可以非常有用。

ThrowCatch 也有其用途,但我认为它们只应该在内部使用,而您的代码应通过Message机制传达错误信息。可以像设置使用信息一样创建消息。我认为构建一个连贯的错误策略的关键在于使用函数:CheckCheckAbortAbortProtect

示例

我的代码中的一个示例是 OpenAndRead,它可以在读取操作中断时防止流未关闭,具体如下:

OpenAndRead[file_String, fcn_]:=
Module[{strm, res},
  strm = OpenRead[file];
  res = CheckAbort[ fcn[strm], $Aborted ];
  Close[strm];
  If[res === $Aborted, Abort[], res] (* Edited to allow Abort to propagate *)
]

直到最近,它一直有使用

fcn[ file_String, <otherparams> ] := OpenAndRead[file, fcn[#, <otherparams>]&]
fcn[ file_InputStream, <otherparams> ] := <fcn body>

然而,每次这样做都很烦人。

这就是 belisarius 提供的解决方案发挥作用的地方,通过创建一种可以一致使用的方法。不幸的是,他的解决方案存在一个致命缺陷:您将失去语法高亮功能的支持。因此,这里是我为连接上面的 OpenAndRead 想出来的一种替代方法

MakeCheckedReader /: 
    SetDelayed[MakeCheckedReader[fcn_Symbol, symbols___], a_] :=
    Quiet[(fcn[file_String, symbols] := OpenAndRead[file, fcn[#, symbols] &];
           fcn[file_Symbol, symbols] := a), {RuleDelayed::"rhs"}]

具有使用情况

MakeCheckedReader[ myReader, a_, b_ ] := {file$, a, b} (*as an example*)

现在,查看myReader的定义会给出两个定义,正如我们所希望的。 但是,在函数体中,file必须被称为file$。(我还没有弄清楚如何将文件变量命名为我希望的名称。)

编辑:MakeCheckedReader的工作原理实际上并不做任何事情。相反,TagSet/:)规范告诉Mathematica,当在SetDelayed的LHS中找到MakeCheckedReader时,用所需的函数定义替换它。此外,请注意使用Quiet;否则,它会抱怨模式a_b_出现在等式的右侧。

编辑2:Leonid指出如何能够在定义检查的读取器时使用file而不是file$。更新后的解决方案如下:

MakeCheckedReader /: 
    SetDelayed[MakeCheckedReader[fcn_Symbol, symbols___], a_] :=
    Quiet[(fcn[file_String, symbols] := OpenAndRead[file, fcn[#, symbols] &];
           SetDelayed @@ Hold[fcn[file_Symbol, symbols], a]), 
           {RuleDelayed::"rhs"}]

这个更改的原因在他的回答中解释。 像上面定义myReader并检查它的定义,我们得到:

myReader[file$_String,a_,b_]:=OpenAndRead[file$,myReader[#1,a_,b_]&]
myReader[file_Symbol,a_,b_]:={file,a,b}

感谢指出这个缺陷。虽然我不确定它有多“致命”,因为您可以“正常地”检查函数语法并使用最少的努力将其转换为/circleminus语法。我仍在钻研这个问题,我真的希望找到一种非侵入式的错误中止方式,而不需要额外的开销代码。 - Dr. belisarius
顺便说一下,+1...我需要调查TagSet...从未使用过它。 - Dr. belisarius
再次感谢您提供这样的答案,让我每天都能回来看看 :-). 虽然我必须说,手动解决Mathematica弱类型问题似乎有些违反直觉。我遇到了这样的问题,在大型项目中这样做(假设有4或5层模块和函数)将会导致显著的减速,并增加必要的错误消息传播到主程序的复杂性。 - Timo
2
@belisarius: TagSet, UpSet (^=), 和 UpSetDelayed (^:=) 为相关函数生成上值(upvalues)。由于内置函数是 Protected,因此很难创建具有任何数学行为的新对象。这些函数通过将转换与对象本身关联而不是操作来解决了这个问题。我从《Mathematica量子方法》(http://www.amazon.com/Quantum-Methods-Mathematica-James-Feagin/dp/0387953655/ref=tmm_pap_title_0?ie=UTF8&qid=1289761092&sr=8-2) 中学到了它们。 - rcollyer
1
@rcollyer 在 http://programmers.stackexchange.com/questions/19146 上发布了一个问题,关于Mathematica和Wolfram Research的书籍推荐。 - Dr. belisarius
显示剩余19条评论

9

虽然我来晚了,但是我想指出一下形如以下定义的内容:

这些定义可能会使人感到困惑。更好的方式是将“抽象”和“具体”视为相对概念,而不是绝对概念。在这种情况下,“抽象”意味着从高层次上看待事物,而“具体”则意味着从低层次上看待事物。

f[...] := Module[... /; ...]

在这种情况下,定义非常有用。这种类型的定义可以在最终放弃并决定该定义不适用之前执行复杂的计算。

我将说明如何在另一个SO问题的情况下使用它来实现各种错误处理策略。问题是要搜索一组固定的对:

data = {{0, 1}, {1, 2}, {2, 4}, {3, 8}, {4, 15}, {5, 29}, {6, 50}, {7,
     88}, {8, 130}, {9, 157}, {10, 180}, {11, 191}, {12, 196}, {13, 
    199}, {14, 200}};

寻找第一个满足第二个元素大于或等于指定值的一对。一旦找到该对,其第一个组成部分将被返回。在Mathematica中有许多方法可以编写此代码,但是这里是其中之一:

f0[x_] := First @ Cases[data, {t_, p_} /; p >= x :> t, {1}, 1]

f0[100] (* returns 8 *)

现在的问题是,如果使用无法找到的值调用该函数会发生什么?

f0[1000]
error: First::first: {} has a length of zero and no first element.

这个错误信息含义晦涩,最好情况下也无法提供问题所在的线索。如果该函数在调用链深处被调用,则可能会出现一系列类似的不透明错误。

处理这种异常情况有多种策略。其中一种是更改返回值,以便可以区分成功和失败的情况:

f1[x_] := Cases[data, {t_, p_} /; p >= x :> t, {1}, 1]

f1[100] (* returns {8} *)
f1[1000] (* returns {} *)

然而,在Mathematica中有一个强烈的传统,即在函数用其域外的参数进行评估时保持原始表达式不变。这就是Module[... /; ...]模式可以提供帮助的地方:

f2[x_] :=
  Module[{m},
    m = Cases[data, {t_, p_} /; p >= x :> t, {1}, 1];
    First[m] /; m =!= {}
  ]

f2[100] (* returns 8 *)
f2[1000] (* returns f2[1000] *)

请注意,如果最终结果是空列表并且原始表达式未求值返回,则f2将完全退出--通过在最终表达式中添加/;条件来实现这一点。
如果发生“未找到”情况,可以决定发出有意义的警告:
f2[x_] := Null /; Message[f2::err, x] 
f2::err = "Could not find a value for ``.";

通过这个改变,相同的值将会被返回,但是在“未找到”情况下会发出警告信息。在新定义中,Null返回值可以是任何值--它不会被使用。

进一步地,有人可能认为“未找到”的情况根本不可能发生,除非是客户端代码存在缺陷。在这种情况下,应该使计算中止:

f2[x_] := (Message[f2::err, x]; Abort[])

总之,这些模式足够简单,可以处理定义域之外的函数参数。在定义函数时,值得花费一些时间来决定如何处理域错误。这样做可以减少调试时间。毕竟,在Mathematica中,几乎所有的函数都是局部函数。考虑以下情况:一个函数可能会被调用以处理字符串、图像、歌曲或漫游的纳米机器人群(也许在Mathematica 9中)。
最后要注意的是...我应该指出,在使用多个定义定义和重新定义函数时,由于“剩余”定义,很容易出现意外结果。作为一般原则,我强烈建议在重复定义函数之前先使用Clear
Clear[f]
f[x_] := ...
f[x_] := Module[... /; ...]
f[x_] := ... /; ...

谢谢,这似乎是一个有用的模式。我不理解 "Condition[...,Message]" 的含义...文档表明它仅在第二个参数计算为 "True" 时匹配,但 Message 返回 Null,那么它为什么有效? - Yaroslav Bulatov
另一个问题是,有没有一种简单的方法可以在评估过程中中止并返回未评估的结果?目前,我使用If[somethingbadhappened,Abort[]],但这会返回$Aborted。 - Yaroslav Bulatov
@Yaroslav Bulatov:当一个条件是定义的守卫时,除了True以外的任何值都会导致求值器继续匹配下一个定义(如果有的话)。在这种情况下,Null =!= True,因此求值器继续向下执行--但没有进一步匹配的定义,因此表达式保持不变返回。 - WReach
@Yaroslav Bulatov:严格来说,中止和返回未评估是互斥的,但我认为我知道你的意思:f[x_]:= Module[{t}, t = someComplexComputation[x]; moreComplexComputation[t] /; !somethingBadHappened[t] ] - WReach
+1,对于处理一致产生的错误的良好示例/模式。 - rcollyer

3

这里的问题基本上是类型问题。一个函数产生了错误的输出(不正确的类型),然后被输入到许多后续函数中,导致了大量的错误。虽然Mathematica没有像其他语言一样的用户定义类型,但你可以对函数参数进行模式匹配而不需要太多的工作。如果匹配失败,函数就不会被评估,因此也不会出现错误提示音。关键的语法片段是“/;”,它放在某些代码的末尾,后面跟着测试。以下是一些示例代码(及其输出)。

Input:
Average[x_] := Mean[x] /; VectorQ[x, NumericQ]
Average[{1, 2, 3}]
Average[$Failed]

Output:
2
Average[$Failed]

如果测试更简单,有另一个符号可以进行类似的模式匹配测试“?”并紧跟在模式/函数声明的参数后面。以下是另一个例子。
Input:
square[x_?NumericQ] := x*x
square[{1, 2, 3}]
square[3]

Output:
square[{1, 2, 3}]
9

3

可以定义一个万能的错误处理程序,以便捕获所有的错误,并以有意义的方式进行报告:

f[x_?NumericQ] := x^2;
f[args___] := Throw[{"Bad Arguments: ", Hold[f[args]]}]

所以你的顶层调用可以使用Catch[],或者你可以让它自动上升:

In[5]:= f[$Failed]

During evaluation of In[5]:= Throw::nocatch: Uncaught Throw[{Bad Args: ,Hold[f[$Failed]]}] returned to top level. >>

Out[5]= Hold[Throw[{"Bad Args: ", Hold[f[$Failed]]}]]

3
我希望得到一种定义通用流程以捕获错误传播的方法,而不需要彻底改变我现在编写函数的方式,最好不需要添加大量的输入。以下是一个尝试:
funcDef = t_[args___]  :c-:  a_ :> ReleaseHold[Hold[t[args] := 
                         Check[a, Print@Hold[a]; Abort[]]]];
Clear@v;
v[x_, y_] :c-: Sin[x/y] /. funcDef;
?v
v[2, 3]
v[2, 0] 
当然是Esc c- Esc,一个未使用的符号(\[CircleMinus]),但任何人都可以使用。
Global`v
v[x_,y_]:=Check[Sin[x/y],Print[Hold[Sin[x/y]]];Abort[]]

Out[683]= Sin[2/3]

During evaluation of In[679]:= Power::infy: Infinite expression 1/0 encountered. >>

During evaluation of In[679]:= Hold[Sin[2/0]]

Out[684]= $Aborted

我们所做的改变是:
       v[x_, y_] := Sin[x/y]

by

       v[x_, y_] :c-: Sin[x/y] /. funcDef;  

这几乎满足我的前提条件。
编辑
也许为函数添加一个“裸体”定义会更方便,它不需要进行错误检查。我们可以将funcDef规则更改为:
funcDef = 
     t_[args___]  \[CircleMinus] a_ :> 

            {t["nude", args] := a, 

             ReleaseHold[Hold[t[args] := Check[a, Print@Hold[a]; Abort[]]]]
            };  

获取

 v[x_, y_] :c-: Sin[x/y] /. funcDef;  

这个输出

v[nude,x_,y_]:=Sin[x/y]

v[x_,y_]:=Check[Sin[x/y],Print[Hold[Sin[x/y]]];Abort[]]

根据我上面的评论,这个解决方案无法利用语法高亮系统,请查看SyntaxInformation函数。根据帮助文件,它允许您告诉Mathematica如何突出显示事物。我没有尝试过,但您可能能够使args中的符号在:c-:两侧都正确地突出显示。 - rcollyer
使用自定义定义运算符的中缀符号+1。通过为:c-:定义DownValue,您就不必执行/. funcDef - masterxilo

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