第4章 大きなプログラムを作る:関数

専修大学人間科学部 情報処理心理学実習 で使用している教科書 『MATLAB プログラミング入門』 の補足説明を行います.

注意:数式が適切に表示されるまで,少し時間がかかるかもしれません.

4.1 関数を作る

4.1.1 平均と分散を求める

テキスト p.107 から p.108 にかけての記述に誤植があります(改訂版第1刷). テキスト p.107 の最終行での [X1, X2](X1, X2) に修正してください. あわせて,p.108 最初の行での "「[」と「]」" という記述を "「(」と「)」" に修正してください.

式 (4.1) では 平均(mean)を表す記号としてギリシャ文字の μ が, 式 (4.1) では 分散(variance)を表す記号としてギリシャ文字の σ 2 が使われています. これらの記号はそれぞれ母集団平均と母集団分散を表すことが一般的なので, データの平均と分散を表すために用いるのはあまりよくありません. データの平均は記号 x _ で,分散は s 2 で表すとよいでしょう.

本章では,すべての処理をひとつのプログラムに書くのではなく, 処理の一部を切り離して別のプログラムとする方法を学習します. メインプログラムが,いくつかのサブプログラムを呼び出し, 特定の処理をそのサブプログラムに任せます. 入力を受け入れ出力を返すサブプログラムを 関数(function)と呼びます. メインプログラムは,必要に応じて,関数が返す出力を利用します.

メインプログラムは スクリプト(script)というタイプのプログラムファイルに記述されます. このファイルを スクリプト M ファイルと呼びます. ここまでにわれわれが書いてきたプログラムはこのタイプです. 多くの場合に,関数もひとつのプログラムファイルに記述されます. このファイルを 関数 M ファイルと呼びます.

関数 M ファイルには関数の定義を記述します. すなわち,その関数が入力として何を受け取り, どのような処理をして,何を出力するかを記述します.

関数 M ファイルを作成するには,スクリプトを書く時と同様に, MATLAB のエディターを使用します. MATLAB の [ホーム] タブの [新規作成]から, [関数] をマウスで左クリックします.

Create Function (menu)

すると,関数を定義するためのフォーマットでエディターが起動します. これまでスクリプトを書くためにエディターを起動したときは, 何も書かれていない空白のフォーマットでした.

Create Function (editor)

関数の定義は function というキーワードで始まります. テキストでの AvaAndVar 関数の定義を見てください. 1行目は関数の定義ステートメントであり,


function Out = AvrAndVae(X)

となっています.ここで,AvrAndVar は関数名, X は関数の 入力引数(input argument), Out は関数の 出力引数(output argument)です. 「引数」は「ひきすう」と読みます.

関数を書くためのフォーマットで MATLAB のエディターを起動すると, 1行目は


function [ output_args ] = Untitled( input_args )

となっています. テキストの関数定義 AvrAndVar の1行目と同じになるように書き換えてください.

入力引数および出力引数には,それぞれわかりやすい名前を用います. 引数は変数なので,アルファベットの文字で始まる必要があります. 2文字目以降に使用できるのは,アルファベット,数字,アンダースコア(_)です. 大文字と小文字は区別されます.

関数名にも,その関数がどのような仕事をするのか想像がつくような, わかりやすい名前を与えます. 名前の規則は引数と同じです.MATLAB で用意されている関数は小文字で始まりますので, ユーザーが自分で作成する関数の名前は大文字で始めるとよいでしょう.

エディターであらかじめ書かれている


%UNTITLED この関数の概要をここに記述
%   詳細説明をここに記述

は注釈です.これは関数のヘルプテキストとなります. 関数が入力として何を受け付け,何を出力するかを書いておくとよいでしょう. MATLAB の関数への入力および出力は,基本的には行列あるいはベクトルです. AvrAndVar 関数では,入力はデータの数値から構成されるベクトルであり, 出力は平均と分散の2つから構成されるベクトルです.

