クロージャについて説明できますか(Pythonに関連しているため)?


83

私は閉鎖についてたくさん読んでいて、理解していると思いますが、自分自身や他の人の写真を曇らせることなく、誰かが閉鎖をできるだけ簡潔かつ明確に説明できることを望んでいます。どこで、なぜ使いたいのかを理解するのに役立つ簡単な説明を探しています。

回答:


95

閉鎖の閉鎖

オブジェクトはメソッドがアタッチされたデータであり、クロージャはデータがアタッチされた関数です。

def make_counter():
    i = 0
    def counter(): # counter() is a closure
        nonlocal i
        i += 1
        return i
    return counter

c1 = make_counter()
c2 = make_counter()

print (c1(), c1(), c2(), c2())
# -> 1 2 1 2

6
nonlocalpython 3で追加されたことに注意してください。python2.xには、完全な読み取り/書き込みクロージャがありませんでした(つまり、変数を閉じて読み取ることはできますが、値を変更することはできません)
James Porter

6
@JamesPorter:注:nonlocalPython 2では、可変オブジェクトを使用してキーワードをエミュレートできます。たとえば、L = [0] \n def counter(): L[0] += 1; return L[0]この場合、名前を変更(別のオブジェクトにバインド)することはできませんが、名前 が参照する可変オブジェクト自体を変更することはできます。に。Pythonでは整数は不変であるため、リストが必要です。
jfs 2013

1
@JFSebastian:そうです。それはいつも汚い、汚いハックのように感じます:)
James Porter

46

簡単です。制御フローがそのスコープを離れた後、含まれているスコープから変数を参照する関数。その最後のビットは非常に便利です:

>>> def makeConstantAdder(x):
...     constant = x
...     def adder(y):
...         return y + constant
...     return adder
... 
>>> f = makeConstantAdder(12)
>>> f(3)
15
>>> g = makeConstantAdder(4)
>>> g(3)
7

12と4は、それぞれfとgの内部で「消えた」ことに注意してください。この機能により、fとgが適切に閉じられます。


する必要はありませんconstant = x; あなただけ行うことができreturn y + x、ネストされた関数の中で(または名前付き引数を受け取るconstant)、それがうまく動作します。引数は、引数のないローカルと同じようにクロージャによってキャプチャされます。
ShadowRanger

15

私はこのラフで簡潔な定義が好きです:

アクティブでなくなった環境を参照できる機能。

追加します

クロージャを使用すると、変数をパラメータとして渡すことなく、変数を関数にバインドできます

パラメータを受け入れるデコレータは、クロージャの一般的な使用法です。クロージャは、その種の「関数ファクトリ」の一般的な実装メカニズムです。ストラテジーが実行時にデータによって変更される場合、ストラテジーパターンでクロージャを使用することを頻繁に選択します。

