問題の背景:
Pythonにおけるパラメータの渡し方は「オブジェクト参照による呼び出し」(時には共有による呼び出しとも呼ばれます)を実装しています。これは、関数内の変数が外部から渡された引数と同じメモリ内のオブジェクトを指し始めることを意味します。
問題:
関数が渡された可変オブジェクト(例えば、リストや辞書)を変更すると、その変更は関数の外部でも見えるようになります。これは、関数が入力データを変更しないことが期待される場合に、難解なバグを引き起こす可能性があります。
解決策:
副作用を避けるためには、関数内でオブジェクトのコピーを作成するか、不変のデータ構造を使用するべきです。コピーを作成するために、標準的な方法を使用します(例えば、リスト用にはlist.copy()、辞書用にはdict.copy()、またはcopy.deepcopy()を使用します)。
コード例:
def append_one(xs): xs.append(1) return xs lst = [0] append_one(lst) print(lst) # [0, 1] # 変更を避けるには? コピーを作成する: def safe_append_one(xs): ys = xs.copy() ys.append(1) return ys lst2 = [0] safe_append_one(lst2) print(lst2) # [0]
主な特徴:
.copy()によって生成されたリストのコピーは元のリストから完全に独立していると言えますか?
いいえ — .copy()は浅いコピーを作成します。内部に可変オブジェクトがある場合、その変更は元のオブジェクトにも見えます。
import copy lst = [[1, 2], [3, 4]] shallow = lst.copy() shallow[0][0] = 42 print(lst) # [[42, 2], [3, 4]] deep = copy.deepcopy(lst) deep[0][0] = 100 print(lst) # [[42, 2], [3, 4]]
新しいオブジェクトを元にして返すことは、元のオブジェクトに変更がないことを保証しますか?
必ずしもそうではありません。新しいオブジェクト内で元のオブジェクトの一部(例えば、内部のリストへの参照)を使用している場合、元のオブジェクトが変更される可能性があります。
def duplicate_list(xs): return xs * 2 lst = [[1], [2]] res = duplicate_list(lst) res[0][0] = 999 print(lst) # [[999], [2]]
可変オブジェクトに対するデフォルト引数が、関数の複数回呼び出し時に問題を引き起こすことがありますか?
はい — デフォルト値は関数定義時に一度だけ計算されます。
def add_item(item, container=[]): container.append(item) return container print(add_item(1)) # [1] print(add_item(2)) # [1, 2]
ネガティブケース
設定処理ライブラリでは、デフォルト値としてリストを使用したため、関数の異なる呼び出しの間で要素が蓄積されることになりました。その動作は予測できず、非常に長い時間がかかって判明しました。
利点:
再呼び出しのためのコードが少なく、メモリの目に見える節約が得られます。
欠点:
暗黙的な動作、デバッグの難しさ、長期間にわたるバグ。
ポジティブケース
デフォルト値としてNoneを使用し、毎回の呼び出しで新しいオブジェクトを明示的に作成します。
利点:
予測可能性、予期しない副作用の回避、信頼性。
欠点:
意識的である必要があり、少し多くのコードを必要とします。