関数の記述は end キーワードで終わります. テキストの AvrAndVar 関数では end キーワードが省略されていますが, 入れた方がよいでしょう. この関数の定義では end キーワードは省略可能ですが, 省略可能でない場合もいくつかあります. 関数を定義するためのフォーマットでエディターを起動すれば, end キーワードはあらかじめ入力されています.

関数の定義を書き終わったら,関数名と同じ名前の M ファイルとして保存してください. たとえば,AvrAndVar 関数のファイル名は AvrAndVar.m となります.

AvrAndVar 関数の関数 M ファイルを保存したら,コマンドウィンドウで


lookfor average

あるいは


lookfor('average')

とキーインしてみてください. この lookfor 関数は, 現在利用可能な関数のプログラムファイル(関数 M ファイル)の H1 行 から,指定された文字列(ここでは average)を検索します, 下図では3つのファイルが見つかり,その H1 行が表示されています.

lookfor average

AvrAndVar というリンクをクリックすると,H1 行を含め, ヘルプテキスト行の全体が表示されます.

lookfor average

同じ情報は,関数のヘルプを表示する help 関数を用いて.


help AvrAndVar

あるいは


help('AvrAndVar')

とキーインしても表示されます.

help for AvrAndVar function

関数本体に書かれている処理を見ておきましょう.

5行目はデータに含まれる数値の数を length 関数で調べ, その数を変数 N に保存しています. この関数は,ベクトルを入力として与えられると,そのベクトルを構成する成分の数を返します. この数は6行目と7行目で平均および分散をそれぞれ計算するときに用いられます.

6行目は入力されたデータの平均値を計算しています. 式 (4.2) を MATLAB のコードに翻訳すると,この行の記述になります. データとして与えられたベクトルに含まれる N 個の数値の合計を sum 関数で求め,それを N で割っています. 得られた平均値は変数 Avr に保存されます.

7行目は入力されたデータの分散を計算しています. 式 (4.3) を MATLAB のコードに翻訳すると,この行の記述になります. 分散の定義式

s 2 = 1 n i = 1 n x i - x _ 2

ではなく,これを変形した

s 2 = 1 n i = 1 n x i 2 - x _ 2

を用いて分散を計算しています. 得られた分散は変数 Var に保存されます.

8行目は出力引数 Out の内容を具体的に与えています. 変数 Avr と Var を成分とするベクトルが Out に代入されます.

AvrAndVar 関数を利用するスクリプトである例題 ex4101b ではデータを変数 X に保存しています. これは AvrAndVar 関数の定義における入力引数 X と同じ名前ですが, 関数への入力とする変数をその関数の定義における引数と一致させる必要はありません. 実際,MATLAB に組み込まれている関数については, われわれはどのような名前の引数が用いられているかを知らないので, 自由に名前を決めた変数を関数への入力としています. 例題 ex4101b の変数 X を他の名前に変えてスクリプトを実行し, まったく問題ないことを確かめてください.

AvrAndVar2 関数の関数 M ファイルは3つの関数を含んでいます. メイン関数であり,ファイル名になっている AvrAndVar2 関数と, ローカル関数 と呼ばれる Average 関数および Variance 関数です. メイン関数は,MATLAB の組み込み関数のように, 他のプログラムから呼び出すことができます. ローカル関数はそれが書かれている関数 M ファイルの内部だけで使用可能です. 他から呼び出すことはできません.

3つの関数が並べられていることを明示するために, 各関数の終端を示す end キーワードを用いて下図のようにプログラムを記述した方がよいでしょう. メイン関数は関数 M ファイルの中で必ず最初に記述します. ローカル関数の記述順序は任意です.

AvrAndVar2 function

