Contents

開発者的な書き方 第1話: 関数化

開発者たちが書くプログラムは、ノンプログラマが書くものとは大きく異なる。

/images/lambda.jpg

彼らのコーディングには、継続的な共同作業を円滑にすることはもちろん、ビジネス成果を最大化するためにも注意が払われている。 とりあえず動くプログラムを書くのは簡単だが、知識としても利用価値の高いプログラムを書くには、それなりの鍛錬が必要だ。 本記事から始まる『開発者的な書き方』シリーズでは、かんたんなプログラムを題材として、メンテナンス性だけでなく、知識としての表現方法にも気を配ったコーディングのしかたを学んでいく。

本記事の流れ

  • 要件をスクリプトに直訳し、目視で動作確認する
  • 方針が良くないことに気づき、いったん降り出しに戻って関数化する
  • スクリプトから関数を呼び出し、目視で動作確認する

前提条件

想定読者
if 文と for 文を使った簡単なプログラムを理解できる
環境
Python3 がインストールされている

FizzBuzz を書いてみよう

サンプルプログラムの代表といえは FizzBuzz1 だ。 英語圏では長距離ドライブの定番ゲームらしい。 日本でいうところの「しりとり」のような立ち位置だろう。 いまから我々は、FizzBuzz ワールドカップに熱狂する世界の住人となる。 ここでは新米プログラマになりきって、コーディング作法を学んでいこう。

西暦2XXX年の夏。 今年もワールドカップの日がやってきた。

決勝の舞台に立てるのは、世界各地で開催された予選を勝ち抜いた4カ国だけ。 今年はアルゼンチン、ブラジル、クロアチア、デンマーク──いずれも常連の強豪だ。

スポットライトを受けた選手がセンターステージの椅子に座るたび、競技場は沸き立った。 審判の合図で、試合開始のカウントダウンが巨大スクリーンに映し出されると、 場内のボルテージはさらに高まり、カウントダウンの大合唱が始まった。

我々のミッションは、プログラミングによる大会運営のサポート: 審判団が選手の回答を正誤判定するためのソフトウェアをつくることだ。 少々無茶な設定だが、話を進めていくには十分だろう。

さぁ、仕事だ。

スクリプト: テストできないプログラム

まず試しに、第3ラウンドまでについて考えてみよう。

ルールを Python プログラムに直訳するとこのようになる:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
players = ['A', 'B', 'C', 'D']
n_round = 3

for i, player in enumerate(players * n_round):
    turn = i + 1
    msg  = ''
    if turn % 3 == 0:
        msg += 'Fizz'
    if turn % 5 == 0:
        msg += 'Buzz'
    if msg == '':
        msg += str(turn)
    print(player + ': ' + msg)

プレイヤー名とラウンド数をそれぞれ変数に格納し、 for ループで回している。 ループの中では、ループカウンタ i から作った通し番号 turn に対して条件判定し、プレイヤーの応答 msg を作っていることがわかる。

これを fizzbuzz_worldcup.py として保存し、実行してみよう。

1
python3 fizzbuzz_worldcup.py

結果はこうだ:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
A: 1
B: 2
C: Fizz
D: 4
A: Buzz
B: Fizz
C: 7
D: 8
A: Fizz
B: Buzz
C: 11
D: Fizz

とりあえずは、期待通りに動いているように見える。

ルール追加

突如、審判が笛を吹いた。ルール追加────国際 FizzBuzz 連盟(IFBA)の方針で、今年からゲームの難度を上げることになったのだ。 大会が始まって以来、各国代表が誰ひとりミスを犯さないため、これまで優勝国が決まったことがない。

審判が追加したルールは、「数字が “12” と “23” のときはつづけて『連番!』とコールすること」というものだった。

どうしたらいいだろうか。これも直訳して実装してみた:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
players = ['A', 'B', 'C', 'D']
n_round = 3

for i, player in enumerate(players * n_round):
    turn = i + 1
    msg  = ''
    if turn % 3 == 0:
        msg += 'Fizz'
    if turn % 5 == 0:
        msg += 'Buzz'
    if msg == '':
        msg += str(turn)
    if str(turn) == '12' or str(turn) == '23':
        msg += '連番!'
    print(player + ': ' + msg)

実行結果はこうだ:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
A: 1
B: 2
C: Fizz
D: 4
A: Buzz
B: Fizz
C: 7
D: 8
A: Fizz
B: Buzz
C: 11
D: Fizz連番!

うーん…“12"が回ってきたデンマーク代表は、正しく「連番!」をコールできているが、 “23"のときの状況は確認できない: いまプレイヤーは4人なので、少なくとも6ラウンドは回してみないと調べられないのだ。

立ち止まろう

しかし、ちょっと待って欲しい。 目視チェックに頼っている今の状態では、プログラムの動作確認をしたことを誰も保証してくれない。 そのうえ、ルールは今後もどんどん追加されることがわかっている。 プログラムの動作確認コストは、ただ数字が増えるだけでも嵩んでいくというのに、 さらに複雑なルールが追加されようものなら、我々はお手上げだ2

