Programmingバックエンド開発者

Pythonにおけるクロージャの働き、通常の関数との違い、および実用的な応用について説明してください。

Hintsage AIアシスタントで面接を突破

回答。

問題の背景

「クロージャ」という用語は関数型プログラミングから借用され、Pythonの初期から存在しています。クロージャを使用すると、関数は作成された環境を記憶することができ、その環境の外で呼び出されてもその環境を保持します。この概念は柔軟性を提供し、多くのパターンを実現可能にします。例えば、関数ファクトリや遅延計算などです。

問題

Pythonでは、関数は第一級オブジェクトです。時々、内側の関数が外側の関数のスコープ内の変数を使用する必要がありますが、外側の関数が終了した後でもその変数にアクセスできることが必要です。通常のレキシカルスコープでは、外側の関数が戻る際にこれが保証されません。こうした場合、変数にアクセスすることでクロージャが形成されます。

解決策

クロージャが発生するのは、内部関数が外部に定義された変数を参照し、その外部関数がその内部関数を外部に返した時です。これは、関数ファクトリを作成し、クラスなしで状態をカプセル化し、「その場」でパラメータを使用した関数を構築するためによく使われます。

コードの例:

def make_multiplier(factor): def multiplier(x): return x * factor return multiplier mul2 = make_multiplier(2) mul3 = make_multiplier(3) print(mul2(10)) # 20 print(mul3(10)) # 30

主な特徴:

  • クロージャは、外側の関数が終了した後でも環境変数の値を保持します。
  • 内部関数の状態は本質的にプライベートで、外部から直接変更することはできません。
  • 外部関数の非イミュータブルな変数をクロージャ内で変更するには、nonlocalキーワードを使用します。

意地悪な質問。

クロージャは、変数が内部関数内で変更されるとき、呼び出し間で可変状態を保持できますか?

はい、内部関数内でnonlocalキーワードを使用すれば可能です。nonlocalなしで代入すると、新しいローカル変数が作成され、外部変数は変更されません。

def counter(): count = 0 def inc(): nonlocal count count += 1 return count return inc c = counter() print(c()) # 1 print(c()) # 2

クロージャを使用してPythonでプライベート変数を実装できますか?クラスの代わりに使用することは可能ですか?

はい、クロージャは外部からアクセスできない「プライベート」変数の簡単な実装を提供します。ただし、内部関数にゲッター/セッターが提供されない場合に限ります。

クロージャは関数にのみ適用されますか? Pythonでラムダを使用してクロージャを形成できますか?

はい、クロージャはラムダ式でも形成されることがあります。なぜなら、ラムダはレキシカル変数のバインディングにおいてdefと同様だからです。

def make_power(n): return lambda x: x ** n square = make_power(2) cube = make_power(3) print(square(4)) # 16 print(cube(2)) # 8

一般的なエラーとアンチパターン

  • nonlocalなしで内部変数を変更すると、クロージャが自動的に外部変数を変更すると期待する。
  • クロージャ内で可変オブジェクトをキャプチャして変更し、デバッグの際に問題に気づかない。
  • 正しい変数のバインディングなしにクロージャ内で関数を作成するためにループを使用する(「すべての関数が変数の最後の値を参照する」罠)。

実生活の例

ネガティブケース

ループ内でハンドラーを生成するファクトリ関数が、クロージャ内でループ変数を使用している場合:

handlers = [] for i in range(3): def handler(x): return x + i handlers.append(handler) print([h(10) for h in handlers]) # [12, 12, 12]

利点:

  • 簡単で、コードが少ない。

欠点:

  • すべてのハンドラーが同じ変数iを参照し、最後の値は2であり、大多数にとって予期しない動作をします。

ポジティブケース

デフォルト引数を使用して値を「固定」します:

handlers = [] for i in range(3): def handler(x, j=i): return x + j handlers.append(handler) print([h(10) for h in handlers]) # [10, 11, 12]

利点:

  • 必要な値のバインディング。
  • 予測可能な動作。

欠点:

  • この微妙さを忘れずに、手動でクロージャを修正する必要があり、コードの保守性が複雑になります。