メイン関数およびローカル関数の中で使用されている変数は, その関数の外側では使用できないということに注意してください. たとえば,Average 関数の内部で使用されている変数 N は, Variance 関数の中では使用できません. ベクトル X の成分の数を Average 関数の内部で求めていても, それは Variance 関数の中では使用できません. そのため,それぞれの関数の中で N = length(X) という記述が必要となっています. Average 関数の中の N と Variance 関数の中の N は別物です. 同様に,ローカル関数である Variance 関数の中で, 別のローカル関数である Average 関数の中で使用された変数 Avr は使用できません. そのため,Variance 関数での分散の計算では Avr.^2 と書くことはできず. かわりに Average(X).^2 としています. AvrAndVar 関数では分散の計算において変数 Avr を使用できたことと比較してください. 関数の内部だけで使用できる変数を 局所的な変数 と呼びます. 4.2.4 節で学習します

3つの関数から構成されている AvrAndVar2 関数は 入れ子関数(nested function)と呼ばれる書き方で定義することもできます. 入れ子関数は親関数の内部に完全に含まれている関数です. 入れ子関数バージョンの AvrAndVa2 関数は下図のようになります. 親関数は AvrAndVar2 関数であり,その内部に Average 関数と Variance 関数が入れ子にされています.

AvrAndVar2 function (nested version)

入れ子関数は自身の親関数で定義された変数にアクセスできます. そのため,親関数で定義された変数 N は入れ子関数の中で改めて定義することなく使用できます.

4.1.2 エラーに対処する:error

セクションタイトルに誤植があります(改訂版第1刷). テキストの旧版では return ステートメントを扱っていたのですが, 改訂版ではなくなっています. セクションタイトルからも消去する必要がありました.

AvrAndVar3 関数は,最初の AveAndVar 関数に, N が 0(データのベクトル X に何も入っていない)の場合の処理を if 文で加えたものです. プログラムの06行目から08行目以外は AvrAndVar 関数と同じです. if 文に書かれた処理を実行するかどうかを決める条件式を N = 0 と書かないよう注意してください, 等号(=)でなく関係演算子 == です.

組み込み関数 error は,引数に与えられたエラーメッセージを表示し, 現在実行しているプログラムを終了します. エラーメッセージは引用符 ' で囲みます. MATLAB R2017a で例題 ex4102 のプログラムを実行すると以下のような出力となります. データのベクトル X が空なので, AvrAndVar3 関数の7行目に書かれたエラーメッセージが表示されて処理が止まります. 結果として,例題 ex4102 のプログラム2行目の Y = AvrAndVar3(X) がエラーとなったことがわかります.

ex4102 using AvrAndVar3

例題 ex4102 のプログラムで呼び出す関数を AvrAndVar3 から AvrAndVar に変えると, プログラムの実行結果は以下のようになります. 平均と分散の計算は試みられますが,N の値が 0 であるため. 結果は NaN(Not a Number という意味)となっています.

ex4102 using AvrAndVar

4.1.3 引数は値が渡される!?

このセクションは少し難しい内容なので,スキップしてもらってかまいません.

MATLAB に限らず, プログラムが扱うデータはコンピュータのメモリのどこかに記録されています. 土地に住所があるように,メモリにはアドレスがあります.

スクリプトであるメインプログラムは, 何らかの変数に保存されている, 自分が扱うデータのアドレスを知っています. メインプログラムが関数を呼び出し, そのデータを使って何らかの計算を行ってもらうとします. このとき,関数へのデータの渡し方には2つの方法があります. ひとつは,データのあるアドレスを関数に教えて, そのアドレスにあるデータを使って計算を行うよう依頼する方法です. これを アドレス渡し(call by address)と言います. もうひとつは,データをコピーし,コピーしたデータを関数に渡す方法です. これを 値渡し(call by value)と言います.

関数にアドレスを渡す方法では,データをコピーする必要がないという利点がありますが, データを書き換えられてしまうかもしれないという問題があります. 関数にデータのコピーを渡す方法では,コピーに時間とメモリを要しますが, オリジナルのデータを変更される心配はないという利点があります.

