手順・分岐・繰り返し|論理・アルゴリズム・情報の基礎

このページはプロモーション(広告)を含みます。

論理・アルゴリズム・情報の基礎(初級・読む25分+演習35分)

この授業のゴール

仕事でどう使うか

データの仕事は、結局のところ「入力を受け取り、決められた手順で処理して、出力を返す」ことの連続です。手順が長くなると、どこで条件が抜けたのかが自分でもわからなくなります。手順を紙の上で1行ずつ追える力が、バグ(不具合)の発見と、業務フローの見直しの両方に効きます。

路線 この授業の使いどころ
DS(データサイエンティスト) 前処理の手順(欠損を除く→単位をそろえる→集計)の順番を確認する。順番が変わると結果が変わる
ML(機械学習エンジニア) 学習ループ(繰り返し)の終了条件と、データ分割の手順を追跡する。終了条件の誤りは、学習が止まらない・早く止まる原因になる
DX(DX推進・AI企画) 業務フロー図の分岐(承認・差し戻し)と繰り返し(再申請)を、抜けなく書く。例外処理の漏れが現場の混乱になる
RS(研究者) 実験や解析の手順を、他の人が同じ結果を再現できる粒度で書く。判定基準の適用順序を明記する

この授業のゴールは、手順を追跡表で1行ずつ追い、条件の漏れや回数のずれを自分で見つけられるようになることです。

前提

次の言葉と操作を使います。不安があれば先に戻ってください。

コードは、画面内の 実行 ボタンで動かせます。実行前に、結果を自分で予想する習慣を付けてください。

身近な状況:5件の注文の合計を、手で追う

ネットショップの担当者が、5件の注文のうち、支払済み(paid)の金額だけを合計します。

順番 注文の状態 金額(円)
1 paid 1,200
2 cancelled 4,000
3 paid 800
4 refunded 2,500
5 paid 3,000

人間が電卓で計算するときは、次の手順をふつうに実行しています。

  1. 合計を0にして始める。
  2. 注文を1件ずつ見る。
  3. その注文が paid なら、金額を合計に足す。paid 以外(取消・返金)なら、何もしない。
  4. 5件見終わったら、合計を答えとする。

この手順の中には、3つの部品があります。「1の最初の準備」と「4の答え」は順に実行、「3の paid か否か」は分岐、「2の1件ずつ見る」は繰り返しです。

答えは 1,200+800+3,000=5,000円です。もし手順の3を「paid 以外も足す」と取り違えると、11,500円になります。この手順を書いたとおりに追うことが、この授業の主題です。

直感的な説明:手順は、レシピと同じ3つの部品でできている

料理のレシピを思い浮かべてください。

プログラムも業務フローも、この3つの組み合わせで書かれています。手順書を読むときは、今どの部品の中にいて、変数(記録している値)がいくつかを意識します。

たとえ話の限界:レシピは「適当に」「しばらく」と曖昧に書いても人間が補えます。コンピュータは補えません。分岐の条件は真偽で決まる式で、繰り返しの終わりは数えられる条件で、厳密に書く必要があります。次の節で、正確な書き方を確認します。

正確な定義:アルゴリズム・分岐・繰り返し・追跡表

アルゴリズムとは、入力から出力に至る、有限の手順のことです。次の性質をすべて満たす書き方を指します。

Python では、3つの部品を次のように書きます。

部品 書き方 動き
分岐 if 条件: / elif 条件: / else: 上から順に条件を調べ、最初に真になった1つだけを実行する。どれも真でなければ else
回数で繰り返す for x in リスト: リストの要素を1つずつ x に入れて、中身を実行する
条件で繰り返す while 条件: 条件が真の間、中身を繰り返す。条件に関わる変数を中で更新しないと終わらない

range(n) は、0 から n−1 までの n 個の整数を作ります。range(1, 4) は 1、2、3 の3個です(終点の4は含まれません)。この「終点を含まない」ルールが、回数のずれ(off-by-one:1つずれる誤り)の原因になりやすいので、実行して確かめます。

print(list(range(4)))        # 0 から 3
print(list(range(1, 4)))     # 1 から 3
print(len(list(range(1, 4))))

追跡表は、手順を1ステップずつ追い、そのときの変数の値を表に書き並べたものです。「どの行で何が変わったか」が見えるので、条件の抜けを見つける最も確実な方法です。冒頭の5件の合計を、コードにして追跡表を出力させます。orders は (状態, 金額) の組のリスト、total は合計を入れる整数(int)の変数です。

