標籤:

對函數的輸入進行檢查和解析

本文所有內容僅代表本人觀點,和MathWorks無關

目錄:

  • 為什麼要對函數的輸入進行檢查
  • validateattributes的基本使用
  • validateattributes的額外提示信息
  • validateattributes支持的檢查類型和屬性
  • validatestring
  • inputParser的基本使用
  • inputParser的可選參數和默認參數值設置
  • inputParser和validateattributes聯合使用
  • inputParser的參數名參數值對的設置
  • inputParser解析結構體輸入
  • 引子:為什麼需要MATLAB的單元測試系統

為什麼要對函數的輸入進行檢查

在工程計算中,如果一個函數的輸入有錯誤,我們總是希望能夠儘早的通過對輸入的檢查,捕捉到這些錯誤,並及時終止程序。這樣做的原因是,如果等到程序運行時出錯或者運行結束後計算結果出錯再查找,那就很遲了,而且通常debug的成本很高。在多人合作的項目中,如果一個開發人員提供了一個公用的API(應用程序介面)給別人使用,除了要提供說明文檔規定輸入的格式之外,API內部通常還需要對輸入進行徹底的檢查,因為開發人員不能保證每個使用者都會仔細地讀文檔,並且每次都能提供符合規定的數據,作為一個友好的API,一旦輸入出了錯,API應該及時提示用戶,並且幫助診斷錯誤原因。同理,這樣做的原因是,如果要等到程序運行時出錯或者運行結束後計算結果出錯,不但成本高,而且使用者也許根本無法查出錯誤的原因。

在MATLAB中,我們可以使用MATLAB提供的專門的函數validateattributes,validatestring和inputParser類來對輸入進行檢查。它們提供全面的檢查功能和清晰的錯誤提示,是全套的參數檢查解析方案。

validateattributes的基本使用

先介紹validateattributes的基本使用。假設在圖像處理計算中,我們設計了一個函數叫做processImg ,用來對一張大小是500 x 500 的灰值圖像進行處理,計算之前我們需要檢查輸入是否符合規定,這可以使用validateattributes函數來完成:

% 函數一開始檢查輸入變數的類型和尺寸nfunction processImg(img)n ...n validateattributes(img,{numeric},{size,[500,500]});n ... % 函數繼續nendn

validateattributes的第一個參數img是輸入的圖像,即要檢查的變數;第二個參數是要檢查的類型,這裡規定img必須是數值類型(numeric);第三個參數對變數要檢查的屬性,這裡的屬性是對img規定的尺寸。

validateattributes的最基本調用格式是:

validateattributes(A,classes,attributes)n

其中classes和attributes通過元胞數組來指定,並且元胞中可以包括多個要檢查的類型和屬性,比如我們除了要檢查圖像的尺寸,還要檢查該圖像矩陣的值都在0到255之間,可以這樣寫:

%元胞數組中可以放置多個要檢查的屬性n...nvalidateattributes(img,{numeric},{size,[500,500],>=,0,<=,255});n...n

在這個例子中,要驗證的類型是numeric,它是一個各種數值類型的集合,包括int8, int16, int32, int64, uint8, uint16, uint32, uint64,single,和double類型。當然我們可以讓類型檢查再具體一點,比如做數值積分的時候,我們通常要提供一個積分的網格,比如一維積分中的X軸,而且通常需要保證該x軸格點的值的類型是double,並是單調遞增的,可以用validateattributes這樣檢查:

% 檢查數據的類型是double且單增n...nvalidateattributes(xgrid,{double},{increasing})n...n

validateattributes最少需要三個參數,如果我們只需要檢查變數的類型,則第三個參數可以用空的元胞數組來代替。比如寫一個階乘的函數,其輸入必須是無符號的整數,除此之外不做之外的其他檢查,可以這樣寫:

% 第三個屬性參數為空n...nvalidateattributes(iA,{uint8},{});n

數據類型還可以是自定義的MATLAB類,比如下面一個簡單的類MyClass

% MyClassnclassdef MyClassn propertiesn mypropn endnendn

如果要規定一個函數的輸入是該類的對象, 可以這樣寫

% 要求變數obj是MyClass類的對象且非空n...nvalidateattributes(obj,{MyClass},{nonempty});n...n

validateattributes的額外提示信息

前面我們提到,一個友好的API在用戶輸入出錯時,應該提供清晰的診斷信息。以下面這個計算面積的API為例,它接受兩個輸入,分別是寬和高,計算就是把兩者相乘返回:

% 一個簡化的計算面積的函數nfunction A = getArea(width,height)n A = width*height;nendn