値渡しでのコピーに要する時間とメモリを節約するため, MATLAB では巧みにアドレス渡しを用いています. たとえば,以下のスクリプトと関数 Test1 ではアドレス渡しが行われます. よって,変数 A と X は同じアドレスにあるデータを共有します.


A = [1 2 3 4 5];
B = Test1(A)

function Out = Test1(X)
   Out = 2 .* X;
end

得られる結果は同じですが, 以下のスクリプトと関数 Test2 では値渡しが行われます. 関数 Test2 が呼び出されるとき,変数 A に保存されたデータのコピーが作成され, それが変数 X に代入されます. よって,変数 A と X は異なったアドレスにあるデータを用いています.


A = [1 2 3 4 5];
B = Test2(A)

function Out = Test2(X)
   X = 2 .* X;
   Out = X;
end

関数 Test1 と関数 Test2 の違いは,渡されたデータを関数の中で変更するかどうかです. 関数 Tset1 では,変数 X に代入されているデータの数値を2倍しますが, このとき変数 X の内容は変更せず,2倍した結果を変数 Out に代入します. 関数 Tset1 では,変数 X に代入されているデータの数値を2倍して, 変数 X に代入しています.したがって,変数 X の内容は変更されます.

テキストで述べられているように, MATAB では,関数の中で値の変更を受けるような変数については値が渡されるようになっています. データが大きいときにはデータのコピーに時間とメモリをかなり費やしてしまいますから, 渡されたデータを関数の中で変更することは避けて, アドレス渡しが行われるようにした方がよいでしょう.

4.2 引数を操る

4.2.1 複数の出力引数

関数の出力引数が複数の場合,それらをコンマで区切って [ ] の中に並べます. MATLAB のエディターで関数を新規作成すれば, 定義行はあらかじめ


function [ output_args ] = untitled( input_args )

と書かれていますので,output_args を書き換えてください.

AvrAndVar4 関数では,AvrAndVar および AvrAndVar3 関数の最終行にあった,


Out = [Avr Var];

という記述はありません. 出力引数は Avr と Var であり, これらは4行目と5行目でそれぞれ値が決められています.

AvrAndVar4 関数の出力引数が2つなので, これを呼び出すスクリプト ex4201 では2つの変数 Avr および Var を用いて AvrAndVar4 関数からの出力を受け取っています. これらの変数名は AvrAndVar4 関数の出力引数と同じ名前である必要はありません. スクリプトの3行目と4行目を


[Heikin Bunsan] = AvrAndVar4(X);
Heikin, Bunsan

と変えて実行してみてください.

例題 ex4201 のスクリプトは,3行目で AvrAndVar4 関数からの出力を受け取り, 4行目でそれらの値を表示しています. 3行目の最後にあるセミコロンを外せば4行目は不要ですが, 呼び出した関数からの出力を受け取ることと, それを表示することは,区別しておいた方がよいでしょう.

AvrAndVar4 関数から2つの数値が返されるのに, それを受け取るためにスクリプト側(ex4201)で用意した変数がひとつだけであっても, エラーにはなりません. 返された2つの数値のうち,最初のひとつがその変数に保存されます. 以下のスクリプトを試してみてください.


N = 10;
X = 1:N;
Heikin = AvrAndVar4(X);
Heikin

4.2.2 出力引数がないとき

出力引数のない関数は第1章で一度経験しています(ex1701sub).

AvrAndVar5 関数は,データを渡されると,平均値と分散をコマンドウィンドウに表示します. コマンドウィンドウへの文字列の表示には sprintf 関数と disp 関数が使われています. AvrAndVar5 関数は値を返さないので, AvrAndVar5 関数が計算した平均値と分散を呼び出し元のスクリプト(ex4202)で利用することはできません.