orders = [('paid', 1200), ('cancelled', 4000), ('paid', 800),
          ('refunded', 2500), ('paid', 3000)]
total = 0
print('回 状態       金額  足した? 合計')
for i, (status, amount) in enumerate(orders, start=1):
    if status == 'paid':
        total = total + amount
        added = '足す'
    else:
        added = '足さない'
    print(f'{i}  {status:10} {amount:5} {added:6} {total}')
print('答え:', total)

最後の合計が、手計算の5,000円と一致します。

段階付き例題:送料の段階料金を、境界の値で追跡する

問題:送料は、購入額によって次のように決まります。この手順を書き、境界の値で追跡してください。

購入額 送料
1,000円未満 500円
1,000円以上3,000円未満 300円
3,000円以上 0円(無料)

ステップ1:入力と出力を決める。入力は購入額 amount(円、整数)、出力は送料(円、整数)です。

ステップ2:条件の並び順を決める。if と elif は、上から順に調べて、最初に真になったものだけを実行します。そこで、範囲が狭い(小さい)ほうから順に並べます。「1,000円未満」→「3,000円未満」→ それ以外、の順です。

ステップ3:コードにする。

def shipping_fee(amount):
    if amount < 1000:
        return 500
    elif amount < 3000:
        return 300
    else:
        return 0

for a in [0, 999, 1000, 1001, 2999, 3000, 3001]:
    print(a, '円 → 送料', shipping_fee(a), '円')

ステップ4:境界の値を追跡する。

購入額 最初の条件 < 1000 2番目 < 3000 実行される部分 送料
999 真 (調べない) 1番目 500
1,000 偽 真 2番目 300
2,999 偽 真 2番目 300
3,000 偽 偽 else 0

境界の値(999と1,000、2,999と3,000)で結果が切り替わっていることを確認します。もし < 1000 を <= 1000 と書き間違えると、1,000円ちょうどの送料が500円に変わります。

ステップ5:意味を言葉に戻す。「3,000円以上は無料」と「3,000円未満は300円以下」が、表と一致することを確認します。どの入力にも、ちょうど1つの送料が決まっている(抜けも重なりもない)ことも確かめます。この例では、3つの範囲が、0円以上のすべての金額をちょうど1回ずつ覆っています。

操作して理解する:while で積み立てて、「以上」と「超える」の違いを見る

問題:毎月3,000円ずつ積み立てます。残高が9,000円に達するのは何か月目か。そして、9,000円を超えるのは何か月目か。

条件で繰り返す while を使います。balance は残高(円、整数)、month は経過月数(整数)です。実行する前に、2つの答えを予想してください。

balance = 0
month = 0
print('月 残高')
while balance < 9000:
    month = month + 1
    balance = balance + 3000
    print(month, balance)
print('以上(>=9000)に達した月:', month)

balance = 0
month = 0
while balance <= 9000:
    month = month + 1
    balance = balance + 3000
print('超えた(>9000)月:', month, ' 残高:', balance)

予想と答え:

ループの条件は、「終わりの条件」の否定になります。「9,000円以上になったら終わり」なら、続ける条件は「9,000円未満の間」です。ここを間違えると、1か月ずれます。

次に、自分で書き換えてみましょう。

  1. 積み立て額を2,500円に変えると、9,000円に達するのは何か月目ですか。
  2. 積み立て額を0円に変えると、何が起きますか(実行前に考えてください。実際には、最大の月数を決めてから試します)。

答えは、(1) 4か月目(2,500×3=7,500で未満、2,500×4=10,000で以上)、(2) balance が増えないので、条件がずっと真のまま、終わらないループになります。

発展:ループの終了の保証・順序の意味・手順の速さと正しさ

ループが終わる理由を説明する:while balance < 9000 が終わるのは、ループのたびに balance が3,000ずつ単調に増えるため、いつか9,000に届くからです。「毎回、終了条件に近づく量」を見つけることが、終了の説明になります。実務では、念のため最大回数を決めておき、超えたらエラーにする(ガード)設計も使います。

分岐の順序:if と elif は、順序によって結果が変わることがあります。範囲が重なる条件では、狭い範囲、または優先度の高い条件を先に書きます。条件が互いに重ならない(排他的な)場合は、順序は結果に影響しませんが、読みやすさのために昇順に並べます。

for と while の使い分け:リストの要素を1つずつ処理するなら for。回数が事前にわからず、条件で止めるなら while。多くの処理は for で書けて、終わらないバグが起こりにくいのが利点です。

