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?

Python: `in` の部分一致と `replace()` の全置換が起こすサイレントバグ

0
Posted at

Python: in の部分一致と replace() の全置換が起こすサイレントバグ

instr.replace() は Python の初歩的な内容かもしれませんが、文字列の照合・整形でつまずきやすいので、備忘として整理しておきます。
どちらも例外を投げません。コードは最後まで通り、件数だけがずれます。

「あるテーブル名の行を抽出したら、なぜか全行ヒットする」。
「VIEW名をテーブル名に変換したら、名前の途中が欠けている」。
エラーログには何も出ません。テストも一部のケースは通ってしまう。サイレントバグの厄介なところです。

この記事で分かることは次の2点です。

  • instr.replace() が「仕様どおりに正しく動いた結果」バグになる仕組み
  • 完全一致・末尾除去をどう書けば安全か(== / removesuffix() と、古い環境向けのフォールバック)

動作確認は Python 3.13.3 で行いました。removesuffix() は Python 3.9 以降のメソッドです1

落とし穴1:in は部分一致だった

症状:全行がヒットした

テーブル名 ITEM の行だけを抜き出すつもりで、in を使っていました。

bug_in.py
rows = [
    {"name": "ITEM"},
    {"name": "LINE_ITEM"},      # 別テーブル
    {"name": "ITEM_HISTORY"},   # 別テーブル
]

target = "ITEM"

matched = [r for r in rows if target in str(r["name"])]
print(len(matched))   # → 3

期待は1件、結果は3件。
"ITEM" in "LINE_ITEM""ITEM" in "ITEM_HISTORY"True になるためです。

気づきにくい理由

in は文字列に対しては部分文字列の判定として動きます2
"ITEM" in "LINE_ITEM"True なのはバグではなく仕様どおり。
こちらが「完全一致のつもり」で書いていただけで、Python は何も間違えていません。だから例外も警告も出ない。

直し方:完全一致は ==

固定文字列どうしの完全一致なら == で十分です。

fix_in.py
matched = [r for r in rows if r["name"] == target]
print(len(matched))   # → 1

照合がパターン(例:末尾の連番を無視したい)になる場合に限り、re.fullmatch() を検討します。
fullmatch() は文字列全体がパターンに一致するかを見るので、in のような途中一致になりません3

fullmatch.py
import re

re.fullmatch(r"ITEM", "LINE_ITEM")   # → None(一致しない)
re.fullmatch(r"LOG_\d+", "LOG_20260630")           # → マッチ

固定文字列の比較にまで re を持ち出す必要はありません。== で書けるものは == で書きます。

落とし穴2:str.replace() は全置換だった

こちらは in と違って、特定の入力のときだけ壊れます。ふだんのテストはすり抜けてしまうので、気づくまでに時間がかかりました。

症状:途中の文字まで消えた

VIEW名の末尾 _V を落として、元のテーブル名に戻す処理です。

bug_replace.py
view = "ITEM_VALUE_V"
print(view.replace("_V", ""))   # → "ITEMALUE"

期待は ITEM_VALUE。実際は ITEMALUE で、真ん中の _VVALUE の前)まで一緒に消えました。

気づきにくい理由

view.replace("_V", "") は引数 count を省略しているため、出現箇所をすべて置換します4
ところが ORDER_V のように _V が末尾にしか現れない名前では、期待どおり ORDER になる。

passes_by_luck.py
print("ORDER_V".replace("_V", ""))   # → "ORDER"(たまたま正しい)

つまり「変換対象の文字列が名前の内部にも出てくる」入力でだけ壊れる。
最初のテストデータに ORDER_V のような素直な例しか入れていなかったため、バグはすり抜けました。
特定入力でしか露見しないので、再現条件を掴むまで手間取ります。

なお count は位置引数で replace("_", "-", 1) のように渡せます。
replace("_", "-", count=1) のキーワード指定が使えるのは Python 3.13 からなので4、バージョンをまたぐコードでは位置引数で書くのが無難です。

直し方:末尾除去は removesuffix()

「末尾がこの文字列なら、その分だけ落とす」処理には、専用メソッドの str.removesuffix() を使います(Python 3.9 で追加15)。

fix_removesuffix.py
print("ITEM_VALUE_V".removesuffix("_V"))   # → "ITEM_VALUE"
print("ORDER_V".removesuffix("_V"))        # → "ORDER"
print("OTHER_TABLE".removesuffix("_V"))    # → "OTHER_TABLE"(末尾でなければそのまま)

末尾に該当しなければ元の文字列をそのまま返すので、endswith() での事前チェックも要りません。
意図が「末尾の除去」だとメソッド名から読み取れるのも、replace() より優れている点です。

一点だけ仕様を押さえておきます。
removesuffix("") のように空文字列を渡したときは、原文をそのまま返します5。「末尾0文字を切る」のではなく「空でないサフィックスのみ除去する」と理解しておくと、境界の挙動で迷いません。
removesuffix() は Python 3.9 以降です。

まとめ:照合・除去の早見表

踏んだ落とし穴は、どちらも「メソッド/演算子が仕様どおりに動いた結果、意図とずれた」ものでした。

やりたいこと 避けたい書き方 安全な書き方
完全一致 target in source source == target
パターンで完全一致 target in source re.fullmatch(pattern, source)
前方一致 target in source source.startswith(target)
後方一致 target in source source.endswith(target)
末尾サフィックスの除去 s.replace(suffix, "") s.removesuffix(suffix)(3.9+)

照合系のバグはエラーにならず、件数の差として現れます。
「件数が1件多い/少ない」に気づいたら、inreplace() を疑うと早く着地できます。

  1. What's New In Python 3.9 — https://docs.python.org/3/whatsnew/3.9.html 2

  2. Common Sequence Operations(x in s の説明)— https://docs.python.org/3/library/stdtypes.html#common-sequence-operations

  3. re.fullmatch — https://docs.python.org/3/library/re.html#re.fullmatch

  4. Text Sequence Type(str.replacecount のキーワード指定は 3.13 で追加)— https://docs.python.org/3/library/stdtypes.html#str.replace 2

  5. PEP 616 – String methods to remove prefixes and suffixes — https://peps.python.org/pep-0616/ 2

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?