効果的なコーディング練習問題の解かせ方


開発現場で役に立つ基本動作を身に着けるための効果的なコーディング練習問題の解かせ方について紹介します。

NOTE: ソースコード、紹介するURL等は基本的にPythonに統一しています

まとめ

  • 練習問題は一度解かせて終わりにしない。
    • 一定時間内に解けるまで何度も反復させる。
    • 一定時間内に解けるようになっても、少し期間を空けた上で再度解かせる。
  • 練習問題を解くことを短期集中で終わらせず、長期継続する。
    • 例えば2週間は毎日ミッチリ解いてもらい、それ以降は1日1時間を数カ月継続する。
  • 練習問題は「解くこと」だけを求めず、以下の指針を設ける。
    • 入力値の手動入力を排除する。(INを意識させる)
    • 結果確認時の目視確認を排除する。(OUTを意識させる)
    • (おまけ) 結果確認のパターンをテストデータ化する。

背景

開発チームに新人プログラマ、開発未経験メンバーが入ってきた場合は以下のように育成をすると思います。

  1. 参考書を読んでもらう
  2. 練習問題を解いてもらう
  3. 簡単なコーディング作業をしてもらう

初心者の段階において、個人的に最も重視しているのは2点目の「練習問題を解く」ということです。for文、if文、文字列操作、ファイル処理などを含んだ簡単な練習問題でウンウン頭を悩ますようでは、仕事の中で要求されるプログラミングは到底できません。だからこそ反復して解いてもらい、徹底的に基本動作として叩き込む必要があります。

NOTE:
いわゆる使えないと言われるプログラマの大半は、簡単な練習問題すら解けないことが多いです。その要因は複数ありますが、練習問題を何度も反復して解くという訓練が絶対的に不足していることは共通しています。ほぼ未訓練のため力がなく、力がないため仕事も任せられず経験や訓練の機会も失うという悪循環に陥ります。
そのような人に完遂できもしない仕事を割り振るよりも、思い切って練習問題を反復して解かせることに集中させた方が中長期的に見た場合は良い結果を生みます。

実際の現場でもある程度は練習問題を解かせたりするようですが、大きく2つの問題があります。

  1. 練習問題を一度解かせて終わり
  2. 練習問題の解かせ方にあるべき指針がない

1点目は言葉通りです。例えば1分で解ける問題は1分で解けるようにする必要があります。しかしこれを1回目に1時間かけて解いてそれ以降は同じ問題を解かないのであれば意味があまりないです。ここは必ず目標を達成するまで繰り返させましょう。また、一度達成できただけでは偶発的なものかもしれないため、何度か達成するまでは定期的に同じ問題を解かせるようにしましょう。

2点目は練習問題を解くときに、我々教える側が適切な指針あるいは作業フローを示せていないということです。おそらく普通に練習問題を解かせると「プログラムを実装」して「プログラムに値を手動入力」して「結果を目視確認する」というフローを取ると思います。これはこれで大事な初めの一歩なのですが、実際の能率的な開発現場でこのような開発は行いません。手動入力や目視確認は極力排除して、作業フローの多くを自動化することを心がけます。これを練習問題の解く指針として与えることにより、能率的な開発現場の基本動作の一部を習慣として身につかせることが大事です。

この記事では2点目の指針について説明します。

練習問題サイト (Python限定)

そもそもどういう練習問題を解かせればいいかで悩む人が多いため、いくつか役に立つ練習問題が載っているサイトを紹介します。基本的に制御構文、文字列操作、数値計算、ファイル操作、簡単なインターネットアクセス等の実用に直結しやすい問題が掲載されているところを紹介しています。

  • Practice Python
    ジャンケン等の簡単なゲーム、ファイル操作、Webページアクセス等の基礎固めに最適な問題集です。
  • Python Exercises, Practice, Solution
    Pythonの文字列、リスト、辞書、クラス、正規表現等のトピック毎の問題が豊富に揃っています。各トピックの基礎固めには最適です。ちなみにNumpyやPandasの問題もあります。
  • CodingBat Python
    算数やパズル的な問題が揃っています。Web上で実装し、予め用意されたテストケースが動作するかを自動的にチェックしてくれます。テスト駆動開発っぽい感じで練習問題を解くことができます。
  • Python課題集 - LOG OPT HOME
    学校のプログラミング入門授業で出てきそうな日本語の問題集です。「学校で最初にやったレベルの問題をやらせたいなぁ」と思っている人には最適です。

なお、複雑なデータ構造、アルゴリズム、ライブラリやフレームワーク、数式が必要な問題集はお勧めしません。キャッチボールもできない段階で、変化球を投げさせたり、高度なサインプレーをさせるのは明らかに無理があります。特に小さい頃からプログラミングに慣れ親しんでいたり、非常に頭の良い人が教える立場になった時にやりがちなので注意しましょう。(彼らは先の先を見据えて教えてあげるのですが、結果的に初めの数歩で挫折させてしまいます)

残念ながら日本語のWebサイトではあまり良質な問題集がない印象を受けました。このためどうしても海外のものを紹介することが多くなってしまいます。海外の問題集を探す場合は、例えばPythonであればpython practicepython exerciseのようにpractice/exerciseというキーワードで組み合わせると色々な問題集を見つけることができます。日本語の問題集と比較すると、より実践や普段のコーディングで使う上で役に立つ問題集が多いです。

練習問題を解く手順

以下のフェーズに分けて練習問題を解くための実装、確認を行います。

  1. 問題を解く
  2. 関数化 -> 手動入力を排除
  3. 結果確認をassertで実装 -> 目視確認を排除
  4. 結果確認をユニットテスト化 -> より柔軟かつ高度な自動確認

Phase 1. 問題を解く

