AIがよく間違える「クロージャ問題」の本質と対策

AIがよく間違える「クロージャ問題」の本質と対策

こんにちは!
本日は「クロージャ問題」に関する話題となります。

Pythonでループ内に関数を定義したことはありますか?
もしあるなら、あれれ?な挙動に遭遇したことがあるかもしれません。

本稿では、Pythonプログラマーなら一度は経験する「クロージャ問題」について、初心者にもわかりやすく解説してみたいとおもいます

クロージャとは何か?

そもそも ”クロージャ” とは何でしょうか。

クロージャ(closure)とは、関数が自分の定義されたスコープの変数を覚えて持ち運ぶ仕組み のことです。

もう少し分解すると、次の2つがポイントとなります

  1. 内側の関数が、外側の関数の変数を使える
  2. 外側の関数が終了しても、その変数は生き続ける

普通の関数とクロージャ―を使った関数を比較してみましょう

普通の関数との比較

まずは普通の関数から、

def add(x, y):
    return x + y

print(add(3, 5))  # 8
print(add(3, 7))  # 10

→ 毎回 xy の両方を渡す必要があります。

クロージャを使った関数

こちらがクロージャを使った関数です

def make_adder(x):
    def add(y):
        return x + y  # 外側の x にアクセスできる!
    return add

add_3 = make_adder(3)  # 3 を「記憶」した関数を作る
print(add_3(5))  # 8
print(add_3(7))  # 10

make_adder の実行が終わっても、内側の addx=3 を覚えているため、
「3を足す専用の関数」を作ることができました。

クロージャのイメージはこんな感じ↓

make_adder(3)
 └─ add(y): return x + y
       ↑
       x=3 が閉じ込められている(クロージャ)

→ 「関数が環境ごと持ち運ぶ」というイメージですね。

では、実用的な例としてカウンターの例をみてみましょう

実用的な例:カウンター

def make_counter(name):
    count = 0
    
    def increment():
        nonlocal count
        count += 1
        print(f"{name}: {count}回目")
        return count
    
    return increment

coffee_counter = make_counter("コーヒー")
tea_counter = make_counter("紅茶")

coffee_counter()  # コーヒー: 1回目
coffee_counter()  # コーヒー: 2回目
tea_counter()     # 紅茶: 1回目

このように、それぞれ独立した count を覚えて動くので、複数の状態を簡単に持てるのがクロージャの便利さです。

クロージャ問題とは?

さて、便利な「クロージャ」ですが、「クロージャ問題」とはどういうものでしょう。

実際の問題発生例でみてみたいとおもいます

問題の発生例

以下のコードをみて、頭の中で動作を想像してみましょう。

はい、ループで変数iが0,1,2と変化しますね。
次に 変数iを参照している click_handlerという関数をbuttonsというリストにいれます。ここでは関数の実行結果ではなく、関数そのものを入れている点に注意です

buttons = []
for i in range(3):
    def click_handler():
        print(f"ボタン {i} がクリックされました")
    buttons.append(click_handler)

buttons[0]()  # ボタン 2 がクリックされました ← 本当は0のはず
buttons[1]()  # ボタン 2 がクリックされました ← 本当は1のはず
buttons[2]()  # ボタン 2 がクリックされました

buttons[0]には、一番最初にいれた click_handler関数がはいっていますので、それを実行するには buttons[0]() ですね。(ややトリッキーでしたかね)

さて、その実行結果はというと、「ボタン 2 がクリックされました」と表示されます。

あれ、一番最初に入れたから「ボタン 0 がクリックされました」じゃないの?
って思ってしまうのが「クロージャ問題」です。

このサンプルの結果は、全部「ボタン2」になってしまいます。

なぜこうなるのか?

  • Pythonのクロージャは「変数そのもの(参照)」をキャプチャする
  • をコピーするのではなく、変数 i を参照している
  • ループ終了時に i=2 になっているので、すべての関数から 2 が見えてしまう

ということです。

実際のプロジェクトでの例

import asyncio

async def send_requests():
    tasks = []
    endpoints = ["server0", "server1", "server2"]
    
    for i, endpoint in enumerate(endpoints):
        async def make_request(endpoint=endpoint, i=i):  # ← 修正済み
            print(f"リクエスト送信: {endpoint} (サーバー{i})")
            # 実際のAPIコールをここに書く
        tasks.append(make_request())  # コルーチンを生成して追加
    
    await asyncio.gather(*tasks)

たとえば上のコードは一見うまく動きそうですが、実行すると…

リクエスト送信: server2 (サーバー2)
リクエスト送信: server2 (サーバー2)
リクエスト送信: server2 (サーバー2)

全部 最後のサーバー (server2) に対して実行されてしまいます。

