0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

REPL からプログラムへの漸進的改造

0
Posted at

漸進的改造

プログラミング言語、便利ですね。特に繰り返しのある計算問題では強力な力を発揮します。

  1. 最初の部分だけ手計算する
  2. 次の部分を生成するプロトタイプを作る
  3. プロトタイプのバグ取り・清書
  4. リファクタリング

このような手順で問題を解決していきます。

日常生活にもこの手順で解決する問題がたくさんあります。

この記事では手順を

  1. REPL で断片的な計算を積み重ね、あとから入力をコピペしてつないだプログラムを作る
  2. バグ取り、読みづらい部分の改善
  3. リファクタリング

と整理し 2. バグ取り・清書 と 3. リファクタリング を扱い、 REPL で作った試作コードを、どう整えていくかを書きます。
例として機械的にウエイトトレーニングのメニューを作るプログラムを作ります。

  day: 1
   40.0 kg
    66.67 %
   42.5 kg
    70.83 %
   45.0 kg
    75.0 %
  day: 2
   42.5 kg
    70.83 %
   45.0 kg
    75.0 %
   47.5 kg
    79.17 %
    ...

こんな感じの出力を目指していきます。

プロトタイプ

後から読むことを想定していないようなプログラムで、しかも一部の出力に問題があります。

proto.py
""" 機械的にウエイトトレーニングのメニューを組んでくれるプログラム
    day ごとに 2.5 kgウエイトが増えていく。
    一日は3セット、次の日は1セットぶん重ねる
    """

def make_program(max, start, end):
    """ 
      max:   開始時の最大重量 kg 重
      start: 開始時に無理なく挙がる重量 kg 重
      end:   目標にする重量 kg 重
     """
    cursor = 0
    day = 1
    # 2.5 kg 刻みのウエイトのリスト。多分作れるはず?
    lst = [
        (i, round(i / max, 2))
        for i in [round(w / 1000, 1) for w in range(start * 1000, end * 1000 + 1, 2500)]
    ]
    skip = []

    print(f"  day: {day}")

    while True:

        cursor += 1

        print(f"   {lst[cursor - 1][0]} kg")
        print(f"    {round(lst[cursor - 1][1] * 100, 2)} %")

        if cursor == len(lst):
            break

        if cursor in skip:
            day += 1
            print(f"  day: {day}")
            continue

        if cursor % 3 == 0:
            day += 1
            skip.append(cursor)
            print(f"  day: {day}")
            cursor -= 3


if __name__ == "__main__":
    make_program(60, 40, 120)

この時点では

  • バグ
  • 数値の丸めround()が複数回使われている
  • 紛らわしい名前

などの問題があります。

ただそれよりも初見では、「cursorの部分が非常に分かりづらいなぁ」と感じられると思います。

        if cursor % 3 == 0:
            day += 1
            skip.append(cursor)
            print(f"  day: {day}")
            cursor -= 3

REPL で対話的に計算している際は不思議と気にならないんですが、プログラムに固定されたら良くはないですね。

バグ取り

  • 1 日目 、 2 日目が同じになる不具合の解決
  • round()が複数回使われているので一回で済むようにする
  • 複雑な f-string を書式指定子によって簡単に
  • その他清書
fixed.py
""" 機械的にウエイトトレーニングのメニューを組んでくれるプログラム
    day ごとに 2.5 kg ずつ増える
    1日は 3 セット、隣接する日とは 2 つ分重なる
"""

def make_program(max_weight, start, end):
    """
    max_weight: 開始時の最大重量 kg
    start:      開始時に無理なく挙がる重量 kg
    end:        目標にする重量 kg
    """
    if max_weight <= 0:
        raise ValueError("max_weight は 0 より大きい必要があります")
    if start > end:
        raise ValueError("start は end 以下である必要があります")

    # 2.5kg刻みの重量リスト
    # [重量, 最大重量比]
    weights = [
        (w / 1000, round((w / 1000) / max_weight, 2))
        for w in range(start * 1000, end * 1000 + 1, 2500)
    ]

    day = 1
    cursor = 0
    skip = set()

    while cursor + 2 < len(weights):
        if cursor in skip:
            cursor += 1
            continue

        print(f"  day: {day}")

        # 3セット表示
        for i in range(3):
            weight, ratio = weights[cursor + i]
            print(f"   {weight:.1f} kg")
            print(f"    {ratio * 100:.1f} %")

        # この開始位置は処理済みとして記録
        skip.add(cursor)

        day += 1

        # 次の日は2セットぶん重ねる
        # 3セット中、先頭の1セットだけ進める
        cursor += 1


if __name__ == "__main__":
    make_program(60, 40, 120)

これで期待している出力が出るようになりました。

ただ、

  • もう skip をなくせるんじゃないか?
  • round() いらないんじゃないか?
    と感じられると思います。

skip がないバージョン

  • skip を廃止してスッキリした見た目に。状態を持たずに書けるなら、そのほうが後から読み直したときに理解しやすくなります。
  • round() 追放。表示上の整形は出力時に行えば十分なので、計算途中での丸めをやめました。
short.py
def make_program(max_weight, start, end):
    weights = [w / 10 for w in range(start * 10, end * 10 + 1, 25)]

    day = 1
    for i in range(0, len(weights)-2, 1):
        print(f"day: {day}")
        for w in weights[i:i+3]:
            print(f"  {w} kg ({w/max_weight*100:.1f}%)")
        day += 1

make_program(60, 40, 120)

だいぶ良くなっていますが、出力形式を増やしたくなるたびに、表示部分をコピペして調整するのは面倒です。
次はデータと出力を分離し、形式追加を容易にします。

データと出力の分離

最後に、出力形式の変更に強い形へ整えます。

  • データと出力の分離によって書式の変更をしやすく
  • round()が復活していますが、出力部では数値を弄くらず単に出力するだけの方が望ましいのでこっちのほうが良いのではないか
separated.py
""" 機械的にウエイトトレーニングのメニューを組んでくれるプログラム
    day ごとに 2.5 kgウエイトが増えていく。
    一日は3セット
"""

def make_program(max_weight, start, end):
    """
    max_weight: 開始時の最大重量 kg
    start:      開始時に無理なく挙がる重量 kg
    end:        目標にする重量 kg

    戻り値:
        [
            {
                "day": 1,
                "sets": [(60.0, 75.0), (62.5, 78.12), (65.0, 81.25)],
            },
            ...
        ]
    """
    weights = [round(w / 1000, 1) for w in range(start * 1000, end * 1000 + 1, 2500)]

    program = []
    day = 1

    for i in range(0, len(weights) - 2, 1):
        day_sets = []
        for w in weights[i:i + 3]:
            percent = round((w / max_weight) * 100, 2)
            day_sets.append((w, percent))

        program.append({
            "day": day,
            "sets": day_sets,
        })
        day += 1

    return program


def print_program(program):
    for day_data in program:
        print(f"  day: {day_data['day']}")
        for weight, percent in day_data["sets"]:
            print(f"   {weight} kg")
            print(f"    {percent} %")


if __name__ == "__main__":
    program = make_program(60, 40, 120)
    print_program(program)

終わりに

日常生活で電卓代わりに REPL を使い、プログラムに向かって漸進的改造していくことで問題解決&再利用しやすくできる。
最近は生成 AI がこのぐらいの問題は解いてくれるのでいらないといえばいらないんですが、
そういう練習法もあることを自分なりに記録しました。

Link

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?