速さと正しさ:手順が短い、速いことは、正しさとは別の性質です。たとえば「先頭の10件だけを調べて合計する」手順は速いですが、全件の合計ではありません。まず正しさを検査ケースで確かめ、そのうえで、速さ(全件を走査する費用)を 計算量と小さい反例 で評価します。

再帰:手順の中で自分自身を呼ぶ書き方もありますが、この授業の範囲を超えます。入門では、for と while で書けば十分です。

よくある失敗

失敗1:elif の順序が逆で、狭い範囲の分岐に到達しない

送料の条件を、広い範囲から先に書いた例です。

def shipping_wrong(amount):
    if amount < 3000:       # 広い範囲を先に書いた
        return 300
    elif amount < 1000:     # ここには永久に到達しない
        return 500
    else:
        return 0

def shipping_right(amount):
    if amount < 1000:
        return 500
    elif amount < 3000:
        return 300
    else:
        return 0

for a in [500, 2000, 3000]:
    print(a, '誤:', shipping_wrong(a), ' 正:', shipping_right(a))

500円の注文でも、最初の amount < 3000 が真になって300円が返ります。「1000円未満」という条件は、書いてあるのに一度も実行されません。エラーも出ないので、気づきにくい誤りです。

直し方:範囲が重なる条件は、狭いほうから順に書く。そして、すべての境界の値(999、1,000、2,999、3,000など)で結果を表にして確認します。

失敗2:ループの範囲が1つずれる(off-by-one)

リストの全要素を足すつもりで、range(1, len(items)) と書いた例です。

items = [1200, 800, 2500]
print('要素数:', len(items))

wrong = 0
for i in range(1, len(items)):      # 先頭(0番目)を飛ばす
    wrong = wrong + items[i]

right = 0
for i in range(len(items)):         # 0 から要素数-1 まで
    right = right + items[i]

print('誤:', wrong, ' 正:', right)

try:
    for i in range(len(items) + 1): # 1つ多い
        items[i]
except IndexError as e:
    print('範囲外エラー:', type(e).__name__)

range(1, 3) は 1、2 の2個なので、先頭の1,200円が足されず、3,300円(正しくは4,500円)になります。逆に range(len(items) + 1) は4個の番号を作り、存在しない3番目を読んで IndexError になります。

直し方:range(n) は「0 から n−1」と覚える。リストの全要素を扱うときは、for x in items: と書くと、番号を使わずに済みます。

失敗3:while の中で、条件の変数を更新し忘れる

積み立ての例で、balance を更新する行を書き忘れた例です。本物の無限ループになると画面が固まるので、ここでは最大回数のガードを付けて動かします。

balance = 0
count = 0
while balance < 9000 and count < 5:   # count < 5 はガード
    count = count + 1
    # balance = balance + 3000 を書き忘れた
print('ループ回数:', count, ' 残高:', balance)

出力は、ループ回数5、残高0です。ガードがなければ、終わりません。balance < 9000 の条件は、balance が変わらない限り永久に真だからです。

直し方:while を書いたら、「ループの中で、条件に使っている変数のどれが、どう変わるか」を必ず1行で言えるようにします。言えないときは、終わらない可能性があります。デバッグのときは、ガード付きにして、ループの回数と変数の値を表示させます。

まとめ

用語

アルゴリズム(algorithm)

入力から出力に至る、有限回で終わるあいまいさのない手順。

分岐(branch)

条件の真偽によって、次に実行する処理を選ぶ手順の部品。Pythonでは if / elif / else。

繰り返し(ループ)(loop)

同じ処理を、回数や条件にもとづいて繰り返す手順の部品。Pythonでは for と while。

追跡表(trace table)

手順を1ステップずつ追い、各ステップでの変数の値を書き並べた表。

off-by-one(1つずれる誤り)(off-by-one error)

ループの回数や範囲の端が、1つ多い・少ない誤り。range の終点を含まない性質で起こりやすい。

よくある質問

なぜ、手順を紙で追跡する必要があるのですか? 実行すれば結果は出ます。

実行結果は「答え」しか示しません。答えが間違っていたとき、どのステップで変数が想定とずれたかは、追跡表でないと見えません。また、実行しても正しい答えになる入力ばかり試すと、条件の抜けは見つかりません。

if と elif を並べるのと、if を何度も書くのは、何が違いますか?

if / elif / else は、1つの分岐の中から最初に真になった1つだけを実行します。if を独立して並べると、それぞれの条件が別々に判定され、複数が実行されることがあります。「どれか1つ」にしたいときは elif でつなぎます。

for と while は、どちらを使うべきですか?

