Pythonの基礎から応用にかけて、開発者が陥りやすい「よくある落とし穴・間違い(アンチパターン)」と、その解決策(ベストプラクティス)をを説明します。
0. 基本データ型 (Basic Data Types)
-
boolはintのサブクラスのため、True == 1やFalse == 0が成立します。
is_ready = True
True + True # 2 -> bool は int のサブクラス(subclass)。これは典型的な落とし穴
- 浮動小数点数(
float)は精度誤差があるため、直接的な一致比較(==)は避けるべきです(math.isclose()を推奨)。
ratio = 0.1 + 0.2
ratio # 0.30000000000000004 -> 浮動小数点数の精度問題(precision issue)
-
Noneの判定は、== Noneではなくis Noneを使用してください。
result = None
result is None # True -> None の判定には `is` を推奨
- 変数への代入は「複製(コピー)」ではなく、「同一オブジェクトへの参照」です。
# 代入はコピーではない
a = [1, 2]
b = a
c = a.copy()
a.append(3)
b # [1, 2, 3] -> b と a は同じオブジェクトを参照している
c # [1, 2] -> c は浅いコピー(shallow copy)
-
tuple自体は不変(immutable)ですが、内部に可変(mutable)オブジェクト(リストなど)を含んでいる場合、その中身は変更可能です。
# tuple 自体は不変だが、内部の可変オブジェクトは変更可能
t = ([1, 2], 'fixed')
t[0].append(99)
t # ([1, 2, 99], 'fixed')
-
setは順序がない(unordered)ため、インデックスによる要素へのアクセスはできません。
# set はインデックスでアクセスできない
nums = {1, 2, 3}
# nums[0] # TypeError: 'set' object is not subscriptable
-
dictのキーやsetの要素にするオブジェクトはハッシュ可能(hashable)である必要があります。
# dict のキーはハッシュ可能(hashable)である必要がある
good = {(1, 2): 'point'} # tuple はキーにできる
# bad = {[1, 2]: 'point'} # TypeError: unhashable type: 'list'
1. コア構文と演算子 (Core Syntax & Operator)
- 割り算
1 / 2は浮動小数点の0.5を返します。切り捨て除算(整数)を求める場合は//を使用します。
10 / 3 # 3.3333333333333335 -> 通常の割り算
10 // 3 # 3 -> 切り捨て除算(floor division)
-
bool('0')は空文字ではないためTrueとなります。
# 真偽値(truthiness)
bool(0) # False -> 0 は偽
bool('0') # True -> 空ではない文字列は真
-
0 == Falseは値としては真(True)ですが、両者のデータ型は異なります。
0 == False # True
0 is False # False -> 同一オブジェクト(same object)ではない
-
andやorは必ずしもboolを返すとは限りません。短絡評価(short-circuit evaluation)により、最後に評価された値自体が返されます。
# and / or の戻り値
[] or ['fallback'] # ['fallback'] -> 左辺が偽のため、右辺を返す
'hello' or 'world' # 'hello' -> 左辺が真のため、そのまま左辺を返す
0 and 99 # 0 -> 左辺が偽のため、そのまま左辺を返す
-
isは主にオブジェクトの同一性の確認(identity comparison:特にNoneやシングルトンの判定)に用い、一般的な「値の等価性(equality)」の判定として使うべきではありません。
# == vs is
left = [1, 2]
right = [1, 2]
alias = left
left == right # True -> 内容が等しい(equal in value)
left is right # False -> 同一オブジェクト(same object)ではない
left is alias # True -> alias と left は同じオブジェクトを参照
-
1 < x < 10のようなチェーン比較は有効で、「両方の条件が同時に満たされること」を意味します。
# チェーン比較(chained comparison)
x = 5
1 < x < 10 # True -> 1 < x and x < 10 と等価
2. 制御構文とループ (Control Flow & Loop)
-
range(5)の範囲は[0, 1, 2, 3, 4]であり、末尾の5は含まれません(左閉右開:half-open interval)。
# for + range:最も基本的なカウントループ
total = 0
for i in range(1, 6): # 1 から 5 まで。6 は含まれない
total += i
-
whileループ内でループ条件変数の更新を忘れると、無限ループに陥ります。
# while:終了回数が不明なループに適している
x = 1
while x < 20:
x *= 2 # x の更新を忘れると無限ループに陥る恐れがある
x # 32
-
breakは現在の「最も内側のループ1つ」のみを抜け出します。多重ループ全体を終了するわけではありません。
# ネストされたループの場合、break は内側のループのみを終了させる
pairs = []
for i in range(2):
for j in range(3):
if j == 1:
break # ここでは内側の for j のみ終了する
pairs.append((i, j))
pairs # [(0, 0), (1, 0)]
- 可変リストを
forで反復処理しながら中身を削除・変更すると、要素のスキップなど予期せぬ動作を引き起こします。
nums = [1, 2, 3, 4]
# ループしながら削除すると、インデックスがずれて隣の要素がスキップされてしまう。
# リスト内包表記などで新しいリストを作成するのが適切。
valid_nums = [n for n in nums if n != 2]
- Python特有の
for ... else/while ... elseにおけるelseブロックは、「条件に合わなかった時」ではなく「ループがbreakされずに途切れることなく完走した時」のみ実行されます。
# for-else:break されなかった場合のみ else に入る
num = 29
for d in range(2, int(num ** 0.5) + 1):
if num % d == 0:
print('not prime') # 約数が見つかったときだけ出力される
break
else:
print('prime') # prime 29は素数(prime number)
-
if x:では、空文字列、空リスト、0なども「偽」と判定されます。これが意図せぬ場合(値自体がNoneかどうか判定したい場合)は、if x is not None:と書く必要があります。
# 真偽値のよくある落とし穴
x = 0
if x: pass # 0 は False と見なされるため、ここには入らない
if x is not None: pass # ここには入る。非 None のみを判定したい場合に適している
3. 文字列処理とエンコーディング (Strings & Encoding)
-
input()で受け取った入力はすべてstr型です。数値として扱うにはキャスト(int()など)が必要です。
age_text = '18'
age = int(age_text) # str -> int
-
strip('abc')は完全な部分文字列「'abc'」を削るのではなく、先頭と末尾にある「'a', 'b', 'c'の全文字」を取り除きます。
# strip の意味に注意
text = 'abcHelloCab'
text.strip('abc') # 'HelloC' -> 先頭と末尾にある a/b/c の文字を削除。「部分文字列」という概念は区別しない
-
replace(),upper(),lower()等のメソッドは元の文字列を変更(破壊的変更)せず、新たな文字列を戻り値として返します。
# replace は新しいオブジェクトを返す
msg = 'I like Java'
new_msg = msg.replace('Java', 'Python')
msg # 'I like Java' -> 元の文字列はそのまま
new_msg # 'I like Python'
-
ord()関数は1文字(単一文字)しか受け付けません。数字以外の文字のコードポイントを取得する場合はint('A')ではなくord('A')を使います。
# 文字と数値間の変換
ord('A') # 65 -> 文字をコードポイントに変換
chr(65) # 'A' -> コードポイントを文字に変換
# ord は単一の文字しか処理できない
# ord('AB') # TypeError になる
-
str(テキスト)とbytes(バイト列)は直接連結できません。
# encode / decode:テキスト <-> バイト列
text = 'cafe'
data = text.encode('utf-8') # str -> bytes
data # b'cafe'
decoded = data.decode('utf-8')
decoded # 'cafe' -> bytes -> str
- 引数なしの
split()はすべての連続する空白文字類(空白文字、タブ文字、改行文字など)を区切りにしますが、split(',')のように指定した場合はその文字のみを基準にするため、空文字がリストに含まれるなど挙動が異なります。
# split / join
line = 'name,age,city'
parts = line.split(',') # ['name', 'age', 'city']
# デフォルトの split は任意の連続する空白文字で分割する
sentence = 'too many\tspaces'
sentence.split() # ['too', 'many', 'spaces']
4. コンテナとアルゴリズム基礎 (Containers & Basic Algorithms)
-
a = bによる代入はコピーを作成しません。独立した別のオブジェクトを作りたい場合は.copy()などが必要です。
# コピーの落とし穴(copy pitfall)
a = [1, 2]
b = a
c = a.copy()
a.append(3)
b # [1, 2, 3] -> b も追従して変わる
c # [1, 2] -> c は変わらない
-
remove(x)は値としての一致要素を削除し、pop(i)はインデックスに基づく要素の取得と削除を行います。
nums = [3, 1, 2, 2]
nums.append(5) # 末尾に追加
nums.pop() # 最後の要素を削除して返す -> 5
-
setは順序がないためインデックスアクセス(s[0]など)は不可能です。
user_ids = {'u001', 'u002', 'u002'}
user_ids # {'u001', 'u002'} -> 自動で重複排除
'u001' in user_ids # True -> 所属判定(membership test)は非常に高速
# user_ids[0] # これを実行すると TypeError になる
- 辞書のキーが存在するかわからない場合、直接アクセスする
dict['missing']は例外(KeyError)を送出してしまいます。安全に取得するにはdict.get('missing')を使用してください。
# dict: key-value のマッピング
scores = {'ann': 95, 'bob': 88}
scores['ann'] # 95
scores.get('cindy', 0) # 0 -> KeyError を投げない
- 2次元リストの初期化において
[[0] * 3] * 2と記述すると、各行が同じリストの参照を共有してしまい、1箇所の変更が他の行にも連動してしまいます。
# 2次元リスト初期化の落とし穴
bad = [[0] * 3] * 2
bad[0][1] = 99
bad # [[0, 99, 0], [0, 99, 0]] -> 両方の行が同時に変わる
good = [[0] * 3 for _ in range(2)]
good[0][1] = 99
good # [[0, 99, 0], [0, 0, 0]] -> 正しい書き方
- 二分探索(バイナリサーチ)は、データが「事前にソート(整列)されていること」が絶対条件です。
# 二分探索(binary search):入力がソート済みであることが前提
def binary_search(sorted_items, target):
pass
# 常にソートされている必要がある
binary_search([1, 3, 5, 7, 9], 7) # 3
-
sorted()は安定ソート(stable sort)であるため、複数条件でソートを重ねる際に役立ちます。
# ソート:key で並べ替え
rows = [
{'name': 'ann', 'age': 22},
{'name': 'bob', 'age': 19},
{'name': 'cindy', 'age': 25},
]
sorted(rows, key=lambda row: row['age'])
# age が小さい順にソートされる
5. 関数、スコープと状態の引き渡し (Functions,Scope & Passing State)
- 戻り値の指定(
return)がない関数は、自動的にNoneを返します(printしただけでは戻り値はありません)。
def my_func():
print("hello") # 出力のみで、return はない
result = my_func()
print(result) # None
- リストや辞書などの可変オブジェクトを関数の「デフォルト引数」として設定(例:
def func(acc=[]))してはいけません。関数が呼び出されるたびに同じオブジェクトが使い回されてしまいます。
# 可変デフォルト引数の落とし穴に対する正しい書き方
def append_item(x, bucket=None):
if bucket is None: # 呼び出されるたびに新しいリストを作成
bucket = []
bucket.append(x)
return bucket
append_item(1) # [1]
append_item(2) # [2] -> 前回のリストを引き継がない
- リストや辞書などの可変オブジェクト(特に可変コンテナ)を引数として渡し、関数内でインプレース変更を行うと、呼び出し元のオブジェクトにも変更が反映されます。
# 可変引数が関数内部で変更される例
def add_tag(tags, tag):
tags.append(tag) # 外部のリストをインプレースに変更(in-place mutation)する
return tags
skills = ['python']
add_tag(skills, 'sql')
skills # ['python', 'sql'] -> 外部側のリストも変わっている
- リストをインプレースでソートする
list.sort()は戻り値がNoneです。そのためa = a.sort()と書くとa自体が消失(None)します。
nums = [3, 1, 2]
# result = nums.sort() # 落とし穴:このとき result の値は None になる
sorted_nums = sorted(nums) # 新しいリストを返す
nums # 元のリストはそのまま存在
sorted_nums # ソート済みの新しいリスト
- 再帰関数において、ベースケース(終了条件)がない、または問題の規模が縮小されない場合、最大再帰深度の超過(RecursionError)を招きます。
# 再帰的関数:階乗(factorial)
def factorial(n):
if n == 1:
return 1
return n * factorial(n - 1)
6. ファイル操作・例外・モジュール (Files, Exceptions & Modules)
- ファイルを開く際、
encoding='utf-8'の指定を怠ると、Windowsなどの環境でテキストの文字化けや読み書きエラーの要因となります。
from pathlib import Path
path = Path('test-file.txt')
path.write_text('first line\nsecond line\n', encoding='utf-8')
-
readlines()を使ってファイルを行ごとに読み込むと、各行の末尾に改行文字\nが付着したままになります。
# 基本的な with open の書き方
with open('test-file.txt', 'r', encoding='utf-8') as file_obj:
lines = file_obj.readlines() # 各行には通常、末尾の \n が付いている
lines # ['first line\n', 'second line\n']
- 例外クラスを指定しない単なる
except:(bare except)はキャッチする範囲が広すぎるため、システム終了など本当にキャッチすべきでないエラーまで握り潰して(swallow exceptions)しまいます。
# 複数処理を含む例外対応
def parse_age(text):
try:
age = int(text)
except ValueError as exc: # ValueError を具体的に指定する
return f'bad input: {exc}'
-
tryブロックは可能な限り小さく保ちます。範囲が広すぎるとどの行がエラートリガーになったかわかりづらくなります。
# try ブロックは極力狭める
def safe_divide(a, b):
if b == 0:
return None # 業務上許容されるなら、事前に処理することも可能
return a / b
- 相対パスの起点は「スクリプトファイルが存在するディレクトリ」ではなく、「現在コードを実行した作業ディレクトリ(カレントディレクトリ/CWD)」です。
# 実行環境(カレントディレクトリ)に依存しないよう、
# このファイルからの相対的な絶対パス(プロジェクトルート)を取得する
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent
-
finallyブロックは関数のreturnやbreak、例外の発生にかかわらず確実に実行されます。
def check_file():
try:
return True # たとえここで実行を終了しても...
finally:
pass # このエリアは常に実行される。主にリソースのクリーンアップに利用
7. クラスとオブジェクト指向 (Class & OOP)
- インスタンスメソッドの定義時、最初の引数として
selfを記述し忘れるエラーが多発します。
class Golem:
def __init__(self, name):
self.name = name # インスタンス属性(instance attribute)
- カスタムクラス内で「インスタンス変数」であるべきものを「クラス変数」として変更してしまう落とし穴があります。
# クラス属性に可変オブジェクトを置く際の落とし穴
class BadBag:
items = [] # すべてのインスタンスがこの1つのリストを共有する
def add(self, item):
self.items.append(item)
bag1 = BadBag()
bag2 = BadBag()
bag1.add('python')
bag2.items # ['python'] -> bag2 からも見えてしまう
- クラス属性としてリストや辞書(可変オブジェクト)を定義すると、すべてのインスタンスで同一のオブジェクトが共有されてしまいます。対策はインスタンスが生成されるたびに新しいリストを作成することです。
class GoodBag:
def __init__(self):
# self.items は各インスタンス固有の変数(インスタンス変数)
# インスタンスごとに独立した空リストが割り当てられる
self.items = []
- ごく単純なデータの処理であれば、無理にクラスを用いたオブジェクト指向(OOP)設計を取り入れる必要はありません。
# dataclass: 純粋なデータオブジェクトに適している
from dataclasses import dataclass
@dataclass
class Point:
x: int
y: int
p = Point(3, 5)
p # Point(x=3, y=5)
8. イテレータ・ジェネレータ・デコレータ・正規表現 (Iterators, Generators, Decorators & Regex)
- イテレータは一度消費すると元に戻らない。
# イテレータ(iterator)
it = iter([10, 20, 30])
next(it) # 10
next(it) # 20
print(list(it)) # [30](10, 20 はもう取り出せない)
- Generator(ジェネレータ)オブジェクトは通常1回しか反復実行(消費)できず、2回目のループでは空になります。
# ジェネレータ式(generator expression)
gen = (x * x for x in range(4))
list(gen) # [0, 1, 4, 9]
list(gen) # [] -> すでに消費されて空になっている
-
yieldを使用した関数は、実行直後にリストを返すわけではなく、Generatorオブジェクトを生成して返します(遅延評価:lazy evaluation)。
# ジェネレータ関数(generator function)
def counter(start, stop):
while start <= stop:
yield start # yield によって関数を一時停止し、値を一つ生み出す
start += 1
list(counter(3, 5)) # [3, 4, 5]
- デコレータを自作する際、元の関数の引数を受け継ぐための
*args, **kwargsを怠ると、汎用性が失われます。
def trace(func):
# 引数を固定してしまっている
def wrapper(x, y):
print("calling", func.__name__)
return func(x, y)
return wrapper
@trace
def add(a, b):
return a + b
@trace
def greet(name):
return f"Hello {name}"
print(add(2, 3))
print(greet("Bob"))
#calling add
#5
#trace.<locals>.wrapper() missing 1 required positional argument: 'y'
- デコレータ関数内で
@functools.wrapsを使い忘れると、元の関数の名前やDocstring(メタデータ)が上書きされ失われます。
from functools import wraps
# シンプルなデコレータ
def trace(func):
def wrapper(*args, **kwargs):
print("calling", func.__name__)
return func(*args, **kwargs)
return wrapper
@trace
def add(x, y):
"""加算する関数"""
return x + y
print(add.__name__) # wrapper
print(add.__doc__) # None
- 正規表現(Regex)の
*や+はデフォルトで「貪欲(Greedy)マッチング」となります。最短一致をおこなう場合は.*?のように書きます。
import re
# 貪欲(greedy)と非貪欲(non-greedy)
html = '<b>one</b><b>two</b>'
re.findall(r'<b>.*</b>', html)
# ['<b>one</b><b>two</b>'] -> 貪欲マッチ
re.findall(r'<b>.*?</b>', html)
# ['<b>one</b>', '<b>two</b>'] -> 非貪欲マッチ(non-greedy)
-
re.match()は文字列の「先頭」からのみ合致を探しますが、文中のどの位置でも探す場合はre.search()を使用します。
# match vs search
re.match(r'world', 'hello world') # None -> 先頭からのマッチではないため
re.search(r'world', 'hello world') # マッチ成功
- バックスラッシュの意図せぬエスケープを防ぐため、正規表現のパターンはRaw文字列(
r'...')で記述することを強く推奨します。
# Raw 文字列(raw string):ファイルパスや正規表現に多用される
path = r'C:\Users\demo\notes.txt'
path # バックスラッシュがエスケープシーケンス(escape sequence)として解釈されない
9. 標準ライブラリと実用パターン (Standard Library & Practical pattern)
-
collections.Counterは、キーが存在しなかった場合に KeyError ではなく0を返します(通常の辞書との挙動の差異)。
from collections import Counter
# Counter:頻度のカウント
words = 'data science data python python python'.split()
word_counts = Counter(words)
word_counts['missing'] # 0 -> 存在しない key でもエラーにならない
-
collections.defaultdictは、存在しないキーに単にアクセス(参照)しただけでも、そのキーをデフォルト値付きで自動生成してしまいます。
from collections import defaultdict
# defaultdict:グループ化
groups = defaultdict(list)
for city, name in [('BJ', 'Ann'), ('BJ', 'Bob'), ('SH', 'Cindy')]:
groups[city].append(name) # key が存在するか事前に確認する必要がない
- リストの先頭から要素を取り出す
list.pop(0)は $O(n)$ の計算コストがかかります。左端からの取り出しが頻発する場合はcollections.deque.popleft()($O(1)$)を使用します。
from collections import deque
# deque:両端キュー。BFS やキュー(queue)操作に適している
queue = deque([1, 2, 3])
queue.popleft() # 左側から取得 -> 1
-
bisectによる二分探索モジュールも、前提として「すでにソートされたリスト」に対してのみ有効です。
from bisect import bisect_left
# bisect:順序付きリストから挿入位置を探す
sorted_nums = [1, 3, 5, 7, 9]
idx = bisect_left(sorted_nums, 4)
idx # 2 -> 4 はインデックス 2 の位置に挿入されるべき
-
heapqはデフォルトで「最小ヒープ(Top要素が最小)」として動作します。最大ヒープとしてはそのまま使えません。
import heapq
# heapq:デフォルトで最小ヒープ(min-heap)
heap = [5, 1, 3, 9]
heapq.heapify(heap)
heapq.heappop(heap) # 1 -> 毎回最小値を取り出す
-
datetimeモジュールのstrptimeフォーマットにおいて、%m(月)と%M(分)など、大文字と小文字の取り違えに注意が必要です。
from datetime import datetime
# datetime:時間のパース、時間差の計算
dt = datetime.strptime('2026-04-03 09:30', '%Y-%m-%d %H:%M')
-
timedelta.daysは日数の整数部のみを返します。秒などを含めた厳密な時間差が欲しい場合はtotal_seconds()を使用します。
from datetime import timedelta
# 総秒数の取得
total_sec = timedelta(days=1, hours=1).total_seconds()
- キャッシュ用途で
@functools.lru_cacheを使う場合、その関数の引数に含まれるすべての値がハッシュ可能(hashable)である必要があります(リストなどは渡せません)。
from functools import lru_cache
# lru_cache:再帰関数のメモ化(memoization)
@lru_cache(maxsize=None)
def fib(n):
if n < 2:
return n
return fib(n - 1) + fib(n - 2)