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

CEATEC 2025に出展します!フォトリアルAIアバター「MotionVox🄬」の最新版を実体験いただけます

CEATEC 2025に出展します!フォトリアルAIアバター「MotionVox🄬」の最新版を実体験いただけます

株式会社Qualitegは、2025年10月14日(火)~17日(金)に幕張メッセで開催される「CEATEC 2025」に出展いたします。今回の出展では、当社が開発したフォトリアリスティックAIアバター技術「MotionVox🄬」をはじめ、最新のAI技術とビジネスイノベーションソリューションをご紹介いたします。 出展概要 * 会期:2025年10月14日(火)~10月17日(金) * 会場:幕張メッセ * 出展エリア:ネクストジェネレーションパーク * ブース番号:ホール6 6H207 * CEATEC内特設サイト:https://www.ceatec.com/nj/exhibitor_detail_ja?id=1915 見どころ:最先端AI技術を体感できる特別展示 1. フォトリアルAIアバター「MotionVox🄬」 テキスト入力だけで、まるで本物の人間のような動画を生成できる革新的なAIアバターシステムです。 MotionVox🄬は自社開発している「Expression Aware🄬」技術により日本人の演者データを基に開発された、

By Qualiteg ニュース
その処理、GPUじゃなくて勝手にCPUで実行されてるかも  ~ONNX RuntimeのcuDNN 警告と対策~

その処理、GPUじゃなくて勝手にCPUで実行されてるかも ~ONNX RuntimeのcuDNN 警告と対策~

こんにちは! 本日は、ONNX RuntimeでGPU推論時の「libcudnn.so.9: cannot open shared object file」エラーの解決方法についての内容となります。 ONNX Runtimeを使用してGPU推論を行う際、CUDAプロバイダの初期化エラーに遭遇することがありますので、このエラーの原因と解決方法を解説いたします。 エラーメッセージの詳細 [E:onnxruntime:Default, provider_bridge_ort.cc:2195 TryGetProviderInfo_CUDA] /onnxruntime_src/onnxruntime/core/session/provider_bridge_ort.cc:1778 onnxruntime::Provider& onnxruntime::ProviderLibrary::Get() [ONNXRuntimeError] : 1 : FAIL : Failed to load

By Qualiteg プロダクト開発部
大企業のAIセキュリティを支える基盤技術 - 今こそ理解するActive Directory 第3回 クライアントとサーバーのドメイン参加

大企業のAIセキュリティを支える基盤技術 - 今こそ理解するActive Directory 第3回 クライアントとサーバーのドメイン参加

こんにちは、今回はシリーズ第3回クライアントとサーバーのドメイン参加について解説いたします! はじめに こんにちは!シリーズ第3回「クライアントとサーバーのドメイン参加」へようこそ。 前回(第2回)では、Active Directoryドメイン環境の構築手順について、ドメインコントローラーのセットアップからDNS設定まで詳しく解説しました。ドメイン環境の「土台」が整ったところで、今回はいよいよ実際にコンピューターをドメインに参加させる手順に進みます。 「ドメインユーザーアカウントを作ったのに、なぜかログインできない」「新しいPCを追加したけど、ドメイン認証が使えない」といった経験はありませんか?実は、Active Directoryの世界では、ユーザーアカウントを作成しただけでは不十分で、そのユーザーが使用するコンピューター自体もドメインに「参加」させる必要があるのです。 本記事では、このドメイン参加について、単なる手順の説明にとどまらず、「なぜドメイン参加が必要なのか」「裏側で何が起きているのか」という本質的な仕組みまで、初心者の方にも分かりやすく解説していきます。Win

By Qualiteg コンサルティング
使い捨てソフトウェア時代の幕開け ― 市場構造の根本的変革と日本企業

使い捨てソフトウェア時代の幕開け ― 市場構造の根本的変革と日本企業

こんにちは、株式会社Qualiteg コンサルティング部門です。 昨今、生成AIの急速な進化により、ソフトウェア開発の在り方が根本から変わりつつあります。2024年にはClaude、GPT-4、Geminiなどの大規模言語モデルがコード生成能力を飛躍的に向上させ、GitHub CopilotやCursor、Windsurf等の開発支援ツールが実際の開発現場で広く活用されるようになりました。さらに、Devin、OpenAI Canvas、Anthropic Claude Codingといった、より高度な自律的コーディング機能を持つAIエージェントも登場しています。 このような技術革新を背景に、当部門では今後のソフトウェア産業の構造変化について詳細な分析を行いました。本シリーズでは、特に注目すべき変化として、従来1000人月規模を要していた企業向けSaaSプラットフォームや、基幹システムが、AIエージェントを効果的に活用することで、わずか2-3名のチームが数日から数週間で実装可能になるという、開発生産性の劇的な向上について考察してまいります。 これは単なる効率化ではなく、ソフトウェア

By Qualiteg コンサルティング