見出し画像

Python 3: Deep Dive (Part 1 - Functional): クロージャー (セクション7-3/11)

  • クロージャは関数とその関数外で定義された自由変数をまとめたものを指す。

  • クロージャではPythonのセルオブジェクトを使い、変数が複数のスコープ間で共有される。

  • クロージャの作成時と実行時の違い、特に自由変数の評価タイミングが重要となる。

Pythonのスコープ、クロージャー、デコレーターについての学習を続ける中で、今回は重要なトピックであるクロージャーに焦点を当てます。この概念は、Pythonの関数型プログラミングにおいて基本的であり、効率的で読みやすいコードを作成するために欠かせない要素です。本ブログ記事では、『Python 3: Deep Dive (Part 1 - Functional)』のセクション7のレッスン102と103からクロージャー、自由変数、および変数スコープの複雑さについて詳しく説明します。


クロージャーの概要

以前のレッスンでは、非ローカルスコープと内部関数が囲むスコープの変数にアクセスできる仕組みを学びました。クロージャーはこの概念をさらに発展させ、囲むスコープが終了した後でも内部関数がそのスコープの変数を覚えてアクセスできるようにします。

クロージャーとは?

クロージャーとは、関数オブジェクトがその囲むスコープの変数にアクセスする能力を持ちながら、それらの変数を関数とともに保持するものです。クロージャーは以下の2つの要素から構成されます:

  • 関数(内部関数)

  • 拡張スコープ(囲むスコープにある自由変数)


自由変数の理解

自由変数とは、関数内で使用されるが、その関数内で定義されていない変数を指します。それは関数のローカル変数でも引数でもなく、代わりに囲むスコープから取得されます。

例: 内部関数での自由変数

def outer():
    x = 'python'
    
    def inner():
        print(f"{x} rocks!")
    
    return inner

fn = outer()
fn()  # 出力: python rocks!

この例では:

  • `x` は `outer` のローカル変数です。

  • `inner` は `x` を使用する内部関数であり、したがって `x` は `inner` にとって自由変数です。

  • `outer` が `inner` を返すと、関数と自由変数 `x` を含むクロージャーが形成されます。

クロージャーの形成

`fn = outer()` を実行すると:

  • `outer` のための新しいスコープが作成され、`x` が `'python'` に設定されます。

  • 内部関数 `inner` が定義され、`x` を自由変数としてキャプチャします。

  • `outer` が `inner` を返すことで、クロージャーが形成されます。

  • `outer` のスコープは終了しても、クロージャーは `x` へのアクセスを保持します。


Pythonがクロージャーをセルで実装する仕組み

Pythonは、セルオブジェクトを使用して、複数のスコープ間で共有される変数を管理します。

セルオブジェクトの中間的役割

  • 複数のスコープで共有される変数は直接参照されません。

  • 代わりに、セルオブジェクトを介して実際の値を参照します。

  • これにより、外側のスコープが終了しても変数をアクセス可能にします。

視覚的なイメージ

outer.x
   |
   v
  cell (中間オブジェクト)
   |
   v
 'python' (実際の値)
   ^
   |
inner.x

`outer.x` と `inner.x` の両方が同じセルを指し、そのセルが値 `'python'` を保持しています。

クロージャーの調査

`code.co_freevars` および `closure` 属性を使用して、クロージャーの自由変数とセルを調査できます。

print(fn.__code__.co_freevars)  # 出力: ('x',)
print(fn.__closure__)           # 出力: (<cell at 0x...: str object at 0x...>,)

`nonlocal`を使って自由変数を変更する方法

通常、関数内で変数に代入を行うと新しいローカル変数が作成され、囲むスコープの変数を覆い隠してしまいます。自由変数を変更するには、その変数を `nonlocal` として宣言する必要があります。

例: 自由変数の変更

def counter():
    count = 0  # 自由変数
    
    def inc():
        nonlocal count
        count += 1
        return count
    
    return inc

c = counter()
print(c())  # 出力: 1
print(c())  # 出力: 2

この例では:

  • `count` は `counter` と `inc` で共有される自由変数です。

  • `inc` 内で `count` を `nonlocal` と宣言することで、囲むスコープ内の `count` を変更できます。

内部動作

  • `counter` と `inc` の両方が `count` のために同じセルを参照します。

  • `inc` 内で `count` を変更すると、`counter` の `count` も変更されます。


クロージャーの複数インスタンス

関数が呼び出されるたびに、新しいローカルスコープと(該当する場合)新しいクロージャーが作成されます。

例: 独立したクロージャー