これはもうとんでもないバグですね。

なぜバグになるのか?

  • ループのたびに make_request が定義されるが、すべての関数が同じ iendpoint を参照している
  • 関数が実行されるのはループが終わった後
  • その時点で i=2, endpoint="server2" になっている
  • 結果として全部「server2」になってしまう

つまり、ループ変数をクロージャが遅延評価してしまう問題となります。この遅延評価こそがクロージャ―問題の本質です。

解決方法

では、どう解決して行けばいいでしょうか。

いくつかの方法をご紹介いたします

もう↓の例を参照しつつ、解決方法をみていきましょうょう

buttons = []
for i in range(3):
    def click_handler():
        print(f"ボタン {i} がクリックされました")
    buttons.append(click_handler)

buttons[0]()  # ボタン 2 がクリックされました ← 本当は0のはず
buttons[1]()  # ボタン 2 がクリックされました ← 本当は1のはず
buttons[2]()  # ボタン 2 がクリックされました

クロージャ問題を含むコード

方法1: デフォルト引数を使う

この方法では、関数の引数にループ変数をデフォルト値として渡すことで、関数定義時に値を固定する という解決策をとります。
例えば次のように書きます

buttons = []
for i in range(3):
    def click_handler(button_id=i):
        print(f"ボタン {button_id} がクリックされました")
    buttons.append(click_handler)

こうすることで、i が後から変化しても、button_id には定義時点の値が入ります。
短く書ける点が魅力ですが、本来のシグネチャには存在しない引数が増えてしまい、コードの意図が少し分かりづらくなるデメリットがあります。

方法2: クロージャファクトリを使う(おすすめ)

この方法では、「関数を返す関数(ファクトリ)」を定義し、その中でループ変数を閉じ込めることで問題を解決します。

def make_click_handler(button_id):
    def handler():
        print(f"ボタン {button_id} がクリックされました")
    return handler

buttons = [make_click_handler(i) for i in range(3)]

このパターンでは、make_click_handler がそれぞれの値を保持した関数を作るため、ループが終わった後でも正しい値が使われます。
「この値を固定した関数を作る」という意図が明確で、誤用した場合はすぐエラーになるため、安全性や保守性の観点から最も推奨される方法です。

方法3: lambda + デフォルト引数

この方法では、無名関数 lambda にループ変数をデフォルト引数として渡すことで、関数をその場で生成します。

buttons = [lambda x=i: print(f"ボタン {x} がクリックされました") for i in range(3)]

非常に短く書けるのが利点ですが、コードの読みやすさが低くなり、処理が複雑になると途端に理解しづらくなります。
小さなスクリプトや簡単な使い捨てコードには便利ですが、業務コードやチーム開発では避けた方が無難です。

方法4: functools.partial を使う

この方法では、functools.partial を用いて「関数にあらかじめ引数を適用しておいた新しい関数」を作ることで解決します。

from functools import partial

def click_handler(button_id):
    print(f"ボタン {button_id} がクリックされました")

buttons = [partial(click_handler, i) for i in range(3)]

partial(click_handler, i) によって、button_id=i が固定された関数が生成され、リストに格納されます。
意図が明確で関数型プログラミングの発想に近い方法ですが、初心者には少し理解しづらく、追加の import が必要になる点も注意が必要です。

それぞれの解決方法の比較表

方法解決アプローチメリットデメリットおすすめ度
方法1: デフォルト引数ループ変数をデフォルト引数に渡して、その時点の値を固定する・シンプルで短く書ける
・Pythonらしい書き方
・不要な引数が関数に現れる
・誤って上書きされても気づきにくい
・型チェッカーで混乱する場合がある
★3
方法2: クロージャファクトリ「関数を返す関数」を定義し、値をクロージャで閉じ込める・値が安全にカプセル化される
・意図が明確で可読性が高い
・誤用時は即エラーで気づける
・型チェッカーと相性が良い
・コードがやや長い
・追加の関数定義が必要
★5
方法3: lambda + デフォルト引数lambda式で無名関数を作り、ループ変数をデフォルト引数に渡す・最短で書ける
・ちょっとした処理に便利
・可読性が低い
・複雑な処理には不向き
・デフォルト引数の問題は方法1と同じ
★2
方法4: functools.partialpartial を使い、関数に事前に引数を適用して新しい関数を生成する・意図が明確で関数型プログラミング的
・引数が綺麗に固定される
・初心者には少し分かりにくい
・追加の import が必要
★3

実務で役立つパターン

ここまででクロージャの仕組みと典型的な問題を見てきましたが、ここで、実際のプロジェクトで役立つクロージャのパターンをご紹介いたします