仕事に使うプログラムには、綿密な動作確認が必要だ。 思いつくままに条件を与え、自在に実験検証できないといけない。 いま、我々に必要なのは「テスト」: 動作確認用のプログラムを使った、再現性のある検証作業だ。 嬉しいことに Python では、検証作業を助けてくれるテストフレームワークが複数あり、好みのものを選んで使うことができる。 例えば次のようなものだ:

  • pytest
  • unittest
  • doctest
  • nose2
  • testify
  • Robot

しかし実は、ここまで書いてきた fizzbuzz_worldcup.py に対して、テストを書くことはできない──特定の条件だけを受け取れる形式になっていないからだ。 このようなプログラムを「スクリプト」という。 作業に再現性があるだけ手作業よりはましだが、スクリプトは仕事には適さない。

書き方を考え直そう。

関数化

テストを書けるようにするには、プログラムを任意の条件を受け取れるような「関数」の形にしておく必要がある。 我々に必要なのは、現在のルールの下で、 任意の数字がそのまま読み上げられるべきなのか、それとも特定のキーワードに置き換えるべきなのかを調べる作業だ。 いったんふり出しに戻り、新ルール追加前のシンプルなプログラムについて、条件判定に関与している部分を関数として切り出そう。

fizzbuzz_worldcup.py と同じディレクトリに fizzbuzz.py を作り、このように定義した:

1
2
3
4
5
6
7
8
9
def generate_fizzbuzz_msg(i):
    msg = ''
    if turn % 3 == 0:
        msg += 'Fizz'
    if turn % 5 == 0:
        msg += 'Buzz'
    if msg == '':
        msg += str(turn)
    return msg

generate_fizzbuzz_msg() は、任意の数字 i に対して、プレイヤーが応答すべきメッセージを生成する関数だ。

さっそく実行してみよう:

1
python3 fizzbuzz.py

さて、今度は結果が表示されなかった。 これはいま、 fizzbuzz.py の中では generate_fizzbuzz_msg() を定義しただけで、この関数を実際に使ってはいないからだ。

スクリプトからの呼び出し

再び fizzbuzz_worldcup.py を開き、この関数を使うように書き換えてみよう:

1
2
3
4
5
6
7
8
9
from fizzbuzz import generate_fizzbuzz_msg

players = ['A', 'B', 'C', 'D']
n_round = 3

for i, player in enumerate(players * n_round):
    turn = i + 1
    msg  = generate_fizzbuzz_msg(turn)
    print(player + ':' + msg)

generate_fizzbuzz_msg()fizzbuzz.py からインポートすることで、 条件判定部分を関数の呼び出しで済ませることができるようになり、スクリプトはだいぶシンプルになった。

スクリプトを実行してみよう:

1
python3 fizzbuzz_worldcup.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
A: 1
B: 2
C: Fizz
D: 4
A: Buzz
B: Fizz
C: 7
D: 8
A: Fizz
B: Buzz
C: 11
D: Fizz

関数を呼び出したスクリプトにおいても、条件判定をベタ書きしていたときと同じように結果が表示されることを確認できた。

まとめ

ここでは関数化について学べばよいので、自作の関数をスクリプトから呼び出し「どうやら動いているらしい」ことが確認できただけで OK だ。 最初のバージョンと今のバージョンの動作が同じかどうかが気になるかもしれないが、これを目視で比べるのはやめておこう。 そのような確認作業にはどのみち再現性がないし、重要なのは「最新バージョンのプログラムが競技規定通りに動いているのか」ということだ。 こういった大事な確認作業は、目視ではなく、必ずテストにしておこう。

次回は、ここで作った generate_fizzbuzz_msg() に対してテストを書き、さらにルールを追加していく。

つぎに学ぶトピック

  • テスト
  • テスト駆動開発

〜おまけ〜

実は、 fizzbuzz.py 自体を、 fizzubuzz_worldcup.py のようにそのまま実行できるようにすることもできる。 関数定義のあとに特徴的な if 節を追加し、その中に fizzbuzz_worldcup.py と同じプログラムを書けばいい (ただし、 generate_fizzbuzz_msg() 関数はこのファイル自身が定義しているのでインポートする必要はない)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
def generate_fizzbuzz_msg(i):
    msg = ''
    if turn % 3 == 0:
        msg += 'Fizz'
    if turn % 5 == 0:
        msg += 'Buzz'
    if msg == '':
        msg += str(turn)
    return msg

if __name__ == '__main__':
    players = ['A', 'B', 'C', 'D']
    n_round = 3

    for i, player in enumerate(players * n_round):
        turn = i + 1
        msg  = player + ': ' + generate_fizzbuzz_msg(turn)
        print(msg)

この if 節についての解説は、例えばここに書いてある。 簡単にいえば、「もしインポート形式でなくコマンドラインから呼ばれたなら」という意味だ。

こうしておくことで、この関数を実際にどうやって使えばいいのかを示すことができる利点がある。 「この関数の使い方」というマニュアルをゼロから作るのではなく、実際に動くプログラムとして「やってみせる」ことができるのだ。 「生きた文書」「動くドキュメント」は、無駄な仕事をなくすための重要な概念だ。


  1. 一人ずつ順に数字を読み上げていくゲームだ。ルールは WikiPedia でも解説されている ↩︎

  2. 目視確認ができるほど頭が切れるのならば、プログラマなどやめて FizzBuzz 選手になるほうが賢明だろう ↩︎