![]() 文章PDF浏览下载链接目录:
为什么要对函数的输入进行检查在工程计算中,如果一个函数的输入有错误,我们总是希望能够尽早的通过对输入的检查,捕捉到这些错误,并及时终止程序。这样做的原因是,如果等到程序运行时出错或者运行结束后计算结果出错再查找,那就很迟了,而且通常debug的成本很高。在多人合作的项目中,如果一个开发人员提供了一个公用的API(应用程序接口)给别人使用,除了要提供说明文档规定输入的格式之外,API内部通常还需要对输入进行彻底的检查,因为开发人员不能保证每个使用者都会仔细地读文档,并且每次都能提供符合规定的数据,作为一个友好的API,一旦输入出了错,API应该及时提示用户,并且帮助诊断错误原因。同理,这样做的原因是,如果要等到程序运行时出错或者运行结束后计算结果出错,不但成本高,而且使用者也许根本无法查出错误的原因。 在MATLAB中,我们可以使用MATLAB提供的专门的函数validateattributes,validatestring和inputParser类来对输入进行检查。它们提供全面的检查功能和清晰的错误提示,是全套的参数检查解析方案。validateattributes的基本使用先介绍validateattributes的基本使用。假设在图像处理计算中,我们设计了一个函数叫做processImg ,用来对一张大小是500 x $500 的灰值图像进行处理,计算之前我们需要检查输入是否符合规定,这可以使用validateattributes函数来完成:
% 函数一开始检查输入变量的类型和尺寸 function processImg(img) ... validateattributes(img,{'numeric'},{'size',[500,500]}); ... % 函数继续 endvalidateattributes的第一个参数img是输入的图像,即要检查的变量;第二个参数是要检查的类型,这里规定img必须是数值类型(numeric);第三个参数对变量要检查的属性,这里的属性是对img规定的尺寸。 validateattributes的最基本调用格式是: validateattributes(A,classes,attributes)其中classes和attributes通过元胞数组来指定,并且元胞中可以包括多个要检查的类型和属性,比如我们除了要检查图像的尺寸,还要检查该图像矩阵的值都在0到255之间,可以这样写: %元胞数组中可以放置多个要检查的属性 ... validateattributes(img,{'numeric'},{'size',[500,500],'>=',0,'<=',255}); ...在这个例子中,要验证的类型是numeric,它是一个各种数值类型的集合,包括int8, int16, int32, int64, uint8, uint16, uint32, uint64,single,和double类型。当然我们可以让类型检查再具体一点,比如做数值积分的时候,我们通常要提供一个积分的网格,比如一维积分中的X轴,而且通常需要保证该x轴格点的值的类型是double,并是单调递增的,可以用validateattributes这样检查: % 检查数据的类型是double且单增 ... validateattributes(xgrid,{'double'},{'increasing'}) ...validateattributes最少需要三个参数,如果我们只需要检查变量的类型,则第三个参数可以用空的元胞数组来代替。比如写一个阶乘的函数,其输入必须是无符号的整数,除此之外不做之外的其他检查,可以这样写: % 第三个属性参数为空 ... validateattributes(iA,{'uint8'},{});数据类型还可以是自定义的MATLAB类,比如下面一个简单的类MyClass % MyClass classdef MyClass properties myprop end end如果要规定一个函数的输入是该类的对象, 可以这样写 % 要求变量obj是MyClass类的对象且非空 ... validateattributes(obj,{'MyClass'},{nonempty}); ... validateattributes的额外提示信息在 为什么要对函数的输入进行检查中我们提到,一个友好的API在用户输入出错时,应该提供清晰的诊断信息。以下面这个计算面积的API为例,它接受两个输入,分别是宽和高,计算就是把两者相乘返回:% 一个简化的计算面积的函数 funciton A = getArea(width,height) A = width*height; end显然,输入width和height必须是大于零的数值,所以我们先在函数中添加上validateattributes的基本调用形式: function A = getArea(width,height) validateattributes(width,{'numeric'},{'positive'}); validateattributes(height,{'numeric'},{'positive'}); A = width*height; end作为测试我们首先要试验该函数的各种合法输入(Positive Test),并且观察结果是否正确;然后还要测试非法的输入(Negative Test),验证函数确实能捕捉到错误,并且给出正确的诊断信息: % 命令行测试函数功能 >> getArea(10,22) ans = 220 >> getArea(10,0) % 如预期捕捉到了错误 Error using getArea (line 3) Expected input to be positive. >> getArea(0,22) Error using getArea (line 2) % 两个错误信息除了行号,都是一样的 Expected input to be positive.到这里我们发现,当第一个参数或者第二个参数不符合规定时,函数确实可以捕捉到错误, 但是提示的错误信息除了行号几乎是一样的(当然我们可以利用行号去检查getArea函数内部,然后发现到底是哪一个参数输入错误了。) ,检查错误还是有些不方便。这里我们可以使用validateattributes它的调用方法,能够提示更清晰的诊断信息,如下所示: % validateattributes支持额外的诊断信息 function A = getArea(width,height) validateattributes(width, {'numeric'},{'positive'},'getArea','width' ,1); validateattributes(height,{'numeric'},{'positive'},'getArea','height',2); A = width*height; %参数4 参数5 参数6 end其中第4个参数通常提供validateattributes所在函数的名称,第5个参数通常是输入参数的名称,第6个参数表示该参数在整个参数列表中的位置,这样错误的诊断信息就清晰了: >> getArea(10,0) Error using getArea Expected input number 2, height, to be positive. 清楚的说明getArea函数的 Error in getArea (line 3) 第2个参数不符合规定 validateattributes(height,{'numeric'},{'positive'},'getArea','height',2); >> getArea(0,22) Error using getArea Expected input number 1, width, to be positive. Error in getArea (line 2) validateattributes(width,{'numeric'},{'positive'},'getArea','width',1);总结一下,validateattributes一共支持5种格式,其中后4种支持输出额外的错误诊断信息,这节演示的是第5种,一共有6个参数的格式。 % 一共5种调用方式 validateattributes(A,classes,attributes) validateattributes(A,classes,attributes,argIndex) validateattributes(A,classes,attributes,funcName) validateattributes(A,classes,attributes,funcName,varName) validateattributes(A,classes,attributes,funcName,varName,argIndex) validateattributes支持的检查类型和属性validateattributes可以检查的数据类型:'single','double','int8','int16','int32','int64','uint8','uint16','uint32','uint64','logical','char','struct','cell','function handle','numeric','class name'. validateattributes可以检查的数据维度属性如下:
validateattributes支持检查的数据的大小范围属性如下:
validateattributes还支持检查的数据其它属性如下:
validatestring如果要检查的变量恰好是字符串类型,我们可以使用专门做字符串检查的validatestring函数,它接受一个字符串,然后检查该字符串的值是给定的几个可取的值之一。 比如在分析化学计算中,给浓度变量赋值时,我们除了要指定浓度的大小,还要指定单位,我们暂时用字符concentrationUnit来代表浓度(以后还会提到,利用面向对象编程,我们有其它的方式来模拟数值计算中的单位甚至量纲) ,如果我们要限制字符串变量concentrationUnit只取ppm (Parts Per Million)或者ppb (Parts Per Billion), 可以这样使用validatestring:
% validatestring基本用法 ... str = validatestring(concentrationUnit,{'ppm','ppb'}); ...其中第一个参数concentrationUnit是要检查的字符串变量,第二个参数是由所有可取的值构成的元胞字符数组,如果变量concentrationUnit满足条件,那么该调用返回的str是匹配到的字符串. % command line >> concentrationUnit= 'ppm'; >> str = validatestring(concentrationUnit,{'ppm','ppb'}); str = ppm % concentrationUnit匹配了ppm如果输入的字符变量不匹配字符串元胞中的任何一个,validateattributes将报错,比如: % command line >> concentrationUnit= 'pp'; >> str = validatestring(concentrationUnit,{'ppm','ppb'}); Error Expected input to match one of these strings: 'ppm', 'ppb' The input, pp, matched more than one valid string.和许多MATLAB函数一样,validatestring也支持部分名称(Inexact Name)(不分大小写的部分名称) 。比如我们要验证colorValue字符串只能取red,green,blue,cyan,yellow,magenta这么几个值,validatestring除了接受全名 % 输入是全名 >> colorValue = 'green'; >> str = validatestring(colorValue,{'red','green','blue','cyan','yellow','magenta'}) str = green还可以接受不会模棱两可的部分名字,比如 % 输入的名字是Inexact Name >> colorValue = 'G'; >> str = validatestring(colorValue,{'red','green','blue','cyan','yellow','magenta'}) str = green % G 匹配了green如果给出的部分名字(Inexact Name)有多于一个的匹配,validatestring则报错 % 匹配必须是独一无二的 >> in = 'color'; >> str = validatestring(in,{'ColorMap','ColorSpace'}) Expected input to match one of these strings: 'ColorMap', 'ColorSpace' %color两个都可以匹配 The input, color, matched more than one valid string. inputParser的基本使用前节所介绍的validateattributes和validatestring是用来验证单个参数的,当一个函数有多个参数,并且允许取默认值时,各种情况的组合就变得复杂起来了,我们可以使用inputParser类来对输入进行解析和检查。下面的几节中,我们将通过不断改进一个求面积的getArea函数,来讲解inputParser的用法。首先,该函数的基本形式是接受宽长两个参数,返回两者的乘积: <% getArea的基本形式 function a = getArea(wd,ht) a = wd*ht; end我们先用inputParser的基本形式来对函数的两个输入进行解析和检查 function a = getArea(wd,ht) p = inputParser; p.addRequired('width', @isnumeric); % 检查输入必须是数值型的 p.addRequired('height',@isnumeric); p.parse(wd,ht); a = p.Results.width*p.Results.height; % 从Results处取结果 end下面在命令行尝试该函数的各种输入,并且检查结果: % 命令行验证 >> getArea(10,22) ans = 220 >> getArea(10) % 如预期报错 调用少一个参数 Error using getArea Not enough input arguments. >> getArea('10',22) % 如预期报错 参数width类型错误 Error using getArea (line 8) The value of 'width' is invalid. It must satisfy the function: isnumeric.下面解释getArea中代码,使用inputParser分成4步:
![]() inputParser的可选参数和默认参数值设置在上个版本的函数中,宽和长都是必要的参数,如果只输入一个值,inputParser将提示输入的数目不够>> getArea(10) Error using getArea (line 8) Not enough input arguments.现在我们希望getArea函数能处理单个参数的情况,比如当计算一个正方形的面积,其实只需要输入一个边长的值就可以了,不需要在重复输入另一个边的数值。也就是说,如果只有一个输入时,函数应该默认我们要计算的是一个正方形的面积,并且把长度取默认的值,即输入的宽度。这要用到inputParser的另一个成员函数,叫做addOptional,示例如下: function a = getArea(width,varargin) p = inputParser; p.addRequired('width',@isnumeric); defaultheight = width; %取默认值为输入的width p.addOptional('height',defaultheight,@isnumeric) %添加height作为可选参数 p.parse(width,varargin{:}); a = p.Results.width*p.Results.height; end这个版本的getArea的语法要点如下:
% 命令行测试函数功能 >> getArea(10) % 正确处理的了单个参数的情况 ans = 100 >> getArea(10,22) % 确保仍然可以处理两个参数的情况 ans = 220 inputParser和validateattributes联合使用inputParser的主要功能是对多个输入参数的解析,其对每个参数的值的检查可以使用匿名函数, 而检查参数的值正是我们前面介绍的validateattributes和validatestring函数的强项,这节中我们把inputParser和validateattributes联合起来使用。% getArea版本2 function a = getArea(width,varargin) p = inputParser; p.addRequired('width',@(x)validateattributes(x,{'numeric'},... {'nonzero'},'getArea','width',1)); defaultheight = width; p.addOptional('height',defaultheight,@(x)validateattributes(x,{'numeric'},... {'nonzero'},'getArea','height',2)); p.parse(width,varargin{:}); % 注意要把varargin元胞中的内容解开提供给parse函数 a = p.Results.width*p.Results.height; end其中validateattributes使用了validateattributes带额外参数的调用格式。如果调用出错,会提示额外诊断信息。 下面在命令行尝试该函数的各种输入,并且检查结果: % 命令行测试函数功能 >> getArea(10,0) % 如预期检查出第二个参数的错误,并给出提示 Error using getArea (line 37) The value of 'height' is invalid. Expected input number 2, height, to be nonzero. >> getArea(0,22) % 如预期检查出第一个参数的错误,并给出提示 Error using getArea (line 37) The value of 'width' is invalid. Expected input number 1, width, to be nonzero. inputParser的参数名参数值对的设置假设我们还要再给getArea函数添加两个可缺省的参数,它们将作为结果的一部分返回
% getArea版本3 function r = getArea(width,varargin) p = inputParser; p.addRequired('width',@(x)validateattributes(x,{'numeric'},... {'nonzero'})); defaultheight = width; p.addOptional('height',defaultheight,@(x)validateattributes(x,{'numeric'},... {'nonzero'})); defaultshape = 'rectangle'; p.addOptional('shape',defaultshape,... @(x)any(validatestring(x,{'square','rectangle','paralelogram'}))); defaultunit = 'inches'; p.addOptional('units',defaultunit,... @(x)any(validatestring(x,{'inches','cm','m'}))); p.parse(width,varargin{:}); r.area = p.Results.width*p.Results.height; r.shape = p.Results.shape; %简单起见,shape和unit作为结构体的中的一部分返回 r.units = p.Results.units; end该函数接受如下几种输入,函数的返回值是一个结构体。 % 命令行测试函数功能 >> getArea(10,22,'square') % 只提供shape ans = area: 220 units: 'inches' % units取默认值 shape: 'square' >> getArea(10,22,'square','cm') ans = area: 220 units: 'cm' shape: 'square'这样的设计有2个缺点: (1) 必须得记住第三个和第四参数的顺序,即第三个参数必须是shape,第四参数必须是unit,如果颠倒了inputParser会报错 >> getArea(10,22,'cm','square') % 颠倒了第三和第四个参数 Error using getArea The value of 'shape' is invalid. Expected input to match one of these strings: 'square', 'rectangle', 'paralelogram' The input, 'cm', did not match any of the valid strings(2)如要想给第四个参数提供任何值,必须指定第三个参数的值,尽管第三个参数的值有可能是默认值: >> getArea(10,22,'rectangle','inches') ans = %^该值等于默认值 area: 220 units: 'inches' shape: 'rectangle'这里其实第三个参数没有必要提供,以为它等于默认值。归根结底,这是因为两个参数的顺序相对固定,无法更换。 MATLAB的许多函数都不需要记住参数的输入顺序,比如plot函数: x = 0:pi/10:pi; y = sin(x) ; plot(x,y,'color','g', 'LineWidth',2,'MarkerSize',10);我们可以随意打乱plot的x,y后面的三组参数的顺序,仍然产生同样的图像 plot(x,y,'LineWidth',2,'MarkerSize',10,'color','g');inputParser中的addParameter成员函数就是用来提供这种功能的,它的使用addOptional几乎是一致的 % getArea版本3:把之前的addOptional都换成addParameter function a = getArea(width,varargin) ..... p.addParameter('shape',defaultshape,... @(x)any(validatestring(x,{'square','rectangle','paralelogram'}))); .... p.addParameter('units',defaultunit,... @(x)any(validatestring(x,{'inches','cm','m'}))); .... endaddParameter和addOptional的区别是输入的时候,通过addParameter指定的参数必须通过name-value对的形式来赋值。正是因为我们必须指定参数的名称,所以才能自由的变换参数的位置: % 命令行测试函数功能 >> getArea(10,22,'shape','square','units','m') ans = %--name value --name value area: 220 shape: 'square' units: 'm' >> getArea(10,22,'units','m','shape','square') % 变化了参数的位置 ans = area: 220 shape: 'square' units: 'm' >> getArea(10,22,'units','m') % 仅仅提供unit参数 ans = area: 220 shape: 'rectangle' units: 'm' inputParser解析结构体输入最后顺便提一下,inputParser还可以对结构体的输入进行解析和检查。比如我们要给一个优化函数提供一些运行参数,这些信息可以通过一个configStruct结构体变量传给函数,该结构中包括MaxIter,Tol,StepSize。 在优化函数中,这些计算参数都有各自的默认值,但也可以通过外部指定来重置,这个函数可以这样设计:% inputParser也可以用来解析结构体 function runProgram(configStruct) p = inputParser; DefaultMaxIter = 100 ; % 计算参数的默认值 DefaultTol = 0.001; DefaultStepSize = 0.01 ; p.addParameter('MaxIter',DefaultMaxIter, @(x)validateattributes(x,{'numeric'},{'>',0,'real'})); %迭代次数下限 p.addParameter('Tol',DefaultTol, @(x)validateattributes(x,{'numeric'},{'<=',0.01,'real'})); %收敛上限 p.addParameter('StepSize',DefaultStepSize, @(x)validateattributes(x,{'numeric'},{'<=',0.01,'real'})); %步长上限 p.parse(configStruct); ..... end我们可以这样在命令行中验证 % 命令行测试函数功能 >> configStruct.MaxIter = 10; >> configStruct.Tol = 0.001; >> configStruct.StepSize = 0.01; >> runProgram(configStruct); >> configStruct.MaxIter = 10; >> configStruct.Tol = 0.001; >>runProgram(configStruct); 引子:为什么需要MATLAB的单元测试系统前面几节在介绍inputParser类时,我们通过不断的改进getArea函数,使其最终变得更加的友好和完善,我们之前工作流程大致可以概括如下:![]() ![]() ![]() 关于作者oopmatlab,计算物理博士,计算机硕士,《MATLAB面向对象编程-从入门到设计模式》作者,现就职一家科学工程计算公司的任架构组C++软件工程师。业余兴趣包括如何把软件工程中的现代手段,应用到科学和工程计算当中去,来更好的解决复杂的问题。包括如何从整体上设计科学计算程序的结构;如何让程序便于扩展和修改;在改进和开发算法的时候,如何保证程序已有的功能没有收到影响;如何让算法开发和测试系统的建立齐头并进。声明: 本文内容所有内容仅代表个人观点,如有任何问题,请联系作者。 本版块所有文章版权归作者个人所有,未经允许,不得作为出版物出版。如需转载,请联系论坛管理员。 |