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連載へどうぞ。