顯然,輸入width和height必須是大於零的數值,所以我們先在函數中添加上validateattributes的基本調用形式:

function A = getArea(width,height)nvalidateattributes(width,{numeric},{positive});nvalidateattributes(height,{numeric},{positive});nA = width*height;nendn

作為測試我們首先要試驗該函數的各種合法輸入(Positive Test),並且觀察結果是否正確;然後還要測試非法的輸入(Negative Test),驗證函數確實能捕捉到錯誤,並且給出正確的診斷信息:

% 命令行測試函數功能n>> getArea(10,22)n ans =n 220nn>> getArea(10,0) % 如預期捕捉到了錯誤n Error using getArea (line 3)n Expected input to be positive.nn>> getArea(0,22)n Error using getArea (line 2) % 兩個錯誤信息除了行號,都是一樣的n Expected input to be positive.n

到這裡我們發現,當第一個參數或者第二個參數不符合規定時,函數確實可以捕捉到錯誤,

但是提示的錯誤信息除了行號幾乎是一樣的(當然我們可以利用行號去檢查getArea函數內部,然後發現到底是哪一個參數輸入錯誤了。) ,檢查錯誤還是有些不方便。這裡我們可以使用validateattributes它的調用方法,能夠提示更清晰的診斷信息,如下所示:

% validateattributes支持額外的診斷信息nfunction A = getArea(width,height)n validateattributes(width, {numeric},{positive},getArea,width ,1);n validateattributes(height,{numeric} {positive},getArea,height,2);nA = width*height; %參數4 參數5 參數6nendn

其中第4個參數通常提供validateattributes所在函數的名稱,第5個參數通常是輸入參數的名稱,第6個參數表示該參數在整個參數列表中的位置,這樣錯誤的診斷信息就清晰了:

>> getArea(10,0)nError using getAreanExpected input number 2, height, to be positive. 清楚的說明getArea函數的nError in getArea (line 3) 第2個參數不符合規定nvalidateattributes(height,{numeric},{positive},getArea,height,2);nn>> getArea(0,22)nError using getAreanExpected input number 1, width, to be positive.nError in getArea (line 2)nvalidateattributes(width,{numeric},{positive},getArea,width,1);n

總結一下,validateattributes一共支持5種格式,其中後4種支持輸出額外的錯誤診斷信息,這節演示的是第5種,一共有6個參數的格式。

% 一共5種調用方式nvalidateattributes(A,classes,attributes)nvalidateattributes(A,classes,attributes,argIndex)nvalidateattributes(A,classes,attributes,funcName)nvalidateattributes(A,classes,attributes,funcName,varName)nvalidateattributes(A,classes,attributes,funcName,varName,argIndex)n

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基本用法n...nstr = validatestring(concentrationUnit,{ppm,ppb});n...n

其中第一個參數concentrationUnit是要檢查的字元串變數,第二個參數是由所有可取的值構成的元胞字元數組,如果變數concentrationUnit滿足條件,那麼該調用返回的str是匹配到的字元串.

% command linen>> concentrationUnit= ppm;n>> str = validatestring(concentrationUnit,{ppm,ppb});n str =n ppm % concentrationUnit匹配了ppmn

如果輸入的字元變數不匹配字元串元胞中的任何一個,validateattributes將報錯,比如:

% command linen>> concentrationUnit= pp;n>> str = validatestring(concentrationUnit,{ppm,ppb});n Errorn Expected input to match one of these strings:n ppm, ppbn The input, pp, matched more than one valid string.n

和許多MATLAB函數一樣,validatestring也支持部分名稱(Inexact Name)(不分大小寫的部分名稱) 。比如我們要驗證colorValue字元串只能取red,green,blue,cyan,yellow,magenta這麼幾個值,validatestring除了接受全名

% 輸入是全名n>> colorValue = green;n>> str = validatestring(colorValue, {red,green,blue,cyan,yellow,magenta})n str =n greenn

還可以接受不會模稜兩可的部分名字,比如

% 輸入的名字是Inexact Namen>> colorValue = G;n>> str = validatestring(colorValue,{red,green,blue,cyan,yellow,magenta})nstr =n green % G 匹配了greenn

如果給出的部分名字(Inexact Name)有多於一個的匹配,validatestring則報錯

% 匹配必須是獨一無二的n>> in = color;n>> str = validatestring(in,{ColorMap,ColorSpace})n Expected input to match one of these strings:n ColorMap, ColorSpace %color兩個都可以匹配n The input, color, matched more than one valid string.n

inputParser的基本使用