AvrAndVar5 関数が計算した平均値と分散の表示に使用されている sprintf 関数は,計算で得られた数値を文字列に埋め込み. 全体を文字列として返す関数です(数値の埋め込みなしでも使えます). 第1の入力引数は文字列です. この文字列で 書式演算子(formatting operator)のある位置に, 2番目以降の入力引数で指定された数値が埋め込まれます. 書式演算子は % 記号で始まります. AvrAndVar5 関数で使われている sprintf 関数では2か所ある %6.3f です. AvrAndVar5 関数では,sprintf 関数が返した文字列は変数 S に代入されます. 変数 S に代入された文字列を disp 関数がコマンドウィンドウに表示しています.

書式演算子は % 記号で始まります. 文字列中で数値をどこに埋め込むかだけでなく, その数値をどのような書式で埋め込むかを示します. AvrAndVar5 関数での sprintf 関数では %6.3f という書式演算子が用いられています. ここで,6 はフィールド幅(field width),3 は精度(precision), f は変換文字(conversion character)です.

フィールド幅とは表示する最低桁数です. 小数点を含め,少なくともここで指定された桁数が表示されます. フィールド幅が 6 であり, 小数点以下を第3位まで表示するならば(これは精度で指定されます), 整数部分は少なくとも10の位までは表示するということになります. 例題 ex4202 では平均値(5.500)および分散(8.250)の整数部分は1桁なので, 10の位は空白となっています.

精度は表示する有効桁数です. 変換文字が f の場合(これは実数の表示を意味します), 精度は小数点以下の桁数となります. 精度が 3 ならば,小数点第3位までが表示されます.

変換文字は出力の表記法を指定します. 変換文字 f は数値を実数で表記(固定小数点表記)します.

文字列を変数 S に一度保存し, それを disp 関数でコマンドウィンドウに表示するという2段階が煩わしければ,


S = sprintf('Average = %6.3f Variance = %6.3f', Avr, Var);
disp(S);

にかえて


fprintf('Average = %6.3f Variance = %6.3f\n', Avr, Var);

と書くこともできます.この fprintf 関数は, テキストファイルにデータを書き込むときに利用される関数ですが(第6章で学習します), コマンドウィンドウへの出力でも利用できます. 出力する文字列の最後についている \n は改行文字です. 改行が行われます.環境により,\ は円マークあるいはバックスラッシュとなります.

4.2.3 入力引数がないとき

入力引数のない関数はこれまでいくつか経験してきました. たとえば,現在の時刻を返す clock 関数(ex3601)や, プログラムの実行時間の計測に用いた tic 関数と toc 関数(ex3301c)があります. これらの関数を呼ぶには,引数を空白にして


clock()

と記述する(あるいは,キーインする)か,


clock

というように関数名だけを記述(キーイン)します.

関数を作成する動機は 「いろいろなデータに対して計算してくれるプログラムがあると便利」(p.106) ということでした. AvrAndVar6 関数は内部で生成しているデータが固定なので, 実際には役に立たない関数ですね. 1から10までの整数の平均値と分散を計算してほしいという状況はほとんどないでしょう. 入力引数のない関数の定義方法を学ぶという学習であるとわりきってください.

4.2.4 出力引数も入力引数もないとき:global

AvrAndVar7 関数は AvrAndVar5 関数と AvrAndVar6 関数の組み合わせなので, 理解は容易でしょう. AvrAndVar6 関数と同様に,内部で生成しているデータが固定なので, 実際には役に立たない関数ですね. 入力および出力引数のない関数の定義方法を学ぶという学習であるとわりきってください.

いくらか実用的な,入出力引数のない関数の例を挙げておきます.


function Hello(  )
Date = fix(clock());
fprintf('こんにちは.今日は%d年%d月%d日です.\n', ...
    Date(1), Date(2), Date(3));
end

この関数はあいさつのメッセージを日付とともに表示します. この関数の使用方法として,たとえば, MATLAB の起動時にこのメッセージをコマンドウィンドウに表示させることができます.

