株式会社nonoi
Python

Python RTA:とりあえず動かして、基本操作を一周する

Pythonでちょっとした作業を書きたいなら、AIに頼めばサッとコードが出てくる。便利な時代になったもんだ。

それでも、あえて自分の手で書いてみたい。動かして、仕組みを覚えて、自分で直せるようになりたい。そんな初学者向けに、この記事を書いてみた。

Pythonは、とにかく何でもできる。万能すぎて、何から始めたらいいか訳がわからなくなる人もいると思う。俺もそっち側だ。

今回は、実務で最低限知っておいてほしいことを、コードを書く、動かす、値を見る、の順で一周する。説明はその場で必要な分だけ挟む。手元で動いたら次へ。そういうRTAでいこう。

スタート地点

Python 3.12以降が動く環境を用意する。追加ライブラリは使わない。環境がまだなら開発環境の用意を先に済ませておこう。

作業フォルダにrta.pyを作り、UTF-8で保存する。仮想環境のPythonで、次のように実行する。

python rta.py

基本は同じファイルを書き換えていく。 ログを用意した後は、指示がなければrun_work()の定義だけを丸ごと差し替える。新しい関数がある場合は、それもmain()より上に置く。コードブロックを全部つなげる必要はない。

途中の状態を残したい人向けに、各区間の実行ファイル一式(ZIP)も置いた。こちらは一つずつ独立して動く。好きなところから再開しておk。

01. mainを作り、作業用の関数を呼んで表示する

最初はこれをファイル全体として保存する。

def run_work():
    print("作業関数まで来た")


def main():
    print("main開始")
    run_work()


if __name__ == "__main__":
    main()

実行結果。

main開始
作業関数まで来た

動いた。ヨシ。

defで関数を定義し、run_work()で呼ぶ。関数を定義しただけでは実行されない。mainという名前にも、自動で呼ばれる特別な力はない。末尾のmain()が呼び出している。

if __name__ == "__main__":は、このファイルを直接実行したときに中を動かすお約束。他のファイルから部品として読み込まれたときには、ここを動かさずに済む。仕組みは公式の説明にある。今はこの形で置いておけばOK。

表示はprintfではなく、Python標準のprint()を使う。数値も渡せるし、print(f"値={value}")のように変数を文章へ埋め込むこともできる。末尾には通常、自動で改行が付く。

字下げはスペース四つでそろえる。Pythonでは、この字下げで処理のまとまりを決めている。見た目を整えるだけのものではないので、ここはエディタに任せよう。

02. loggingを入れる。以後、値はこいつで見る

ファイル全体を次に差し替える。これが以後の基本形になる。

import logging

logger = logging.getLogger(__name__)


def run_work():
    logger.info("ログキター")
    logger.info("値=%s", 123)
    logger.debug("細かい確認用のログ")


def main():
    logging.basicConfig(
        level=logging.INFO,
        format="%(levelname)s %(message)s",
    )
    run_work()


if __name__ == "__main__":
    main()
INFO ログキター
INFO 値=123

logging.INFOを指定すると、INFO以上の重要度を表示する。DEBUGは表示されない。level=logging.DEBUGに変えて、三行目も出ることを確認したらINFOへ戻しておく。

logger.info("値=%s", value)の%sへ、後ろの値が入る。空文字列や空白まで見たいときは%rを使う。あとで文字列の確認に使うので覚えておこう。

用途はざっくり、通常の動作確認がinfo、詳しい調査がdebug、注意事項がwarning、処理の失敗がerror。この程度から始めればよい。ログの用途とレベルは公式HOWTOで確認できる。

この設定のログは、画面の標準エラー出力へ流れる。ファイルに保存しているわけではない。print()の標準出力とは別なので、両方を混ぜると表示順が前後する場合もある。以後の動作確認はログにそろえる。

03. intで四則演算する

ここから、基本形のrun_work()だけを差し替える。