前節所介紹的validateattributes和validatestring是用來驗證單個參數的,當一個函數有多個參數,並且允許取默認值時,各種情況的組合就變得複雜起來了,我們可以使用inputParser類來對輸入進行解析和檢查。下面的幾節中,我們將通過不斷改進一個求面積的getArea函數,來講解inputParser的用法。首先,該函數的基本形式是接受寬長兩個參數,返回兩者的乘積:

% getArea的基本形式nfunction a = getArea(wd,ht)n a = wd*ht;nendn

我們先用inputParser的基本形式來對函數的兩個輸入進行解析和檢查

% getArea版本1 nfunction a = getArea(wd,ht)nn p = inputParser;nn p.addRequired(width, @isnumeric); % 檢查輸入必須是數值型的n p.addRequired(height,@isnumeric);nn p.parse(wd,ht);nn a = p.Results.width*p.Results.height; % 從Results處取結果nendn

下面在命令行嘗試該函數的各種輸入,並且檢查結果:

% 命令行驗證n>> getArea(10,22)nans =n 220nn>> getArea(10) % 如預期報錯 調用少一個參數nError using getArean Not enough input arguments.nn>> getArea(10,22) % 如預期報錯 參數width類型錯誤nError using getArea (line 8)n The value of width is invalid. It must satisfy the function: isnumeric.n

下面解釋getArea中代碼,使用inputParser分成4步

  1. 首先第3行聲明一個inputParser的對象,等式右邊是inputParser的類名稱,也是該類的構造函數。
  2. 第5,6行給Parser對象添加要解析的參數,其中addRequired 是inputParser的一個成員函數。 這裡我們添加了兩個要解析的參數,名稱分別叫做width和height。這些名稱和getArea的輸入的實參有順序上的對應關係,但是名稱並不一定要完全一樣。
  3. 第8行把函數的實參wd,ht提供給inputParser對象,並且進行解析,解析的內容將存放在p.Results中。
  4. 第10行從p.Results中取出解析的結果,計算面積並返回。

inputParser是一個MATLAB類,其UML類圖如下:

Figure.5, inputParser類圖

這節中我們介紹了addRequired成員方法,下面幾節中我們將介紹另外兩個成員方法addOptional和addParameter.

inputParser的可選參數和默認參數值設置

在上個版本的函數中,寬和長都是必要的參數,如果只輸入一個值,inputParser將提示輸入的數目不夠

>> getArea(10)nError using getArea (line 8)nNot enough input arguments.n

現在我們希望getArea函數能處理單個參數的情況,比如當計算一個正方形的面積,其實只需要輸入一個邊長的值就可以了,不需要在重複輸入另一個邊的數值。也就是說,如果只有一個輸入時,函數應該默認我們要計算的是一個正方形的面積,並且把長度取默認的值,即輸入的寬度。這要用到inputParser的另一個成員函數,叫做addOptional,示例如下:

% getArea版本2 nfunction a = getArea(width,varargin)nn p = inputParser;n p.addRequired(width,@isnumeric);nnn defaultheight = width; %取默認值為輸入的widthn p.addOptional(height,defaultheight,@isnumeric) %添加height為可選參數nn p.parse(width,varargin{:});nn a = p.Results.width*p.Results.height;nendn

這個版本的getArea的語法要點如下:

  1. 第1行中的參數被分成了兩個部分,第一個輸入width和其餘的部分,其餘部分的參數被包裝在了元胞數組中,後面還會看到更多這樣的例子。
  2. 第7行指定了可選參數的默認值。
  3. 第8行給inputParser添加了height作為可選參數

下面在命令行嘗試該函數的各種輸入,並且檢查結果:

% 命令行測試函數功能n>> getArea(10) % 正確處理的了單個參數的情況nans =n100nn>> getArea(10,22) % 確保仍然可以處理兩個參數的情況nans =n220n

inputParser和validateattributes聯合使用

inputParser的主要功能是對多個輸入參數的解析,其對每個參數的值的檢查可以使用匿名函數,

而檢查參數的值正是我們前面介紹的validateattributes和validatestring函數的強項,這節中我們把inputParser和validateattributes聯合起來使用。

% getArea版本2nfunction a = getArea(width,varargin)nn p = inputParser;n p.addRequired(width,@(x)validateattributes(x,{numeric},...n {nonzero},getArea,width,1));nnn defaultheight = width;n p.addOptional(height,defaultheight,@(x)validateattributes(x, {numeric},...n {nonzero},getArea,height,2));nn p.parse(width,varargin{:}); n % 注意要把varargin元胞中的內容解開提供給parse函數nn a = p.Results.width*p.Results.height;nendn