4.1.1 節で,メイン関数およびローカル関数の中で使用されている変数は, その関数の外側では使用できないということを学びました, 関数の内部だけで使用できる変数を 局所的な変数(local variable)と呼びます. これに対して,どの関数からもアクセスできる変数を 大域的な変数(global variable)と呼びます. 大域的な変数を使用するには, 大域的な変数を使う関数すべてにおいて,変数名に global をつけて宣言します.

大域的な変数はむやみに使うべきではありません. ある関数において,大域的な変数の値を変更すると, その影響はその変数にアクセスするすべての関数に及びます. これはプログラムミスの原因となりやすいものです.

4.2.5 入力引数の数を可変にする:nargin, varargin

組み込み関数 nargin の名前は number of input arguments(入力引数の数)という意味です. この関数を入力引数なしで呼び出すと, 現在実行中の関数が呼び出されたときに渡された入力引数の数が返されす. たとえば,AvrAndVar9 関数において使用されている nargin 関数は, AvrAndVar9 関数に与えられた入力引数の数を返します. 関数であることがわかりやすいように, AvrAndVar9 関数の定義の5行目は, 関数名の後に空白の括弧を明示して if nargin() == 1 と書いてもよいでしょう.

組み込み関数 varargin の名前は variable-length input arguments list(可変長入力引数リスト)という意味です. varargin 関数は,関数というよりも,関数定義における入力引数のように使われます. AvrAndVar10 関数の場合,関数定義の入力引数が varargin となっています. AvrAndVar10 関数はデータをまとめたベクトルをいくつでも受け入れます.

入力引数が varargin である関数が実行され,N 個の入力があると, それらは 1 行 N 列の セル配列(cell array)となります. このセル配列は varargin という名前です. セル配列はベクトルや行列に似ていますが, 任意のデータ型を混在させて格納できるという点が異なります. たとえば,ベクトルでは文字の成分と数値の成分を混在させることはできませんが, セル配列では可能です. セル配列の成分の数は length 関数を使って調べることができます(AvrAndVar10 関数の定義の6行目).

セル配列の成分を取り出すには,ベクトルの成分を ( ) で取り出すのとは異なり, { } を使います.たとえば,セル配列 varargin の第1成分は varargin{1} として得られます. 例題 ex4205b のプログラム3行目にある


AvrAndVar10(X, Y, Z);

というコードを実行すると,AvrAndVar10 関数は3つの入力データを


Z = [[] varargin{1} varargin{2} varargin{3}]

すなわち


Z = [X Y Z]

というひとつのベクトル Z にまとめます.

4.2.6 メインプログラムに関数を書く:@

実行するステートメントがひとつだけの簡単な関数は,名前を与えずに, スクリプトの中に書くことができます. これを 無名関数(anonymous function)と呼びます. 関数 M ファイルとして関数を保存する必要がなく, スクリプトの中で「使い捨て」する簡単な関数を作成したいときに便利です.

無名関数は


関数ハンドル = @( 入力引数 ) 実行ステートメント

という構文で作成します.たとえば,例題 ex4206 では


Average = @(X, N) sum(X) ./ N;
Variance = @(X, N) sum(X.^2) ./ N - Average(X).^2;

という2つの無名関数が定義されています. これら無名関数はそれぞれデータの平均値と分散を計算します. 変数 Average および Variance は関数ハンドル, X と N は入力引数です.引数をデータのベクトルだけにして


Average = @(X) sum(X) ./ length(X);
Variance = @(X) sum(X.^2) ./ length(X) - Average(X).^2;

と定義してもよいでしょう.

無名関数は関数名を持たないので,関数が結びつけられた変数である 関数ハンドル(function handle)を使って呼び出します. 例題 ex4206 では


Average(X, N)
Variance(X, N)

としています.関数ハンドル Average および Variance が関数名のような書き方です.

例題 ex4206 のプログラムの最後に


Average
Variance

という記述を加えると,関数ハンドル Average および Variance の持つ情報が示されます.

ex4206 function handle

テキストで最後に言及している, 関数ハンドルを引数として他の関数に渡す具体例は, 第5章の 5.1.7 節で学習します.