def counter():
    count = 0
    
    def inc():
        nonlocal count
        count += 1
        return count
    
    return inc

c1 = counter()
c2 = counter()

print(c1())  # 出力: 1
print(c1())  # 出力: 2
print(c2())  # 出力: 1
  • `c1` と `c2` は独立したクロージャーです。

  • それぞれが別々のセルで `count` 変数を持ちます。

  • `c1` で `count` を変更しても `c2` には影響しません。


共有された拡張スコープ

クロージャーは同じ拡張スコープを共有することができ、つまり自由変数を共有することができます。

例: 共有されたカウンター

def outer():
    count = 0
    
    def inc1():
        nonlocal count
        count += 1
        return count
    
    def inc2():
        nonlocal count
        count += 1
        return count
    
    return inc1, inc2

f1, f2 = outer()
print(f1())  # 出力: 1
print(f2())  # 出力: 2
  • `inc1` と `inc2` は同じ `count` 変数を共有しています。

  • 片方で `count` を変更すると、もう片方にも影響します。

実用的な用途

共有された拡張スコープは、複数の関数が共有状態を維持する必要がある場合に役立ちます。


クロージャーでの一般的な落とし穴

ループでの遅延バインディング

クロージャーがループ内で作成されると、遅延バイン

ディングにより予期せぬ動作が発生することがあります。

問題のある例

def create_adders():
    adders = []
    for n in range(1, 5):
        adders.append(lambda x: x + n)
    return adders

adders = create_adders()
print(adders )  # 出力: 14
print(adders )  # 出力: 14
print(adders )  # 出力: 14
print(adders )  # 出力: 14
  • すべてのクロージャーが同じ `n` を共有し、ループが終了した後の値 `4` を使用します。

  • `n` はクロージャーが呼び出されたときに評価されるため、このような動作になります。

解決策: デフォルト引数を使用

def create_adders():
    adders = []
    for n in range(1, 5):
        adders.append(lambda x, n=n: x + n)
    return adders

adders = create_adders()
print(adders )  # 出力: 11
print(adders )  # 出力: 12
print(adders )  # 出力: 13
print(adders )  # 出力: 14
  • `n=n` をデフォルト引数として設定することで、関数が作成された時点で `n` が評価されます。

  • 各クロージャーは自身の `n` の値を持つようになります。

変数が評価されるタイミングの理解

  • 作成時: デフォルト引数の値は関数が作成された時点で評価されます。

  • 呼び出し時: クロージャー内の自由変数は関数が呼び出されたときに評価されます。


ネストされたクロージャー

クロージャーは他のクロージャー内にネストすることができ、複数レベルのスコープと自由変数を持つことができます。

例: インクリメンター関数

def incrementer(n):
    def inner(start):
        current = start
        
        def inc():
            nonlocal current
            current += n
            return current
        
        return inc
    
    return inner

inc_by_2 = incrementer(2)
inc = inc_by_2(100)
print(inc())  # 出力: 102
print(inc())  # 出力: 104
  • `incrementer` は `n` を持つクロージャーを作成します。

  • `inner` は `current` を持つ別のクロージャーを作成し、`n` にアクセスできます。

  • `inc` は `current` を変更しますが、それは `inner` に対して非ローカルであり、`inc` に対してローカルです。

ネストされたクロージャーの調査

print(inc.__code__.co_freevars)  # 出力: ('current', 'n')
  • `inc` が2つの自由変数 `current` と `n` を持つことが確認できます。


結論

Pythonにおけるクロージャーの理解は、言語のフルパワーを活用するために不可欠であり、特に関数型プログラミングのパラダイムでは重要です。クロージャーは関数が囲むスコープの変数へのアクセスを保持することを可能にし、より動的で柔軟なコード構造を作成できます。

重要なポイント:

  • クロージャーは関数とその自由変数を組み合わせて形成されます。

  • 自由変数は関数内で使用されるが、その関数内で定義されていない変数です。

  • Pythonはセルオブジェクトを使用して、スコープ間で共有される変数を管理します。

  • `nonlocal` キーワードを使用して、クロージャー内で自由変数を変更できます。

  • ループ内でクロージャーを作成する際は遅延バインディングに注意してください。

  • ネストされたクロージャーは、より複雑な関数生成や状態管理を可能にします。

クロージャーをマスターすることで、よりエレガントで強力なPythonコードを書くことができ、言語の能力を最大限に活用することができます。


次回の投稿では、デコレーターとクロージャーの関係について詳しく見ていきますので、お楽しみに!

「超本当にドラゴン」へ

この記事が気に入ったらサポートをしてみませんか?