其中validateattributes使用了validateattributes帶額外參數的調用格式。如果調用出錯,會提示額外診斷信息。

下面在命令行嘗試該函數的各種輸入,並且檢查結果:

% 命令行測試函數功能n>> getArea(10,0) % 如預期檢查出第二個參數的錯誤,並給出提示nError using getArea (line 37)nThe value of height is invalid. Expected input number 2, height, to be nonzero.nn>> getArea(0,22) % 如預期檢查出第一個參數的錯誤,並給出提示nError using getArea (line 37)nThe value of width is invalid. Expected input number 1, width, to be nonzero.n

inputParser的參數名參數值對的設置

假設我們還要再給getArea函數添加兩個可預設的參數,它們將作為結果的一部分返回

  • 一個叫做shape,用來表示形狀,可取的值是rectangle,square和paralelogram. 其默認值是rectangle。
  • 另一個叫做unit,用來表示輸入的單位,可取的值是cm,m,inches,其默認值是inches

在上節的基礎上,可以再加入兩個addOptional的調用

% getArea版本3nfunction r = getArea(width,varargin)nn p = inputParser;n p.addRequired(width,@(x)validateattributes(x,{numeric},...n {nonzero}));nnn defaultheight = width;n p.addOptional(height,defaultheight,@(x)validateattributes(x, {numeric},...n {nonzero}));nn defaultshape = rectangle;n p.addOptional(shape,defaultshape,...n @(x)any(validatestring(x,{square,rectangle,paralelogram})));nn defaultunit = inches;n p.addOptional(units,defaultunit,...n @(x)any(validatestring(x,{inches,cm,m})));nn p.parse(width,varargin{:});nn r.area = p.Results.width*p.Results.height;n r.shape = p.Results.shape; %簡單起見,shape和unit作為結構體的中的一部分返回n r.units = p.Results.units;nendn

該函數接受如下幾種輸入,函數的返回值是一個結構體。

% 命令行測試函數功能n>> getArea(10,22,square) % 只提供shapenans =nnarea: 220nunits: inches % units取默認值nshape: squarenn>> getArea(10,22,square,cm)nans =nnarea: 220nunits: cmnshape: squaren

這樣的設計有2個缺點: (1) 必須得記住第三個和第四參數的順序,即第三個參數必須是shape,第四參數必須是unit,如果顛倒了inputParser會報錯

>> getArea(10,22,cm,square) % 顛倒了第三和第四個參數nError using getAreanThe value of shape is invalid. Expected input to match one of these strings:nsquare, rectangle, paralelogramnThe input, cm, did not match any of the valid stringsn

(2)如要想給第四個參數提供任何值,必須指定第三個參數的值,儘管第三個參數的值有可能是默認值:

>> getArea(10,22,rectangle,inches)nans = %^該值等於默認值nnarea: 220nunits: inchesnshape: rectanglen

這裡其實第三個參數沒有必要提供,以為它等於默認值。歸根結底,這是因為兩個參數的順序相對固定,無法更換。

MATLAB的許多函數都不需要記住參數的輸入順序,比如plot函數:

x = 0:pi/10:pi;ny = sin(x) ;nplot(x,y,color,g, LineWidth,2,MarkerSize,10);n

我們可以隨意打亂plot的x,y後面的三組參數的順序,仍然產生同樣的圖像

plot(x,y,LineWidth,2,MarkerSize,10,color,g);n

inputParser中的addParameter成員函數就是用來提供這種功能的,它的使用addOptional幾乎是一致的

% getArea版本3:把之前的addOptional都換成addParameternfunction a = getArea(width,varargin)nn .....n p.addParameter(shape,defaultshape,...n @(x)any(validatestring(x,{square,rectangle,paralelogram})));nn ....n p.addParameter(units,defaultunit,...n @(x)any(validatestring(x,{inches,cm,m})));n ....nendn

addParameter和addOptional的區別是輸入的時候,通過addParameter指定的參數必須通過name-value對的形式來賦值。正是因為我們必須指定參數的名稱,所以才能自由的變換參數的位置:

% 命令行測試函數功能n>> getArea(10,22,shape,square,units,m)nans = %--name value --name valuenarea: 220nshape: squarenunits: mnn>> getArea(10,22,units,m,shape,square) % 變化了參數的位置nans =narea: 220nshape: squarenunits: mnnn>> getArea(10,22,units,m) % 僅僅提供unit參數nans =narea: 220nshape: rectanglenunits: mn

