if文でif節とelif節がズラズラと並ぶことってよくありますよね? そんな状況になってきたら、もっとスマートに処理を分岐するためにアレを使ってみませんか?
以下のhandle_command関数では、if文の中で変数commandの値に応じて処理を分岐するようになっている(ただし、サンプルコードなのでここでは処理は単純にprint関数を呼び出すだけ)。このくらいなら問題ないが、コマンドの数がもっと増えると、このif文は長大なものになることが予想される。もっと見やすくスッキリとしたコードに書き換えてみよう。ただし、match文は使わないものとする。
def handle_command(command):
if command == 'go':
print('go straight')
elif command == 'right':
print('turn to right')
elif command == 'left':
print('turn to left')
elif command == 'back':
print('go back')
else:
print('unknown command')
どうもHPかわさきです。
今回はif文での条件分岐が増えてきたらどーする? って問題です。match文を使って書き直してもちょっとはスッキリしますけど、今回は別の方法を考えてみてください。あのデータ構造が便利に使えます。
それはそうと、この記事が公開される頃にはゴールウィークがもう目前です! 皆さんは何をする予定ですか? 筆者は特に何も考えていません。お酒を飲んでグズグズする日々になるであろうことが確定しているような気がします。次回にはベロベロになった筆者が考えたグダグダな問題を皆さんに解いてもらう予定です(笑)。
以下に正解のコード例を示します。
def handle_command(command):
actions = {
'go': 'go straight',
'right': 'turn to right',
'left': 'turn to left',
'back': 'go back'
}
action = actions.get(command, 'unknown command')
print(action)
辞書のキーにif文の各節で比較している条件である文字列を、その値にprint関数で出力する文字列を設定しています。そして、その辞書のgetメソッドに比較対象のコマンドを渡すことで、そのコマンドに対応した文字列を得られるのがポイントです。辞書にないコマンドをgetメソッドに渡すと'unknown command'が返されますが、これはif文のelse節での処理に対応しています。
こうすることで、if文で「command == ...」形式の節を延々と書く必要がなくなり、行数はそれほど変わりませんが、全体にスッキリとした見やすいコードとなります。
問題文のコードはユーザーからのコマンド入力を受け取って、それを処理するための関数です。例えば、次のような使い方をすることになるでしょう(以下ではループを終了する'exit'というコマンドはhandle_command関数とは別に処理するようにしています。これをhandle_command関数で処理することも可能ですが、ここではこのような書き方にしました)。
def handle_command(command):
if command == 'go':
print('go straight')
elif command == 'right':
print('turn to right')
elif command == 'left':
print('turn to left')
elif command == 'back':
print('go back')
else:
print('unknown command')
while True:
command = input('action: ')
if command == 'exit':
break
handle_command(command)
このコードは分かりやすく、意図した通りに動作するでしょう。しかし、処理するコマンドが増えると、次のようにif文が長大になっていくのが難点です。
def handle_command(command):
if command == 'go':
print('go straight')
elif command == 'right':
print('turn to right')
elif command == 'left':
print('turn to left')
elif command == 'back':
print('go back')
elif command == 'rest':
print('take a break')
elif command == 'eat':
print('mog mog')
elif command == 'look':
print('there is something to inspect')
else:
print('unknown command')
こんなときには、「if文で比較している条件」をキーに、「条件に合致したときに処理する部分」を値にした辞書を作るとコードがスッキリします。問題文では全ての節でprint関数を呼び出していて、その引数が異なっていただけなので、キーに対応する値はprint関数に渡している文字列で十分です。そのため、キーと値の両方が文字列である辞書を作成します(キーから値を検索するという意味で、辞書のことをルックアップテーブルと呼ぶことがあります)。キーから値を取り出すには以下のコード例に示すようにgetメソッドを使えばよいでしょう。
def handle_command(command):
actions = {
'go': 'go straight',
'right': 'turn to right',
'left': 'turn to left',
'back': 'go back'
}
action = actions.get(command, 'unknown command')
print(action)
if文と比べると、こちらのコードはユーザーが入力したコマンドでどんな処理が行われるか(ここではどんな文字列が出力されるか)が一目瞭然で見やすくなっています。そして、ユーザーから受け取ったコマンドを辞書のgetメソッドに渡すと、辞書のキーにそのコマンドがあれば、対応する値が返され、辞書のキーになければ'unknown command'という文字が返されます。なお、上のコードでは値の部分も文字列でしたが、処理が複雑になってきたら、関数を値にすることも可能です(このような処理を「関数のディスパッチ」と、その目的で使用する辞書を「ディスパッチテーブル」と呼ぶことがあります。後述)。
受け付けるコマンドが増えたらコードがどうなるかも見てみましょう。
def handle_command(command):
actions = {
'go': 'go straight',
'right': 'turn to right',
'left': 'turn to left',
'back': 'go back',
'rest': 'take a break',
'eat': 'mog mog',
'look': 'there is something to inspect'
}
action = actions.get(command, 'unknown command')
print(action)
if文を使った方法ではコマンドが増えるとコードが2行増えていましたが、辞書を使う方法ではキーと値の組が1行追加されるだけなので、関数の見通しもそれほど悪くなっていないように思えます。
ここまでは、辞書の値として文字列を保存し、それをprint関数に渡すだけでした。しかし、コマンドごとに行う別々の処理になったり、より複雑になったりしたらどうでしょう。例えば、goコマンドでは位置情報を更新し、eatコマンドでは満腹度を更新するといった具合です。
そのようなときには、上でも述べましたがコマンドごとの処理を関数にまとめ、それらの関数を辞書の値として使用できます(それができるのはPythonでは関数もまたオブジェクトだからです)。コマンドごとに処理を関数にまとめた例を以下に示します(ただし、ここではコマンドに対応する関数ではprint関数呼び出しだけを行うものとします。実際にはそこに複雑なコードが入るものだと考えてください)。
def go():
print('go straight')
def right():
print('turn to right')
def left():
print('turn to left')
def back():
print('go back')
def unknown():
print('unknown command')
def handle_command(command):
actions = {
'go': go,
'right': right,
'left': left,
'back': back
}
action = actions.get(command, unknown)
action()
goコマンドにはgo関数が、rightコマンドにはright関数が……のように、ここではコマンドの名前に対応する形で、実行する関数を定義しています。unknown関数は用意されていないコマンドが入力されたときに実行されます。
そして、それらの関数をここでは辞書の各キーの値としていることに注目してください。「action = actions.get(command, unknown)」行では、getメソッドにコマンドを渡していますが、これにより辞書のキーにそのコマンドがあれば、対応する値として関数が返され、変数actionに代入されます。そして、「action()」のようにすることで、辞書から得た関数を実行するわけです。
このように何らかの条件(ここではユーザーからのコマンド入力)を基に、プログラムの実行時に実際に呼び出す関数を決定することを「関数の(動的な)ディスパッチ」と、そのために使われる辞書のようなデータ構造のことを「ディスパッチテーブル」と呼ぶことがあります。
ルックアップテーブルとディスパッチテーブルの差は? というと、前者はある値から対応する値を得るためのもので、後者はある値から何の処理をするかを決定するためのものということになるでしょう。
getメソッドにはキーが辞書になかった場合の戻り値として、unknown関数を指定しているので、上でも述べたように用意されていないコマンドが入力されたら、この関数が実行されます。
関数のディスパッチはif文での処理の分岐が多くなってきた際に、コードを簡潔にしてくれるテクニックといえます。最初はもちろんif文を使って問題ないでしょう。しかし、if文が長くなり出したら、このような対処を考えるようにすることをオススメします。
最後にPython 3.10以降で使用可能なmatch文を使うとどうなるか、見てみましょう。
def handle_command(command):
match(command):
case 'go':
print('go straight')
case 'right':
print('turn to right')
case 'left':
print('turn to left')
case 'back':
print('go back')
case _:
print('unknown command')
「command ==」という部分がなくなった分、if文よりも簡潔になったようにも感じます。が、辞書を用いる方法と比べると、あえてこちらを選ぶ理由もないかなぁという感じですね。match文を使えば、もちろんif文よりも簡潔で可読性の高いコードになります。ただし、match文がその力を存分に発揮するのは、単なる簡潔さや可読性の向上にとどまらず、パターンマッチングを生かした複雑な条件分岐など、より高度な場面かもしれません。
なお、if文については「Python入門」の「if文による条件分岐」で、辞書については「辞書」で触れています。興味のある方はぜひそちらもお願いします。
なんか思ったよりも書き過ぎちゃったので、次回からはもっとコンパクトにしたいと思います。
初心者向け、データ分析・AI・機械学習・Pythonの勉強方法 @ITのDeep Insiderで学ぼう
Copyright© Digital Advantage Corp. All Rights Reserved.
Deep Insider 鬯ッ�ッ�ス�ョ�ス�ス�ス�ォ�ス�ス�ス�ス�ス�ス�ス�ェ鬯ョ�ッ陋ケ�コ�ス�サ郢ァ謇假スス�ス�ス�ソ�ス�ス�ス�ス�ス�ス�ス�ス�ス�ス�ス�ス�ス�ス�ス�ス�ス�ス�ス�コ鬯ョ�」陋ケ�ス�ス�ス�ス�オ鬮ォ�エ遶擾スオ�ス�コ�ス�キ�ス�ス�ス�ク�ス�ス�ス�キ�ス�ス�ス�ス�ス�ス�ス�ケ鬮ォ�エ髮懶ス」�ス�ス�ス�「�ス�ス�ス�ス�ス�ス�ス�ス�ス�ス�ス�ス�ス�ス�ス�ウ鬯ッ�ゥ陝キ�「�ス�ス�ス�「�ス�ス�ス�ス�ス�ス�ス�ァ�ス�ス�ス�ス�ス�ス�ス�ス�ス�ス�ス�ス�ス�ス�ス�ュ鬯ッ�ゥ陝キ�「�ス�ス�ス�「鬮ォ�エ髮懶ス」�ス�ス�ス�「�ス�ス�ス�ス�ス�ス�ス�ス�ス�ス�ス�ス�ス�ス�ス�ウ鬯ッ�ゥ陝キ�「�ス�ス�ス�「�ス�ス�ス�ス�ス�ス�ス�ァ�ス�ス�ス�ス�ス�ス�ス�ス�ス�ス�ス�ス�ス�ス�ス�ー