クロージャはたとえば、↓のような場面でクロージャは大活躍してくれます

  • 状態管理をシンプルに行いたいとき
  • 処理を動的に生成して柔軟に対応したいとき
  • クラスを使うほどではないが、ちょっとした「覚えておく仕組み」が欲しいとき


具体例として「進捗レポーター」と「動的バリデーション」という2つのパターンを見てみましょう。どちらも「方法2:クロージャファクトリ」の応用例です!

進捗レポーター

このパターンでは、処理の進捗状況をトラッキングする関数を動的に生成します。

def create_progress_reporter(server_name, total_tasks):
    completed = 0
    def report_progress(task_name):
        nonlocal completed
        completed += 1
        percent = (completed / total_tasks) * 100
        print(f"[{server_name}] {task_name} 完了 ({percent:.1f}%)")
        return completed == total_tasks
    return report_progress
  • create_progress_reporter が返す report_progress 関数は、外側の completed を覚えているため、呼び出すたびに進捗を更新できます。
  • サーバーごとに「独立した進捗カウンター」を作れるので、複数の処理を並列に監視するのに便利です。

実務での活用例
サーバーごとのタスク進捗、ファイル処理の進行度、バッチ処理の進行率などを簡単に管理できます。
→ 普通なら外部でクラスや状態管理が必要ですが、クロージャを使うことで簡潔に記述できます。

動的バリデーション

次に、入力フィールドごとに異なる検証処理を持つハンドラーを生成するパターンです。

def create_validation_handler(field_name, validator_func):
    def handler(event):
        value = event.get('value')
        if not validator_func(value):
            print(f"検証エラー: {field_name} の値が不正です: {value}")
            return False
        print(f"✓ {field_name}: OK")
        return True
    return handler
  • フィールド名(field_name)と検証ロジック(validator_func)をクロージャに閉じ込めることで、フィールドごとに独立したハンドラーを作成できます。
  • 例えば「メールは @ を含むか」「年齢は0〜150の範囲か」「名前は空文字でないか」といった検証を、それぞれ専用の関数に変換可能です。

実務での活用例
フォーム入力の検証、APIの入力チェック、ログデータのバリデーションなど。
→ ループでフィールドリストを回してハンドラーを自動生成できるため、コードの重複を防ぎ、保守性が大きく向上します。

AIペアプログラミングでのTips

さて、最後に、AIでコードを生成するときに、このクロージャ問題に対して適切な実装をさせるためのTipsを共有いたします

最近は ChatGPT、Claude、Gemini など、コード生成AIを使ったプログラミングが当たり前になってますが、このブログのタイトルにもあるようにAIはけっこう クロージャ問題を含んだコード を生成してしまいます。

AIがやらかした例

handlers = []
for i, item in enumerate(items):
    def handler():
        process(i, item)  # 危険! 全部最後の i を使ってしまう
    handlers.append(handler)

このように「ループ内で関数を定義 → ループ変数をそのまま使う」パターンで間違って出力してしまう例はけっこうあるんです。
また、AIは「短く動くコード」を優先して提案することも多く、バグを生まなくても読みづらかったり、modifyしづらいコードが紛れ込みやすいです。
そこで、AIにコードをつくらせるまえに、ちゃんとAIに指導をいれてやる必要があります。

AIへの指示の工夫

といっても指導は簡単です

AIにコードを書かせるときは、プロンプトに
「クロージャ問題を避けるためにクロージャファクトリを使ってください」
と書くだけで精度がかなり上がります。

良い指示例

複数のボタンにイベントハンドラーを設定するコードを書いてください。
ただしクロージャ問題を避けるため、クロージャファクトリパターンを使用してください。

さらに具体的な指示

forループ内で関数を定義する場合は、ループ変数の遅延評価問題を避けるため、
必ずクロージャファクトリを使って実装してください。

はい、この程度の指示でコード品質が向上します。

レビュー時のチェックポイント