inputParser解析結構體輸入

最後順便提一下,inputParser還可以對結構體的輸入進行解析和檢查。比如我們要給一個優化函數提供一些運行參數,這些信息可以通過一個configStruct結構體變數傳給函數,該結構中包括MaxIter,Tol,StepSize。 在優化函數中,這些計算參數都有各自的默認值,但也可以通過外部指定來重置,這個函數可以這樣設計:

% inputParser也可以用來解析結構體nfunction runProgram(configStruct)nn p = inputParser;nn DefaultMaxIter = 100 ; % 計算參數的默認值n DefaultTol = 0.001;n DefaultStepSize = 0.01 ;nnn p.addParameter(MaxIter,DefaultMaxIter,n @(x)validateattributes(x,{numeric},{>,0,real}));n %迭代次數下限n p.addParameter(Tol,DefaultTol,n @(x)validateattributes(x,{numeric},{<=,0.01,real}));n %收斂上限n p.addParameter(StepSize,DefaultStepSize,n @(x)validateattributes(x,{numeric},{<=,0.01,real}));n %步長上限n p.parse(configStruct);nn .....nendn

我們可以這樣在命令行中驗證

% 命令行測試函數功能n>> configStruct.MaxIter = 10;n>> configStruct.Tol = 0.001;n>> configStruct.StepSize = 0.01;n>> runProgram(configStruct);nnn>> configStruct.MaxIter = 10;n>> configStruct.Tol = 0.001;n>>runProgram(configStruct);n

引子:為什麼需要MATLAB的單元測試系統

前面幾節在介紹inputParser類時,我們通過不斷的改進getArea函數,使其最終變得更加的友好和完善,我們之前工作流程大致可以概括如下:

Figure.6, 函數更新開發流程1

在這個工作流程中,我們除了改進演算法,還在設計完成之後,在命令行中都試了幾種典型的調用方式來驗證新的函數。這也是實際開發中常見的流程:一邊開發一邊驗證結果。但是隨著函數支持越來越多的功能,我們在命令行不但要測試新的調用語法,(包括Positive和Negative的測試)。還要驗證以前的調用仍然可以使用,保證新功能的加入沒有破壞已有的功能。 這是很重要的一個過程,它保證新的函數或演算法是可靠的向後兼容的。所以其實工作流程圖 Section 還要修改,還要添加對新的函數進行舊的測試,所以更完善可靠的工作流程應該是圖 Section ,每次都要把已經有的測試都檢驗一遍

Figure.7, 函數更新開發流程2

總結下來,實踐中在改進函數和增加新功能的同時,我們需要在添加新的測試的同時,

不斷的重複已有測試。這些測試包括正向測試(Positive Test),也包括錯誤測試(Negative Test)。顯然在命令行中不停的重複這樣的工作效率很低,那麼很自然的問題就是這些測試該如何的組織。

非常直覺的方法是,我們可以把這些測試放到一個測試腳本文件中,每次給函數或者計算增加新功能的時候就運行一遍這個腳本,保證結果沒有變化,如果有變化,按實際情況修改函數或者修改測試。添加新功能的時候也要往這個腳本中添加新的測試。基本的工作流程應該如下:

Figure.8 函數和函數測試共生的模式

在這個測試模塊中,不但要包括正向測試,還要包括錯誤測試情況,即要保證函數能夠如預期的處理非法輸入,拋出錯誤,一個簡單的原始的方法是使用try catch。

測試模塊還應該這樣的功能:比如測試腳本中有10個測試點,如果第二個測試錯誤就退出了,那麼這個腳本的運行也就結束了,直到我們解決了第二個測試點的問題,腳本才能繼續向下運行,最好有這樣一個功能,使得一個測試點的錯誤不影響其它測試點的運行,等到測試結束之後,生成一個報告告訴用戶都是哪幾個測試通過了,哪幾個測試沒有通過,這樣方便用戶一次性解決所有的問題。

最後這節闡釋了在一個可靠的科學工程計算中為什麼需要一個測試模塊,並且一個測試模塊該滿足哪些基本的要求。其實這裡討論的功能和工作流程,正是MATLAB的單元測試所提供的解決方案。MATLAB的單元測試系統是任何一個大型的MATLAB工程項目中不可缺少的一個組成部分。我們將在後面的章節中詳細介紹。

推薦閱讀:

MATLAB高級數據結構連載5: table 3
Matlab2012a(32/64位)
MATLAB神經網路(四):基於Adaboost的強分類器設計
MATLAB和物聯網連載6: Thingspeak Tutorial 5

TAG:MATLAB |