
【AI文芸 玲奈様のプログラミング講座】Pythonの型非明示によるバグ
多分今回のは実用的な記事だと思います。
Part 1 概論
「Pythonの型非明示によるバグについて、詳細に語れって? いいわ、付き合ってあげる」
玲奈は、ゆっくりと指を組みながら話し始める。
「Pythonは動的型付け言語ね。つまり、変数の型を明示しなくても動くわけ。でも、それが原因で地雷を踏むことになるのよ。たとえば、こういうコードがあったとする」
def add(a, b):
return a + b
print(add(2, 3)) # 5
print(add("2", "3")) # "23"
print(add(2, "3")) # TypeError
「見ての通り、add(2, 3) は整数同士だから問題なし、add("2", "3") も文字列の結合として動く。でも add(2, "3") でクラッシュ。だって、Pythonは引数の型を決め打ちしてないから、どんな値が入ってくるか分からない。型ヒントを使っても、あくまでヒントでしかなく、静的な型チェックがない限り実行時エラーは防げない」
玲奈は一度間を置いて、スマホを弄るふりをしながら続ける。
「こういうのは mypy みたいなツールを使えば事前に検出できるけど、実際のプロジェクトでは 'とりあえず動けばいい' って書かれることが多いのが現実よね。だから、こんなバグもよく見るわ」
def process_data(data):
return data.strip() # 文字列の前後の空白を削除
print(process_data(" hello ")) # "hello"
print(process_data(None)) # AttributeError: 'NoneType' object has no attribute 'strip'
「None を渡されたら即死。静的型付けの言語ならコンパイル時に気づけるけど、Pythonだと実行するまで分からない」
玲奈は微かに笑いながら、「でも、これが一番ヤバいわね」と言いながら、次のコードを書いた。
def multiply_by_two(value):
return value * 2
print(multiply_by_two(5)) # 10
print(multiply_by_two("5")) # "55" (文字列の繰り返し)
「このバグの厄介なところは、エラーにならないこと。期待と違う動作をしているのに、クラッシュしないから、気づかないまま進行するのよ。しかも、Pythonのコードはこういう型の曖昧さが積み重なってるから、気づいた頃には大規模なデバッグが必要になる」
玲奈は腕を組み、淡々と言う。
「結論? 型を信用しないこと。型ヒントをつける、mypy を導入する、テストを書く、どれも大事。でも、最も重要なのは 'Pythonは型が自由なぶん、型のミスが発生しやすい' って意識することよ。油断したら、きっと痛い目を見るわ」
Part 2 JSONのパース
「勘違いじゃないわ。Pythonの型非明示のせいで、JSONのパース時に地雷を踏むのは、かなりよくあることよ」
玲奈はスマホを取り出し、手早くメモを開きながら話し始める。
1. JSONはすべてが"文字列"として扱われがち
「まず前提として、JSONはテキストデータだから、元の型情報は保持してないの。数値も文字列も、JSONとしてはただのテキスト。だから、Pythonに読み込ませたときの解釈次第でバグる」
import json
data = '{"age": "25"}' # ageが文字列になっている
parsed = json.loads(data)
print(parsed["age"] + 5) # TypeError: can only concatenate str (not "int") to str
「APIのレスポンスなんかだと、"25" のように数値が文字列として入っていることがよくあるの。でも、Pythonは型推論をしないから、そのまま str として処理される。数値計算しようとすると、エラーになるのよ」
2. None と null の誤解
「JSONの null はPythonの None に変換されるんだけど、これは別の型問題を引き起こす」
data = '{"value": null}'
parsed = json.loads(data)
if parsed["value"] == 0:
print("ゼロと同じ?")
「実行しても何も表示されない。だって、Pythonでは None と 0 は別物。でも、他の言語やデータベースでは null == 0 みたいな扱いをするものもあるから、ここでバグる可能性がある」
3. True / False の変換ミス
「JSONの true / false は、Pythonの True / False にちゃんと変換される。でも、問題は文字列として入ってる場合」
data = '{"is_active": "false"}' # 本当は boolean のつもり
parsed = json.loads(data)
if parsed["is_active"]:
print("有効") # 実行される(!)
「このバグのヤバいところは、Pythonでは 空でない文字列は True になること。だから、本当は False のつもりなのに、if "false" は True と判定される。ブール値を文字列として扱っていると、真逆の動作をする危険がある」
4. 数値の型変換 (int vs float)
「JSONでは 1 も 1.0 も数値としては一緒に見えるけど、Python側では int と float の区別がある」
data = '{"score": 95.0}'
parsed = json.loads(data)
if parsed["score"] == 95:
print("一致") # 実行される
print(type(parsed["score"])) # <class 'float'>
「このコードは if parsed["score"] == 95 で True になるから、一見問題ないように見えるわね。でも、型をチェックすると float になってる。例えば、辞書のキーに使おうとすると、意図しない挙動になる」
scores = {95: "A", 96: "B"}
print(scores.get(parsed["score"])) # None になる
「parsed["score"] は 95.0(float)だから、int の 95 とは別物として扱われるの。つまり、辞書のキーとして一致しない」
5. Decimal との相性
「Pythonには decimal.Decimal っていう高精度の数値型があるんだけど、JSONをパースすると float になってしまって、精度が失われることがある」
from decimal import Decimal
import json
data = '{"price": 19.99}'
parsed = json.loads(data)
price = Decimal(parsed["price"]) # 精度が落ちる可能性あり
print(price) # 19.990000000000001 になることがある
「これは float の丸め誤差の問題ね。金融系の処理では Decimal を使うべきだけど、JSONを経由すると float に変換されるから、誤差が入り込むリスクがある」
結論
玲奈はコーヒーを一口飲み、静かに話を締めくくる。
「JSONのパースは単純そうに見えるけど、Pythonの型の曖昧さのせいで思ってたのと違うデータ型になることがよくある。対策? ちゃんとやるなら、こういう方法があるわ」
対策
型を明示的に変換する parsed["age"] = int(parsed["age"]) # 文字列だったら `int` にする
None を意識する if parsed["value"] is None: print("デフォルト値をセット")
ブール値を str で扱わない if str(parsed["is_active"]).lower() == "true": is_active = True else: is_active = False
float / int の型チェック if isinstance(parsed["score"], float): parsed["score"] = int(parsed["score"])
json.loads(data, parse_float=Decimal) を使う import json from decimal import Decimal data = '{"price": 19.99}' parsed = json.loads(data, parse_float=Decimal) print(parsed["price"]) # Decimal('19.99')
「Pythonは便利だけど、こういう型のゆらぎに気をつけないと、静かにバグが潜むのよ。ま、慎重にやりなさいってことね」