def run_work():
    left = 7
    right = 2

    logger.info("left=%s right=%s", left, right)
    logger.info("足す=%s", left + right)
    logger.info("引く=%s", left - right)
    logger.info("掛ける=%s", left * right)
    logger.info("割る=%s", left / right)
    logger.info("/の結果の型=%s", type(left / right).__name__)
    logger.info("商=%s 余り=%s", left // right, left % right)

四則演算の結果は、順に9、5、14、3.5。

left = 7で整数の値を名前に結び付ける。型名はint。C系のint left = 7;のような宣言は、ここでは要らない。実際の型はtype(left)で見られる。

整数同士でも、/の結果はfloatになる。整数の商が必要なら//、余りは%。この例の商は3、余りは1だ。//は負の無限大側へ丸めるので、負数を使うときは単なる小数部分の切り捨てと混同しないように。

まずleftとrightを別の整数に変えて、答えを予想してから実行してみる。0で割ったときの話は、例外処理の区間でやる。

04. floatでも四則演算する

def run_work():
    left = 7.5
    right = 2.0

    logger.info("足す=%s", left + right)
    logger.info("引く=%s", left - right)
    logger.info("掛ける=%s", left * right)
    logger.info("割る=%s", left / right)
    logger.info("0.1 + 0.2 = %r", 0.1 + 0.2)
    logger.info("表示を小数2桁にする: %.2f", 0.1 + 0.2)

四則演算の結果は9.5、5.5、15.0、3.75。

その後に0.30000000000000004が出る。壊れたわけではない。floatは多くの小数を近似値で持つので、こういう端数が出る。詳しくは浮動小数点の公式解説にある。

%.2fは表示を小数二桁にそろえる指定で、今回は0.30になる。表示を変えても、内部の計算が厳密になるわけではない。金額の端数処理などが必要になったら、整数で扱う単位やDecimalの利用を別途決める。ここでは、こういう値が出ることを実物で見ておけばよい。

05. 文字列を入れる、足す、挿す、消す

Pythonの文字列の型名はstr。stringという名前で型を宣言する必要はない。

def run_work():
    text = "Python"
    logger.info("最初=%r", text)

    text += " RTA"
    logger.info("追加=%r", text)

    text = text[:6] + "入門" + text[6:]
    logger.info("挿入=%r", text)

    text = text.replace("入門", "", 1)
    logger.info("削除=%r", text)

    text = text[:-4]
    logger.info("末尾4文字を削除=%r", text)
    logger.info("先頭=%r 文字数=%s", text[0], len(text))

変化はこうなる。

'Python'
'Python RTA'
'Python入門 RTA'
'Python RTA'
'Python'

text[:6]は先頭から位置6の手前まで、text[6:]は位置6から末尾まで。添字は0から始まる。[:-4]なら末尾四文字の手前までを取り出せる。

replace("入門", "", 1)は、最初に見つかった一か所を空文字列へ置き換える。末尾の1を省略すると、見つかった箇所をすべて置き換える。

ここでのお約束。文字列そのものは書き換えられない。新しい文字列を作り、text = ...で受け直している。text.replace(...)を呼ぶだけでは、元のtextは変わらない。text[0] = "X"もできない。

%rで付く引用符は、値を確認するための表示だ。文字列の中に引用符を追加したわけではない。

06. listを作る、足す、変える、消す

def run_work():
    items = ["A", "B", "C"]
    logger.info("最初=%s", items)

    items.append("D")
    logger.info("末尾へ追加=%s", items)

    items.insert(1, "X")
    logger.info("位置1へ挿入=%s", items)

    items[0] = "AA"
    logger.info("先頭を変更=%s", items)

    items.remove("B")
    logger.info("値Bを削除=%s", items)

    removed = items.pop()
    logger.info("末尾から取り出した値=%s 残り=%s", removed, items)

    del items[1]
    logger.info("位置1を削除=%s", items)

最後のリストは['AA', 'C']になる。

  • append()は末尾に一件追加する。
  • insert()は指定した位置の前に挿入する。
  • remove()は一致した最初の一件を削除する。
  • pop()は削除した値を返す。位置を省略すると末尾。
  • del items[1]は位置を指定して削除する。

文字列と違って、リストの要素はその場で変更できる。各操作は公式のデータ構造の説明でも確認できる。

items = items.append("D")とは書かない。append()の戻り値はNoneなので、せっかくのitemsがNoneになる。追加したいだけなら、今回のように呼べばOK。

存在しない値をremove()するとValueError、範囲外の位置を取り出すとIndexErrorになる。後の例外処理で受け止められる。

07. dictも作る、足す、変える、消す

dictは、項目名と値を組にして持つ。連想配列の出番だ。

def run_work():
    settings = {"name": "Python", "count": 1}
    logger.info("最初=%s", settings)

    settings["mode"] = "rta"
    logger.info("項目追加=%s", settings)

    settings["count"] = 2
    logger.info("値を変更=%s", settings)

    del settings["mode"]
    logger.info("項目削除=%s", settings)

    name = settings.pop("name")
    logger.info("取り出した値=%r 残り=%s", name, settings)
    logger.info("ない項目=%r", settings.get("missing", "未設定"))

最後に残るのは{'count': 2}。新しいキーに代入すれば追加、既存のキーなら更新になる。

settings["missing"]は、項目がなければKeyError。get("missing", "未設定")なら、項目がない場合に指定した値を返す。必須の項目まで何でもget()で済ませると、不備を見落とすので使い分けよう。

08. 関数へ引数を渡して、受け取った値を見る

ここからは、追加の関数とrun_work()をセットで置く。基本形のmain()はそのままでよい。

def show_arguments(name, count):
    logger.info("受け取ったname=%r count=%s", name, count)


def run_work():
    show_arguments("Python", 3)
    show_arguments(name="RTA", count=2)
INFO 受け取ったname='Python' count=3
INFO 受け取ったname='RTA' count=2

一つ目は順番で渡す位置引数。二つ目は名前で渡すキーワード引数だ。関数側のnameとcountで、その値を使える。

関数の中で定義した名前は、基本的にはその関数の中のもの。結果を呼び出し元で使いたければreturnで返す。次にやる。

09. 引数でもらった数値で四則演算する

def show_arithmetic(left, right):
    logger.info("受け取り: left=%s right=%s", left, right)
    logger.info("足す=%s", left + right)
    logger.info("引く=%s", left - right)
    logger.info("掛ける=%s", left * right)
    logger.info("割る=%s", left / right)


def run_work():
    show_arithmetic(10, 4)
    show_arithmetic(left=7.5, right=2.0)

最初は14、6、40、2.5。次は9.5、5.5、15.0、3.75になる。

関数の中に数値を固定せず、外から渡せるようになった。同じ仕事を別の値で繰り返すなら、まあこうしておけば使い回せる。

10. 足すか引くかも引数で決める

def calculate(left, right, operation):
    if operation == "add":
        return left + right
    elif operation == "sub":
        return left - right
    else:
        raise ValueError(f"知らない演算です: {operation}")


def run_work():
    answer = calculate(10, 3, "add")
    logger.info("足し算の答え=%s", answer)

    answer = calculate(10, 3, operation="sub")
    logger.info("引き算の答え=%s", answer)

答えは13と7。returnした値を、呼び出した側のanswerで受け取っている。

ここでifを先行使用した。「addなら足す、subなら引く、それ以外はエラー」という処理だ。条件分岐は後でまとめて触る。

operationを"plus"に変えて動かしてみると、ValueErrorで止まる。これは意図した失敗。知らない指定を、勝手に足し算として扱わないためだ。確認できたら"add"へ戻しておこう。

11. forでlistを一件ずつ取り出す

def run_work():
    items = ["A", "B", "C"]

    for value in items:
        logger.info("値=%s", value)

    for index, value in enumerate(items):
        logger.info("位置=%s 値=%s", index, value)

一周目はA、B、C。二周目は位置0、1、2と、それぞれの値が表示される。

値だけ使うならfor value in items:でよい。位置も必要なときにenumerate()を使う。毎回カウンタを自作しなくて済む。

リストが空なら、中の処理は一度も実行されない。items = []に変えて確認したら元へ戻す。

12. whileでもlistを取り出す

次はwhile。条件が成り立っている間、処理を繰り返す。

def run_work():
    items = ["A", "B", "C"]
    index = 0

    while index < len(items):
        logger.info("位置=%s 値=%s", index, items[index])
        index += 1

    pending = ["A", "B", "C"]
    while pending:
        value = pending.pop(0)
        logger.info("取り出した値=%s 残り=%s", value, pending)

前半は位置を自分で進める。index += 1を忘れると、同じ位置のままループが終わらない。ここの一行は飾りではない。

後半は先頭から一件取り出し、空になるまで処理する。空のリストは条件判定で偽になるので、while pending:と書ける。このやり方では元のpendingは空になる。件数が多いキューを扱うなら、後でcollections.dequeも調べるとよい。

普通に一覧を読むだけならfor、条件が変わるまで回すならwhile。まずこの使い分けで進めよう。

13. ループ中にlistを書き換えて、罠を踏む

ここは実際に値を見てほしい。リストを回しながら同じリストを触ると、追加した要素まで拾ったり、削除後の要素を飛ばしたりする。

追加した要素も処理される

def run_work():
    items = [1, 2, 3]

    for value in items:
        logger.info("取り出した値=%s", value)
        if value == 2:
            items.append(4)
            logger.info("4を追加: %s", items)

    logger.info("終了: %s", items)

取り出す値は1, 2, 3, 4。最初は三件だったのに、追加した4も処理される。

毎回無条件に追加し続けると、終わりが後ろへ逃げ続ける。今回は2を取り出したときだけ一件追加するので、ちゃんと終わる。

まだ取り出していない要素を書き換える

def run_work():
    items = [1, 2, 3]

    for index, value in enumerate(items):
        if index == 0:
            items[1] = 20
        logger.info("位置=%s 値=%s list=%s", index, value, items)

    for value in items:
        value = 999
    logger.info("変数valueだけを代入し直した後=%s", items)

最初のループで取り出す値は1, 20, 3。次に読む場所の中身を変更したので、その値を拾う。

一方、後半のvalue = 999は、変数に別の値を入れただけ。リストは[1, 20, 3]のままだ。整数の要素を変えたいならitems[index] = ...とする。この差は地味にハマる。

削除すると、次の値が飛ぶ

def run_work():
    items = [1, 2, 3, 4]

    for value in items:
        logger.info("取り出した値=%s", value)
        items.remove(value)
        logger.info("削除後=%s", items)

    logger.info("最後に残ったもの=%s", items)

取り出す値は1, 3。最後は[2, 4]が残る。全部消したつもりが半分残った。なんだこれは。

1を消すと、2が位置0へ詰まる。しかしループは次の位置1へ進むので、そこに移った3を拾う。同じことがもう一度起き、2と4が処理されず残る。

Pythonの可変シーケンスのイテレータは、このように位置を進めながら要素を読む。仕様上の説明は組み込み型のリファレンスにある。リストを書き換えると必ず例外になる、という動作ではない。

取り出す一覧と、変更する一覧を分ける

def run_work():
    items = [1, 2, 3, 4]

    for value in items.copy():
        if value % 2 == 0:
            items.remove(value)

    logger.info("偶数を削除した結果=%s", items)

結果は[1, 3]。copy()で複製した外側のリストを回しているので、元のリストから削除しても、取り出し順がずれない。

% 2 == 0は、2で割った余りが0、つまり偶数という条件だ。copy()は浅いコピーなので、要素がさらにリストや辞書になっている場合、その内側のオブジェクトまでは複製しない。今回の整数の例では、ここまででOK。

別のやり方として、残したい値を新しいリストへ追加していく方法もある。普段の実装では、読む一覧を途中で伸び縮みさせない方が追いやすい。

whileで追加し続けると、終了条件が遠のく

最後に、終了しない形を、見張り付きで体験する。

def run_work():
    items = [1, 2, 3]
    index = 0

    while index < len(items):
        if index >= 5:
            logger.warning("5件で強制停止。今の要素数=%s", len(items))
            break

        value = items[index]
        items.append(value + 10)
        logger.info("位置=%s 値=%s 追加後の件数=%s", index, value, len(items))
        index += 1

一件進むたびに要素も一件増える。indexを増やしているのに、len(items)へ追い付かない。見張りが五件で止め、その時点のリストは八件になる。

breakはループから抜ける命令。この見張りは残して実行すること。今日は無限ループ祭りは開催しない。もし別の書き換えで止まらなくなったら、ターミナルでCtrl+Cを押して中断する。

14. if、elif、elseで処理を分ける

def run_work():
    score = 70

    if score >= 80:
        logger.info("80以上")
    elif score >= 60:
        logger.info("60以上80未満")
    else:
        logger.info("60未満")

    if score >= 60 and score < 80:
        logger.info("andで二つの条件を確認")

    items = []
    if not items:
        logger.info("listは空です")

上から条件を調べ、最初に成立した枝を実行する。この例は60以上80未満。scoreを90、30に変え、別の枝も確認してほしい。

比較は==、!=、<、<=、>、>=。代入の=とは別。条件をつなぐならand、どちらかならor、否定はnotを使う。

それぞれ独立したifを並べると、複数の処理が動く場合がある。一つだけ選びたいときはifとelifをつなぐ。条件分岐の構文は公式チュートリアルでも確認できる。

15. try..exceptで例外を受け止める

他言語のtry..catchに当たるものは、Pythonではtry..except。ここも名前を読み替える。

def run_work():
    for text in ["2", "0", "abc"]:
        try:
            number = int(text)
            answer = 10 / number
        except ValueError:
            logger.error("整数にできない値です: %r", text)
        except ZeroDivisionError:
            logger.error("0では割れません")
        else:
            logger.info("10 / %s = %s", number, answer)
INFO 10 / 2 = 5.0
ERROR 0では割れません
ERROR 整数にできない値です: 'abc'

tryの中で例外が起きると、その場所から一致するexceptへ移る。"abc"なら整数への変換で失敗するので、割り算までは進まない。elseは、例外が起きなかったときに実行される。

この例は三種類の結果を見るため、失敗をログに出して次の値へ進む。実際の作業で止めるべきか、除外してよいかは別に決める。何でもexcept: passでなかったことにすると、あとで自分が困る。

調査用に呼び出し経路も見たいなら、exceptの中でlogger.exception("変換に失敗")のように書ける。例外の流れは公式の説明も参照してほしい。

なお、logger.error()は表示するだけで、プログラムを失敗終了させる機能ではない。次の完成形では、失敗時に終了コードも返す。

16. コマンドライン引数を受け取る道具にする

ここまでの引数は、関数に渡す値だった。最後はターミナルから値を渡す。

今度は**別ファイルのrta_calc.py**として、次を丸ごと保存する。main、作業用関数、引数、四則演算、if、例外処理、ログを一本につなぐ。

import argparse
import logging

logger = logging.getLogger(__name__)


def calculate(left, right, operation):
    if operation == "add":
        return left + right
    elif operation == "sub":
        return left - right
    elif operation == "mul":
        return left * right
    elif operation == "div":
        return left / right
    else:
        raise ValueError(f"知らない演算です: {operation}")


def run_work(left, right, operation):
    logger.info("受け取った値: left=%r right=%r operation=%s", left, right, operation)
    answer = calculate(left, right, operation)
    logger.info("答え=%s", answer)
    return answer


def main():
    logging.basicConfig(
        level=logging.INFO,
        format="%(levelname)s %(message)s",
    )

    parser = argparse.ArgumentParser(description="Python RTAの四則演算ツール")
    parser.add_argument("--left", type=float, required=True)
    parser.add_argument("--right", type=float, required=True)
    parser.add_argument("--operation", choices=["add", "sub", "mul", "div"], default="add")
    args = parser.parse_args()

    try:
        run_work(args.left, args.right, args.operation)
    except (ValueError, ZeroDivisionError) as error:
        logger.error("計算できません: %s", error)
        return 1
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

まずは受け取った値と、足し算の答えを確認する。

python rta_calc.py --left 7 --right 2 --operation add
INFO 受け取った値: left=7.0 right=2.0 operation=add
INFO 答え=9.0

引き算、掛け算、割り算も同じ調子で指定する。

python rta_calc.py --left 7 --right 2 --operation sub
python rta_calc.py --left 7 --right 2 --operation mul
python rta_calc.py --left 7 --right 2 --operation div

答えは順に5.0、14.0、3.5。--operationを省略するとaddになる。

ターミナルから渡した値はもともと文字列だが、type=floatによって数値へ変換される。整数だけ受け付けたいならtype=intに変える。使える演算はchoicesに列挙した四つだけ。argparseの機能は公式リファレンスにある。

最後に、不正な指定も試しておく。

python rta_calc.py --left 7 --right 0 --operation div
python rta_calc.py --left abc --right 2 --operation add
python rta_calc.py --left 7 --right 2 --operation plus
python rta_calc.py --help

0で割ると作業関数の中で例外になり、ログを表示して終了コード1で終わる。floatに変換できない引数や未知の演算は、argparseが作業前に拒否して終了コード2。正常終了は0だ。直後にPowerShellなら$LASTEXITCODE、macOSならecho $?で確認できる。

これで、値を受け取り、処理を選び、結果を出し、失敗も知らせる道具になった。ここまで来れば、あとはrun_work()の中身を自分の作業へ置き換えていける。

ゴール。次は自分の作業を一本書く

もう一度ゼロから書くなら、ログ付きのmainと作業関数を置く。小さな値を入れて、途中の結果を見る。入力を引数へ出し、失敗時の扱いを決める。この順で進めれば、最初の一本は組み立てられる。

分からない値が出たら、logger.info("値=%r 型=%s", value, type(value).__name__)で現物を見る。慣れないうちは、頭の中のPythonより手元のPythonの方が正しい。まあ、まず動かして確認しよう。

次にファイルやCSVを扱いたくなったら、実務ツールを作るPython連載へどうぞ。