mcpField
P
python-gotchas

python-gotchas

github.com/josiahsiegel開発5971 回インストール19 週間前に更新
リポジトリ

python-gotchas について

Python開発者がデバッグ、コードレビュー、バグ予防に使うリファレンススキルです。よくある落とし穴の原因と修正方法を説明します。

主な機能

  1. 01可変デフォルト引数の問題と修正
  2. 02リストや辞書の反復中の変更に関する注意
  3. 03同一性と値の比較の区別
  4. 04クロージャの束縛、スコープ、浮動小数点精度
  5. 05例外、インポート、クラス属性の注意点

活用シーン

  1. 01Pythonコードの予期しない動作を調査
  2. 02微妙なバグを見つけるコードレビュー
  3. 03Pythonの評価規則とスコープの学習

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/josiahsiegel/claude-plugin-marketplace --skill python-gotchas

インストール後、Claude Code で/python-gotchasを入力して読み込まれたか確認します。

詳細

クイックリファレンス

注意点問題解決策
ミュータブルなデフォルト値def f(x=[])Noneを使い、関数内で作成する
イテレーション中の変更項目がスキップされるコピーitems[:]を反復処理する
isと==同一性と値Noneに対してのみisを使う
遅延束縛lambda: iは変数をキャプチャするlambda i=i: i
浮動小数点の精度0.1 + 0.2 != 0.3math.isclose()
辞書の変更RuntimeErrorlist(d.keys())
クラス属性ミュータブルな値が共有される__init__で初期化する
偽と評価される値例
ブール値False
NoneNone
数値0、0.0、0j
空のコレクション""、[]、{}、set()
スコープ規則順序
LEGBローカル → 外側のスコープ → グローバル → 組み込み
globalモジュールレベルの変数にアクセスする
nonlocal外側の関数の変数にアクセスする

このスキルを使う場面

デバッグと予防に使用します:

  • コードが予期しない動作をする理由を理解する
  • Pythonでよくある落とし穴を避ける
  • 微妙なバグがないかコードをレビューする
  • Pythonの評価規則を学ぶ
  • ミュータブルなデフォルト引数を修正する

関連スキル:

  • 基礎については、python-fundamentals-313を参照してください
  • テストについては、python-testingを参照してください
  • 型ヒントについては、python-type-hintsを参照してください

Pythonでよくある注意点と落とし穴

概要

Pythonには、経験の浅い開発者からベテランまでがつまずきやすい、よく知られた落とし穴がいくつかあります。こうした注意点を理解すると、微妙なバグや予期しない動作を防げます。

1. ミュータブルなデフォルト引数

問題
# BAD: Mutable default argument
def add_item(item, items=[]):
    items.append(item)
    return items

# Unexpected behavior!
print(add_item("a"))  # ['a']
print(add_item("b"))  # ['a', 'b'] - NOT ['b']!
print(add_item("c"))  # ['a', 'b', 'c']
発生する理由

デフォルト引数は、関数が呼び出されるたびではなく、関数の定義時に一度だけ評価されます。同じリストオブジェクトが、すべての呼び出しで再利用されます。

解決策
# GOOD: Use None as default
def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

# Works correctly
print(add_item("a"))  # ['a']
print(add_item("b"))  # ['b']
print(add_item("c"))  # ['c']
その他のミュータブルなデフォルト値
# BAD: All mutable types have this issue
def bad_dict(data={}): ...
def bad_set(data=set()): ...
def bad_class(config=SomeClass()): ...

# GOOD: Always use None
def good_dict(data=None):
    if data is None:
        data = {}
    return data

def good_set(data=None):
    if data is None:
        data = set()
    return data

2. 反復処理中にリストを変更する

問題
# BAD: Modifying list during iteration
numbers = [1, 2, 3, 4, 5, 6]
for num in numbers:
    if num % 2 == 0:
        numbers.remove(num)

print(numbers)  # [1, 3, 5] - missed 4!
発生する理由

イテレーターは内部でインデックスを使用します。項目を削除すると、それ以降のすべてのインデックスがずれるため、項目がスキップされます。

解決策
# GOOD: Iterate over a copy
numbers = [1, 2, 3, 4, 5, 6]
for num in numbers[:]:  # Slice creates a copy
    if num % 2 == 0:
        numbers.remove(num)
print(numbers)  # [1, 3, 5]

