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

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

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

なぜGPTで成功したTransformerが、リップシンクでは簡単に使えないのか?データ量・計算量・過学習という3つの課題を深掘りし、LSTMとTransformerの実践的な使い分け方を解説。さらに転移学習という第三の選択肢まで、CEATEC 2025で見せた「アバター」の舞台裏を、クオ先生とマナブ君の対話でわかりやすく紐解きます。

By Qualiteg プロダクト開発部
(株)Qualiteg、CEATEC 2025 出展レポート

(株)Qualiteg、CEATEC 2025 出展レポート

こんにちは! 2025年10月14日から17日までの4日間、幕張メッセで開催されたアジア最大級の総合展示会「CEATEC 2025」(主催者発表、総来場者数98,884名)に、株式会社Qualitegとして出展してまいりました! プレスリリース 株式会社Qualiteg、CEATEC 2025に出展 ― AIアバター動画生成サービス「MotionVox®」最新版を実体験株式会社Qualitegのプレスリリース(2025年10月10日 08時50分)株式会社Qualiteg、CEATEC 2025に出展 ― AIアバター動画生成サービス「MotionVox®」最新版を実体験PR TIMES株式会社Qualiteg CEATEC 2025 出展概要 当社は幕張メッセのホール6にあるネクストジェネレーションパークというエリアの 6H207 にブースを構えました。 「Innovation for All」というCEATECのテーマにあわせ、今回は、 AIアバター動画生成サービスMotionVoxを中心に当社の革新的なAIソリューションを展示させていただきました。 展示内容紹介に

By Qualiteg ビジネス開発本部 | マーケティング部, Qualiteg ニュース
日本語対応 LLMランキング2025 ~ベンチマーク分析レポート~

日本語対応 LLMランキング2025 ~ベンチマーク分析レポート~

はじめに 本レポートは、Nejumi Leaderboard 4のベンチマークデータ(2025/10/11版)に基づいて、日本語対応LLMの性能を総合的に分析したものです。 Nejumi Leaderboard 4は、日本語タスクにおけるLLMの性能を多角的に評価する信頼性の高いベンチマークとして知られています。 本分析では、総合スコアとコーディングスコアの2つの観点から、商用APIモデルとオープンモデルの両方を対象に、それぞれの特徴や傾向を詳しく見ていきます。 オープンソースモデルについて Weightがオープンなモデルは場合によっては「オープンソースモデル」、「OSSモデル」と呼ばれますが、モデルによっては「オープンソース」と呼ぶには不十分な場合があるため本稿では、「オープンソースモデル」ではなく「オープンモデル」と表現しています。 ベンチマーク分析について 本レポートは、LLM選択の参考情報として、ベンチマークデータから読み取れる傾向や特徴を提示するものです。最終的なモデル選択においては、これらの情報を踏まえつつ、実際の使用環境での検証を行うことをおすすめいたし

By Qualiteg コンサルティング, Qualiteg プロダクト開発部
Pythonの落とし穴:__len__メソッドを実装したらオブジェクトの真偽値判定が変わってしまった話

Pythonの落とし穴:__len__メソッドを実装したらオブジェクトの真偽値判定が変わってしまった話

こんにちは! Pythonでカスタムクラスを作成していて、 「オブジェクトは存在するのにif文でFalseと判定される」 という不可解な現象に遭遇したことはありませんか? この記事では、__len__メソッドを実装することで生じる、予期しない真偽値判定の挙動について解説いたします! 実際に遭遇したバグ ユーザーの投稿を管理するクラスを実装していたときのことです class PostManager: """ブログ投稿を管理するクラス""" def __init__(self, user_id): self.user_id = user_id self._posts = [] self._cache = {} def __len__(self): """投稿数を返す""" return len(self._posts) def add_post(

By Qualiteg プロダクト開発部