まずはプログラミング問題を解きます。
例として、ここでは入力した数字の階乗を求める問題を解きます。
おそらく多くの場合は、以下のように「手動で値を入力して、結果をprint文で出力する」という実装になると思います。

# 手動で数値を入力
n = int(raw_input("enter number:"))

# 処理する
result = 1
for i in range(1, n+1):
    result = result * i

# 処理結果を出力する(目視確認)
print(result)

このPhase 1の実装には以下の欠点があります。

  • 値を手動入力しなければならない
  • 結果を目視確認しなければならない

実際のプログラムは機能の追加、変更、修正などによって実装が刻々と変化します。このような現実を考えた場合、手動入力や目視確認というのは非常に非効率です。Phase 1で終ってしまう練習問題の解き方は、この非効率なやり方が悪癖として身についてしまう可能性があります。

Phase 2. 関数化する

Phase 2では解いた問題の実装を関数にします。これにより手動入力を排除できます。Phase 2では相変わらず関数の戻り値をprint文で出力しているため目視確認する必要がありますが、これはPhase 3以降で対応します。

  • 手動入力する値 ⇒ 関数の引数とする。(IN=入力)
  • 目視確認する内容 ⇒ 関数の戻り値とする。(OUT=出力)
# Phase 1の実装を関数化
# -> 適切な引数と戻り値を考える
def func(n):
    result = 1
    for i in range(1, n+1):
        result = result * i
    return result

print(func(0))
print(func(1))
print(func(2))
print(func(3))
print(func(4))
print(func(5))

関数化する時には常にIN/OUTを意識してもらうようにしましょう。意識するというのは、ただ実装するだけではなくIN/OUTを何故そのようにしたかを相手に説明できるところまでを指しています。

現在のコーディングは、既存の関数やライブラリを理解して活用することの重要性が高まっています。ドキュメント等をもとにIN/OUTが何かを理解した上で使用することが要求されます。練習問題を解くときからIN/OUTを意識させるということは、他の人が作った関数、ライブラリを理解して使用することにも役立ちます

NOTE:
関数の段階でIN, OUTだけではなくSide Effect(副作用)も叩き込もうという欲に駆られるかもしれませんがお勧めしません。既に練習問題を解かせる時に色々とプラスアルファを要求しているため、ここで初心者には理解しづらい副作用という概念はノイズにしかならない可能性が高いです。

Phase 3. 関数をassertで動作確認

関数の戻り値をassert文でチェックすることにより、戻り値が期待値と異なる場合はエラー出力するようになります。これにより各入力値パターンの目視確認を排除することができます。ここではPythonのassert文によるチェックを使っていますが、他のプログラミング言語でもif文などによるチェックで実装できると思います。

NOTE:
後述するunittestの利点を強調するため、まずはより原始的なassert文という手法で自動確認を行っています。何故assert文よりもunittestが良いかを体感させることにより、「より良いやり方があるのではないか」という考えが芽生えてくれることを期待しています。

# Phase 2と同じ関数
def func(n):
    result = 1
    for i in range(1, n+1):
        result = result * i
    return result

# assertで動作確認
assert( func(0) == 1 )
assert( func(1) == 1 )
assert( func(2) == 2 )
assert( func(5) == 120 )

なお、assert文には以下の欠点があるため、このassert文に十分に慣れてきた段階でPhase 4のユニットテストによる動作確認に移行します。なおPhase 4に移行したら基本的にPhase 3の実施は不要です。

  • 結果が期待値と異なる場合、結果の具体的な値が分からない。
  • NGが発生したassert文より後の処理が行われないため、他の動作確認結果が分からない。

Phase 4. 関数をユニットテストで動作確認

最後は自動チェックの部分をユニットテスト化します。
これで失敗時はどこで失敗し、どのような値で失敗したのかを確認できるようになります。

import unittest

# Phase 2と同じ関数
def func(n):
    result = 1
    for i in range(1, n+1):
        result = result * i
    return result

# unittest
class FuncTest(unittest.TestCase):
    def test_zero(self):
        self.assertEqual(func(0), 1)

    def test_one_four_five(self):
        self.assertEqual(func(1), 1)
        self.assertEqual(func(4), 24)
        self.assertEqual(func(5), 120)

unittest.main()

なお、ここでプラスアルファの課題としてテストパターンをテストデータ化してテストしてもらうと学習効果が高まります。処理の共通化、リファクタリングといった基本動作の意識付けになります。

以下ではテストデータをタプルのリストに格納してテスト処理を共通化する例です。

import unittest

test_data = [(0, 1), (1, 1), (4, 24), (5, 120)]

class FuncTestWithTestData(unittest.TestCase):
    def test_all_test_data(self):
        for input_value, expected in test_data:
            self.assertEqual(func(input_value), expected)

さらに発展させてテストデータをCSVやJSON形式にして読み込ませると、リストやハッシュマップ等のデータ構造を活用する練習にもなります。リスト、ハッシュマップ、JSON、CSVなどのデータ構造やファイル形式はどこでも登場してくるため、練習問題を解かせたり、テストケースを書いてもらう時に積極的に使わせましょう。(プロダクトコードでチャレンジはさせづらいですが、こちらであれば色々とチャレンジさせやすいはずです)

import json
import unittest

testdata_json = """
    [
      { "value": 0, "expected": 1 },
      { "value": 1, "expected": 1 },
      { "value": 4, "expected": 24 },
      { "value": 5, "expected": 120 }
    ]
"""

class FuncTestWithTestJson(unittest.TestCase):
    def test_all_test_json(self):
        testdata_list = json.loads(testdata_json)
        for entry in testdata_list:
            self.assertEqual(func(entry["value"]), entry["expected"])