# GOOD: Use list comprehension (preferred)
numbers = [1, 2, 3, 4, 5, 6]
numbers = [num for num in numbers if num % 2 != 0]
print(numbers)  # [1, 3, 5]

# GOOD: Use filter
numbers = [1, 2, 3, 4, 5, 6]
numbers = list(filter(lambda x: x % 2 != 0, numbers))
print(numbers)  # [1, 3, 5]

# GOOD: Iterate backwards (for in-place modification)
numbers = [1, 2, 3, 4, 5, 6]
for i in range(len(numbers) - 1, -1, -1):
    if numbers[i] % 2 == 0:
        del numbers[i]
print(numbers)  # [1, 3, 5]

3. isと==

問題
# Comparing values vs identity
a = [1, 2, 3]
b = [1, 2, 3]

print(a == b)  # True - same values
print(a is b)  # False - different objects

# Integer interning gotcha
x = 256
y = 256
print(x is y)  # True (integers -5 to 256 are interned)

x = 257
y = 257
print(x is y)  # False! (outside interning range)
規則
  • 値を比較するには==を使います
  • isは同一性(None、True、Falseなどのシングルトン)に対してのみ使います
# GOOD: Correct usage
if value is None:
    ...

if value == other_value:
    ...

# BAD: Don't use `is` for value comparison
if value is 0:  # Wrong!
    ...

4. 変数のスコープ(LEGB)

問題
# Closure gotcha
functions = []
for i in range(3):
    functions.append(lambda: i)

# All return the same value!
print([f() for f in functions])  # [2, 2, 2]
発生する理由

ラムダはiの値ではなく、変数をキャプチャします。ラムダが呼び出される頃には、iは2になっています。

解決策
# GOOD: Capture value with default argument
functions = []
for i in range(3):
    functions.append(lambda i=i: i)  # Default arg captures value
print([f() for f in functions])  # [0, 1, 2]

# GOOD: Use functools.partial
from functools import partial

def return_value(x):
    return x

functions = [partial(return_value, i) for i in range(3)]
print([f() for f in functions])  # [0, 1, 2]
UnboundLocalError
# BAD: This raises UnboundLocalError
x = 10

def increment():
    x = x + 1  # Error! x is local but used before assignment
    return x

# GOOD: Use global (sparingly)
def increment():
    global x
    x = x + 1
    return x

# BETTER: Avoid global, pass as parameter
def increment(x):
    return x + 1

5. 文字列の連結

暗黙の連結に関する注意点
# Missing comma creates concatenation
items = [
    "apple"
    "banana"  # Oops! Missing comma
    "cherry"
]
print(items)  # ['applebanana', 'cherry']

# CORRECT
items = [
    "apple",
    "banana",
    "cherry",
]
型の混在
# BAD: Can't concatenate str and int
name = "User"
count = 42
# message = "Hello " + name + ", you have " + count + " messages"  # TypeError!

# GOOD: Use f-strings
message = f"Hello {name}, you have {count} messages"

# GOOD: Use str()
message = "Hello " + name + ", you have " + str(count) + " messages"

6. クロージャの遅延束縛

問題
# Class method gotcha
class MyClass:
    def __init__(self, callbacks=[]):  # BAD: Mutable default!
        self.callbacks = callbacks

    def add_callback(self, func):
        self.callbacks.append(func)

obj1 = MyClass()
obj2 = MyClass()
obj1.add_callback(lambda: print("Hello"))

# obj2 also has the callback!
print(len(obj2.callbacks))  # 1
解決策
class MyClass:
    def __init__(self, callbacks=None):
        self.callbacks = callbacks if callbacks is not None else []

    def add_callback(self, func):
        self.callbacks.append(func)

7. ブール値の評価

偽と評価される値
# These are all falsy
falsy_values = [
    False,
    None,
    0,
    0.0,
    0j,
    "",
    [],
    {},
    set(),
    range(0),
]

# Gotcha: Empty collections are falsy
data = []
if data:
    print("Has data")  # Not printed
else:
    print("No data")   # Printed

# But None and empty are different!
if data is None:
    print("Is None")      # Not printed
elif data == []:
    print("Is empty list")  # Printed
明示的なチェック
# BAD: Ambiguous check
def process(items):
    if not items:  # Could be None OR empty
        return