リストの全要素や、決まった回数なら for。条件が成立するまで、あるいは回数が事前にわからないなら while。for は有限回で必ず終わるので、まず for で書けないかを考えると、終わらないバグを避けられます。

境界の値は、どう選べば漏れませんか?

条件に出てくる数値(1000、3000など)について、その値ちょうどと、その前後(999、1001、2999、3001)を検査ケースに入れます。さらに、0、負の数、空のリストのような端の入力も加えます。条件が切り替わる場所が、間違いの出やすい場所です。

手順の中の分岐が増えると、確認はどうなりますか?

分岐が増えるほど、通りうる道筋(経路)の数が増えます。分岐が3つ並ぶ単純な構造なら、各分岐を1回ずつ通る検査ケースで足ります。条件が入れ子になるほど増えるので、小さい関数に分けて、それぞれを検査します(「小さい関数への分割」(有料版))。

仕事で、手順のバグを見つけるいちばん早い方法は何ですか?

まず、入力の例を3〜5個選びます。通常の値、境界の値、異常値、空を含めます。次に、それぞれを追跡表で追い、期待する答えと比べます。ずれた行が見つかったら、その行の条件か更新式を見直します。

確認問題

問1 次のコードで、x = 1000 のとき返る値はどれか。

if x < 1000: return 'A' / elif x < 3000: return 'B' / else: return 'C'

  1. ① 'A'
  2. ② 'B'
  3. ③ 'C'
  4. ④ エラーになる
解答と解説

正解:②

1000 < 1000 は偽、1000 < 3000 は真なので、2番目の分岐が実行されて 'B' を返します。

問2 range(1, 4) が作る整数の並びはどれか。

  1. ① 0, 1, 2, 3
  2. ② 1, 2, 3, 4
  3. ③ 1, 2, 3
  4. ④ 0, 1, 2
解答と解説

正解:③

range(a, b) は a から b−1 までの整数を作り、終点 b は含みません。

問3 while ループが終わらなくなる、最も典型的な原因はどれか。

  1. ① ループの中で print を使ったから
  2. ② リストが長すぎるから
  3. ③ 条件が真偽値ではなく数値だから
  4. ④ 条件に使っている変数を、ループの中で更新していないから
解答と解説

正解:④

条件に関わる変数が変わらなければ、条件は永久に真のままで、ループが終わりません。

問4 送料の分岐で、if amount < 3000 を elif amount < 1000 より先に書くと、何が起きるか。

  1. ① 500円の注文でも300円が返り、「1000円未満」の分岐は実行されない
  2. ② 500円の注文は500円、2000円の注文は300円で、正しく動く
  3. ③ エラーになって実行できない
  4. ④ 3000円以上の注文が500円になる
解答と解説

正解:①

先に広い条件を書くと、狭い条件の範囲も先に真になります。後ろの狭い条件には到達しません。エラーは出ません。

問5 追跡表とは何か。最も適切な説明はどれか。

  1. ① プログラムの実行時間を測った表
  2. ② 手順の各ステップでの変数の値を、順に書き並べた表
  3. ③ コードの文法の誤りを自動で見つけた結果
  4. ④ 入力データを昇順に並べた表
解答と解説

正解:②

追跡表は、手順を1ステップずつ追い、変数の値を書き並べて、動きを確認するためのものです。

問6 5万件の注文の合計を求める手順として、「先頭の100件だけを足して答えにする」手順は、速さと正しさの点でどう評価できるか。

  1. ① 速いので、正しい手順である
  2. ② 速いが、全件の合計ではないので、正しくない
  3. ③ 遅いので、正しくない
  4. ④ 速さと正しさは常に同じ意味である
解答と解説

正解:②

速さと正しさは別の性質です。条件を落とした(全件を足していない)手順は、速くても答えが違います。

次に学ぶこと

次は 入力・出力・状態 で、手順の入力・出力と、手順の途中で変わる値(状態)を整理します。この授業の追跡表は、状態の変化を見る道具として続けて使います。

if・for・while の文法は 「ifと条件式」(有料版)〜「whileと終了条件」(有料版)、ネスト(入れ子)の追跡は 「入れ子と追跡」(有料版) で扱います。この授業の確認問題だけでモジュールの習得とはしません。ON06 の総合テストと、会員割引の条件を表で検証する自力課題で判定します。

キャリア学習:データサイエンティスト・機械学習エンジニア・DX推進・研究者を目指す人の学習アプリ。164モジュール・984授業を、ブラウザで動くSQLとPythonの演習で。基礎と各分野の最初のモジュールは無料。全部は2,980円の買い切り。

アプリを無料で開く有料版と無料版の違い