さらに、AIが生成したコードをレビューするときは、特に以下を確認すると安心です

  • ループ内で定義された関数の中に iitem をそのまま参照していないか
  • 非同期処理(async def)でクロージャを作っていないか
  • デフォルト引数で誤魔化していないか(def handler(x=i): ...

危険な場合は「クロージャファクトリで書き直して」と修正指示を出しましょう。
ちなみに、このレビューもAIにやらせたほうが早いというときは、上記観点をちゃんとレビューするようにレビュワーAIに指示しましょう。

プロンプトテンプレート

ということで、AIを使うときにコピペできる以下のような「クロージャ問題回避お守りプロンプト」を持っておくと便利ですね。

ループ内で関数を定義する必要がある場合
1. クロージャファクトリパターンを使用する
2. ループ変数は必ず関数の引数として渡す
3. 内部関数で値をキャプチャする
これらを徹底して実装してください。

まとめ

クロージャ問題は「一度はまると地味に時間を奪う」落とし穴ですが、正しい知識を持っていれば怖くないですね
特に クロージャファクトリを習慣化すること で、安全で明確なコードを日常的に書ける(AIに書かせられる)ようになります。

これからループ内で関数を定義するときは、ぜひ今回紹介した方法が参考になれば幸いです。

それでは、また次回お会いしましょう!

Read more

NVIDIA GeForce RTX 50xx with CUDA capability sm_120 is not compatible with the current PyTorch installation. が発生したとき

NVIDIA GeForce RTX 50xx with CUDA capability sm_120 is not compatible with the current PyTorch installation. が発生したとき

こんにちは、PyTorch 2.6.0 環境で以下のような問題が発生したときの対処方法について解説いたします。 NVIDIA GeForce RTX 5090 with CUDA capability sm_120 is not compatible with the current PyTorch installation. The current PyTorch install supports CUDA capabilities sm_50 sm_60 sm_70 sm_75 sm_80 sm_86 sm_90. 他のBlackwell GeForce の場合は以下のようなメッセージとなります。 NVIDIA GeForce RTX

By Qualiteg プロダクト開発部
OpenCV cv2.imwrite で発生する「_img.empty()」エラーと「動画安定化」による解決法

OpenCV cv2.imwrite で発生する「_img.empty()」エラーと「動画安定化」による解決法

こんにちは! 画像処理や動画解析の現場で広く利用されている OpenCV。 しかし実務で動画処理を行っていると、時折以下のようなエラーに遭遇することがあります。 cv2.error: OpenCV(4.11.0) /io/opencv/modules/imgcodecs/src/loadsave.cpp:929: error: (-215:Assertion failed) !_img.empty() in function 'imwrite' このエラーは、cv2.imwrite() に渡された画像が空(None またはサイズ0) の場合に発生します。 一見単純に見える問題ですが、背後には「入力動画の不安定さ」や「並列処理の競合」といった要因が潜んでいることが少なくありません。 本記事では、このエラーの発生原因を掘り下げ、実務で効果のある解決策として 「動画の安定化(正規化)」 を紹介します。 TL;

By Qualiteg プロダクト開発部
発話音声からリアルなリップシンクを生成する技術 第5回(前編):Transformerの実装と実践的な技術選択

発話音声からリアルなリップシンクを生成する技術 第5回(前編):Transformerの実装と実践的な技術選択

こんにちは!リップシンク技術シリーズもいよいよ終盤となりました。 前回(第4回)では、LSTMの学習プロセスと限界について詳しく解説しました。限られたデータでも効果的に学習できるLSTMの強みを理解する一方で、長距離依存の処理に限界があることも明らかになりました。そして、この問題を解決する革新的なアプローチとして、すべての位置の情報を同時に参照できるTransformerのSelf-Attention機構を紹介しました。 第5回の今回は、 Transformerの具体的なネットワーク設計から始め、その実装上の課題を明らかにします。(前編※) そして、LSTMとTransformerの長所を組み合わせたハイブリッドアプローチを紹介し、実際の製品開発における技術選択の指針を示します。最後に、感情表現への拡張という次なる挑戦についても触れていきます。(後編※) ※Transformerの仕組みは複雑であるため、第5回は前編と後編に分けて解説させていただく予定です。 1. Transformerベースのネットワーク設計 1.1 全体アーキテクチャ図 では、さっそく、Tran

By Qualiteg 研究部, Qualiteg コンサルティング
大企業のAIセキュリティを支える基盤技術 - 今こそ理解するActive Directory 第2回 ドメイン環境の構築

大企業のAIセキュリティを支える基盤技術 - 今こそ理解するActive Directory 第2回 ドメイン環境の構築

こんにちは、今回はシリーズ第2回ドメイン環境の構築 - 検証環境の構築手順について解説いたします! 連載の構成 第1章:基本概念の理解 - Active DirectoryとKerberos/NTLM認証の基礎 【★今回です★】第2章:ドメイン環境の構築 - 検証環境の構築手順 第3章:クライアントとサーバーのドメイン参加 - ドメイン参加の詳細手順 第4章:プロキシサーバーと統合Windows認証 第5章:ブラウザ設定と認証 - 各ブラウザでの設定方法 第6章:トラブルシューティング - よくある問題と解決方法 第7章:セキュリティとベストプラクティス - 本番環境での考慮事項 第8章:実践的な構成例 - AIセキュリティツールとの統合事例 第2章:ドメイン環境の構築 2.1 ドメイン名の設計 2.1.1 ドメイン名の命名規則 Active Directoryを構築する際、

By Qualiteg コンサルティング