# GOOD: Be explicit about what you're checking
def process(items):
    if items is None:
        raise ValueError("items cannot be None")
    if len(items) == 0:
        return  # Early return for empty list

8. 浮動小数点の精度

問題
# Floating point arithmetic isn't exact
print(0.1 + 0.2)  # 0.30000000000000004
print(0.1 + 0.2 == 0.3)  # False!
解決策
import math
from decimal import Decimal

# GOOD: Use math.isclose for comparisons
print(math.isclose(0.1 + 0.2, 0.3))  # True

# GOOD: Use Decimal for financial calculations
price = Decimal("19.99")
tax = Decimal("0.0875")
total = price * (1 + tax)
print(total)  # 21.739125

# Round appropriately
print(round(total, 2))  # 21.74

9. 例外処理

bare except
# BAD: Catches everything, including KeyboardInterrupt
try:
    risky_operation()
except:
    pass

# BAD: Too broad
try:
    risky_operation()
except Exception:
    pass  # Silently ignores all errors

# GOOD: Catch specific exceptions
try:
    risky_operation()
except ValueError as e:
    logger.error(f"Invalid value: {e}")
except ConnectionError as e:
    logger.error(f"Connection failed: {e}")
    raise  # Re-raise after logging
例外変数のスコープ
# Python 3: Exception variable is deleted after except block
try:
    1 / 0
except ZeroDivisionError as e:
    error = e  # Save if needed
    print(e)

# print(e)  # NameError! e is deleted
print(error)  # OK

10. 辞書キーの順序

現代のPython(3.7以降)
# Since Python 3.7, dicts maintain insertion order
d = {"b": 2, "a": 1, "c": 3}
print(list(d.keys()))  # ['b', 'a', 'c'] - insertion order

# But comparison ignores order
d1 = {"a": 1, "b": 2}
d2 = {"b": 2, "a": 1}
print(d1 == d2)  # True
注意点:反復処理中に辞書を変更する
# BAD: RuntimeError
d = {"a": 1, "b": 2, "c": 3}
for key in d:
    if d[key] == 2:
        del d[key]  # RuntimeError: dictionary changed size during iteration

# GOOD: Iterate over copy of keys
d = {"a": 1, "b": 2, "c": 3}
for key in list(d.keys()):
    if d[key] == 2:
        del d[key]
print(d)  # {'a': 1, 'c': 3}

# GOOD: Dict comprehension
d = {"a": 1, "b": 2, "c": 3}
d = {k: v for k, v in d.items() if v != 2}

11. インポートに関する注意点

循環インポート
# module_a.py
from module_b import func_b  # Fails if module_b imports from module_a

def func_a():
    return func_b()

# SOLUTION: Import inside function
def func_a():
    from module_b import func_b
    return func_b()

# OR: Import module, not function
import module_b

def func_a():
    return module_b.func_b()
モジュール名の衝突
# BAD: File named random.py shadows stdlib
# random.py (your file)
import random  # Imports YOUR file, not stdlib!
random.randint(1, 10)  # AttributeError

# Python 3.13 now warns about this!

12. クラス属性とインスタンス属性

class MyClass:
    shared_list = []  # Class attribute - shared by all instances!

    def add_item(self, item):
        self.shared_list.append(item)

a = MyClass()
b = MyClass()
a.add_item("hello")
print(b.shared_list)  # ['hello'] - Oops!

# GOOD: Initialize in __init__
class MyClass:
    def __init__(self):
        self.items = []  # Instance attribute - unique to each instance

    def add_item(self, item):
        self.items.append(item)

クイックリファレンス

注意点問題解決策
ミュータブルなデフォルト値def f(x=[])Noneを使い、関数内で作成する
反復処理中の変更項目がスキップされるコピーを反復処理するか、内包表記を使う
isと==同一性と等価性None、True、Falseに対してのみisを使う
遅延束縛値ではなく変数をキャプチャするデフォルト引数を使ってキャプチャする
偽と評価される値空はNoneと異なるチェックを明示的に行う
浮動小数点の精度0.1 + 0.2 != 0.3math.isclose()またはDecimalを使う
bare except広範囲に捕捉しすぎる特定の例外を捕捉する
辞書の反復処理反復中に変更できないlist(d.keys())を反復処理する
循環インポートインポートエラー関数内でインポートするか、モジュールをインポートする
クラス属性意図せず共有される__init__で初期化する

コメント