匿名のブロック定義を許可する言語(Ruby、C#など)では、クロージャーを使用して、新しい新しい制御構造を実装できます。匿名ブロックの欠如は、Pythonのクロージャの制限の1つです


15

正直なところ、私はクロージャーを完全によく理解していますが、「クロージャー」とは何か、そしてそれについて「クロージャー」とは何であるかを正確に理解したことはありません。用語の選択の背後にある論理を探すのをあきらめることをお勧めします。

とにかく、ここに私の説明があります:

def foo():
   x = 3
   def bar():
      print x
   x = 5
   return bar

bar = foo()
bar()   # print 5

ここで重要なのは、fooから返された関数オブジェクトは、「x」がスコープ外になり、機能しなくなったとしても、ローカル変数「x」へのフックを保持するということです。このフックは、varがその時点で持っていた値だけでなく、var自体に対するものであるため、barが呼び出されると、3ではなく5が出力されます。

また、Python 2.xのクロージャーが制限されていることも明確にしてください。「x = bla」と書くと、fooの「x」に割り当てられずにバーでローカルの「x」が宣言されるため、「bar」内の「x」を変更する方法はありません。 。これは、Pythonのassignment = declarationの副作用です。これを回避するために、Python3.0では非ローカルキーワードが導入されています。

def foo():
   x = 3
   def bar():
      print x
   def ack():
      nonlocal x
      x = 7
   x = 5
   return (bar, ack)

bar, ack = foo()
ack()   # modify x of the call to foo
bar()   # print 7

7

クロージャとは何かを説明するのと同じコンテキストでトランザクションが使用されていることは聞いたことがなく、ここにはトランザクションのセマンティクスは実際にはありません。

これは、外部変数(定数)を「閉じる」ため、クロージャと呼ばれます。つまり、関数だけでなく、関数が作成された環境のエンクロージャです。

次の例では、xを変更した後にクロージャーgを呼び出すと、gがxを閉じるため、g内のxの値も変更されます。

x = 0

def f():
    def g(): 
        return x * 2
    return g


closure = f()
print(closure()) # 0
x = 2
print(closure()) # 4

また、g()現状では、計算しますが、x * 2何も返しません。それはあるはずですreturn x * 2。それにもかかわらず、「閉鎖」という言葉の説明については+1。
Bruno Le Floch 2012

3

これがクロージャーの典型的なユースケースです-GUI要素のコールバック(これはボタンクラスをサブクラス化する代わりになります)。たとえば、ボタンの押下に応答して呼び出される関数を作成し、クリックの処理に必要な親スコープ内の関連する変数を「閉じる」ことができます。このようにして、同じ初期化関数から非常に複雑なインターフェースを接続し、すべての依存関係をクロージャーに組み込むことができます。


2

Pythonでは、クロージャは変数が不変にバインドされている関数のインスタンスです。

実際、データモデルは、関数の__closure__属性の説明でこれを説明しています。

関数の自由変数のバインディングを含むセルがないか、セルのタプル。読み取り専用

これを実証するには:

def enclosure(foo):
    def closure(bar):
        print(foo, bar)
    return closure

closure_instance = enclosure('foo')

明らかに、変数名から指定された関数があることがわかりますclosure_instance。表面上、オブジェクトで呼び出すとbar、文字列、'foo'および文字列表現が何であれ、それを出力する必要がbarあります。

実際、文字列 'foo'関数のインスタンスにバインドされており、cell_contents属性のタプルの最初の(そして唯一の)セルの属性にアクセスすることで、ここで直接読み取ることができ__closure__ます。

>>> closure_instance.__closure__[0].cell_contents
'foo'

余談ですが、セルオブジェクトはCAPIドキュメントで説明されています。

「セル」オブジェクトは、複数のスコープによって参照される変数を実装するために使用されます

そして、クロージャーの使用法を示すことができます。これ'foo'は関数でスタックし、変更されないことに注意してください。

>>> closure_instance('bar')
foo bar
>>> closure_instance('baz')
foo baz
>>> closure_instance('quux')
foo quux

そして、何もそれを変えることはできません:

>>> closure_instance.__closure__ = None
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: readonly attribute

部分関数

与えられた例では、クロージャを部分関数として使用していますが、これが唯一の目標である場合、同じ目標を次のように達成できます。 functools.partial

>>> from __future__ import print_function # use this if you're in Python 2.
>>> partial_function = functools.partial(print, 'foo')
>>> partial_function('bar')
foo bar
>>> partial_function('baz')
foo baz
>>> partial_function('quux')
foo quux

部分関数の例に適合しない、より複雑なクロージャもあります。時間の許す限り、それらについてさらに説明します。


2
# A Closure is a function object that remembers values in enclosing scopes even if they are not present in memory.

# Defining a closure

# This is an outer function.
def outer_function(message):
    # This is an inner nested function.
    def inner_function():
        print(message)
    return inner_function

# Now lets call the outer function and return value bound to name 'temp'
temp = outer_function("Hello")
# On calling temp, 'message' will be still be remembered although we had finished executing outer_function()
temp()
# Technique by which some data('message') that remembers values in enclosing scopes 
# even if they are not present in memory is called closures

# Output: Hello

クロージャが満たすべき基準は次のとおりです。

  1. 入れ子関数が必要です。
  2. 入れ子関数は、囲んでいる関数で定義された値を参照する必要があります。
  3. 囲んでいる関数は、ネストされた関数を返す必要があります。

# Example 2
def make_multiplier_of(n): # Outer function
    def multiplier(x): # Inner nested function
        return x * n
    return multiplier
# Multiplier of 3
times3 = make_multiplier_of(3)
# Multiplier of 5
times5 = make_multiplier_of(5)
print(times5(3)) # 15
print(times3(2)) #  6

1

これがPython3クロージャの例です

def closure(x):
    def counter():
        nonlocal x
        x += 1
        return x
    return counter;

counter1 = closure(100);
counter2 = closure(200);

print("i from closure 1 " + str(counter1()))
print("i from closure 1 " + str(counter1()))
print("i from closure 2 " + str(counter2()))
print("i from closure 1 " + str(counter1()))
print("i from closure 1 " + str(counter1()))
print("i from closure 1 " + str(counter1()))
print("i from closure 2 " + str(counter2()))

# result

i from closure 1 101
i from closure 1 102
i from closure 2 201
i from closure 1 103
i from closure 1 104
i from closure 1 105
i from closure 2 202

0

私にとって「クロージャ」とは、作成された環境を記憶できる機能です。この機能を使用すると、クロージャー内で変数またはメソッドを使用できます。他の方法では、変数またはメソッドが存在しないか、スコープが原因で到達できないために使用できません。Rubyでこのコードを見てみましょう:

def makefunction (x)
  def multiply (a,b)
    puts a*b
  end
  return lambda {|n| multiply(n,x)} # => returning a closure
end

func = makefunction(2) # => we capture the closure
func.call(6)    # => Result equal "12"  

「multiply」メソッドと「x」変数の両方が存在しなくなった場合でも機能します。覚えておくべき閉鎖能力のためにすべて。


0

私たちは皆、Pythonでデコレータを使用しています。これらは、Pythonのクロージャ関数とは何かを示す良い例です。

class Test():
    def decorator(func):
        def wrapper(*args):
            b = args[1] + 5
            return func(b)
        return wrapper

@decorator
def foo(val):
    print val + 2

obj = Test()
obj.foo(5)

ここで最終値は12です

ここで、ラッパーは「字句クロージャ」であるため、ラッパー関数はfuncオブジェクトにアクセスでき、その親属性にアクセスできます。そのため、funcオブジェクトにアクセスできます。


0

私の例と閉鎖についての説明を共有したいと思います。Pythonの例と、スタックの状態を示す2つの図を作成しました。

def maker(a, b, n):
    margin_top = 2
    padding = 4
    def message(msg):
        print('\n’ * margin_top, a * n, 
            ' ‘ * padding, msg, ' ‘ * padding, b * n)
    return message

f = maker('*', '#', 5)
g = maker('', '♥’, 3)
…
f('hello')
g(‘good bye!')

このコードの出力は次のようになります。

*****      hello      #####

      good bye!    ♥♥♥

これは、関数オブジェクトにアタッチされたスタックとクロージャーを示す2つの図です。

関数がメーカーから返されるとき

関数が後で呼び出されたとき

関数がパラメーターまたは非ローカル変数を介して呼び出される場合、コードには、margin_top、padding、およびa、b、nなどのローカル変数バインディングが必要です。関数コードが機能することを保証するために、ずっと前になくなったmaker関数のスタックフレームにアクセスできる必要があります。これは、メッセージの関数オブジェクトと一緒に見つけることができるクロージャにバックアップされています。


-2

クロージャについて私が今まで見た中で最も良い説明は、メカニズムを説明することでした。それは次のようになりました:

プログラムスタックを、各ノードに子が1つだけあり、単一のリーフノードが現在実行中のプロシージャのコンテキストである縮退ツリーとして想像してください。

ここで、各ノードが持つことができる子は1つだけであるという制約を緩和します。

これを行うと、ローカルコンテキストを破棄せずにプロシージャから戻ることができる構造(「yield」)を作成できます(つまり、戻ったときにスタックからポップされません)。次回プロシージャが呼び出されると、呼び出しは古いスタック(ツリー)フレームを取得し、中断したところから実行を継続します。


それは閉鎖の説明ではありません。
ジュール

あなたは、閉鎖ではなく、継続について説明しています。
Matthew Olenik
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.