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

ログを ちょこっと grep するツール "ちょこぐれっぷ" つくりました

ログを ちょこっと grep するツール "ちょこぐれっぷ" つくりました

こんにちは! 今日はちょこっとしたツールをつくりました。 ログをちょこっとgrepするツールです。もちろん無料。 chocoGrep - ちょこっとgrep!ログフィルタツールちょこっとgrepするならchocoGrep!「error or warning」と書くだけの簡単or/and検索。AIエージェントに渡す前にログを最適化。正規表現不要、インストール不要。chocoGrepQualiteg Inc. Cursor、Devin、Claude Code、ChatGPT——AIコーディングエージェントにエラーログを渡してデバッグを手伝ってもらう。もう日常ですよね。 でも、 * ログを全部貼り付けたら、AIの応答がやたら遅い * 「トークン制限を超えました」と怒られる * 大量のログの中から、AIが的外れな部分に注目してしまう そこで、つくったちょこっとgrepするためのツールです 名付けて ちょこぐれっぷ!chogoGrep! chocoGrepって何? ブラウザで動く、ゆるいgrepツールです。 ログを貼り付けて、検索ワードを入れるだけ。インストール不要

By Qualiteg プロダクト開発部
GPUを使った分散処理で見落としがちなCPUボトルネックとtasksetによる解決法

GPUを使った分散処理で見落としがちなCPUボトルネックとtasksetによる解決法

こんにちは! 複数枚のGPUをつかった並列処理システムを設計しているときCPUについてはあまり考えないでシステムを設計してしまうことがあります。 「機械学習システムの主役はGPUなんだから、CPUなんて、あんまり気にしなくてよいのでは」 いいえ、そうでもないんです。 推論中のあるタイミングに急に動作が遅くなったりするときCPUが原因であることがけっこうあります。 概要(5分で分かる要点) 先日GPUを使った並列処理システムで、予期しないCPUボトルネックが発生し、パフォーマンスが大幅に低下する問題に遭遇しました。 複数のプロセスが異なるGPUを使用しているにも関わらず、処理が極端に遅くなる現象の原因は、処理パイプラインの一部に含まれるCPU集約的な計算処理でした。 問題の症状 * 単一プロセス実行時:正常な速度 * 複数プロセス並列実行時:処理時間が数倍に増加 * GPUリソースに競合なし(nvidia-smiで確認済み) 根本原因 処理パイプラインにGPUに適さないCPU集約的な計算(データ前処理、統計変換など)が含まれており、複数プロセスが同じCP

By Qualiteg プロダクト開発部
Model Context Protocol完全実装ガイド 2025- 仕様変遷から最新Streamable HTTPまでの全て

Model Context Protocol完全実装ガイド 2025- 仕様変遷から最新Streamable HTTPまでの全て

こんにちは! 現在、LLM業界で破竹の勢いでひろまっているMCPについて、本日はとくに実装面について解説していきたいとおもいます。 MCP、MCPとひとくちにいっていますが、実は短期間でけっこう「標準」とよばれる仕様が変化しておりますので、仕様のバリエーションを順を追って解説しつつ、実際に実装をしていきたいとおもいます。 さて、MCPですが、2024年後半、Anthropicが発表したModel Context Protocol(MCP)は、AI分野における重要な転換点となりました。 従来、各AIベンダーが独自に実装していたツール呼び出し機能(tool useと呼びます)を標準化し、AIモデルと外部システムの連携を統一的に扱える仕組みを提供しました 本記事で、MCPの誕生から現在に至るまでの技術的変遷を詳細に追いながら、2025年時点での最適な実装方法を完全なソースコードと共に解説します。特に、仕様の変化に振り回されがちな実装者の視点から、なぜ現在の形に収束したのか、そして今後どのような実装アプローチを取るべきかを明確にしていきます。 第1章 MCPが解決しようとした問題

By Qualiteg プロダクト開発部
【出展報告】ASCII STARTUP TechDay 2025

【出展報告】ASCII STARTUP TechDay 2025

こんにちは! 本日、「ASCII STARTUP TechDay 2025」に出展してまいりましたのでレポートさせていただきます! ASCII STARTUP TechDay 2025 ASCII STARTUP TechDay 2025は、2025年11月17日(月)に東京・浅草橋ヒューリックホール&カンファレンスで開催された、ディープテック・スタートアップのエコシステム構築をテーマにした展示交流・カンファレンスイベントです。 秋の展示会は本当にいいですね 本日はとてもよいお天気で、涼しくて、展示会にはピッタリの気候で朝からルンルンでした。しかも午後からの展示会ということで、気持ちに余裕をもって朝の業務をこなしていたところ、けっこうすぐに昼前になり、あわてて現場へ。 浅草橋は当社からもわりと近いという立地の良さを甘く見ておりましたが💦、なんとか予定時刻前に到着しました。やっぱり、都心開催は本当にありがたいですね。 会場へ急いでいると、おなかが「ぐ~」と鳴り 「そういえば、朝食まだだったわ」 とおもったところに、なんと私の大好きなエッセンさん🍞のトラックがあるで

By Qualiteg ビジネス開発本部 | マーケティング部