Pythonの入れ子関数がクロージャと呼ばれないのはなぜですか?


249

私はPythonでネストされた関数を見て使用しましたが、それらはクロージャーの定義と一致しています。では、なぜnested functions代わりに呼び出されるのclosuresですか?

ネストされた関数は外部の世界で使用されていないため、クロージャーではありませんか?

更新:クロージャーについて読んでいて、Pythonに関するこの概念について考えさせられました。以下のコメントで誰かが言及している記事を検索して見つけましたが、その記事の説明が完全に理解できなかったため、この質問をしました。


8
興味深いことに、2006年12月付けのグーグルでこれを見つけました:effbot.org/zone/closure.htm。よくわかりません。「外部複製」はSOで不快に感じられますか?
hbw

回答:


394

クロージャーは、関数が、その実行を終了した囲んでいるスコープからローカル変数にアクセスできるときに発生します。

def make_printer(msg):
    def printer():
        print msg
    return printer

printer = make_printer('Foo!')
printer()

ときmake_printerに呼び出され、新しいフレームがためにコンパイルされたコードをスタックに置かれprinter、一定の値として機能するmsgローカルとして。次に、関数を作成して返します。関数printermsg変数を参照するため、関数make_printerが戻った後も変数は保持されます。

したがって、ネストされた関数がそうでない場合

  1. 囲んでいるスコープにローカルな変数にアクセスし、
  2. それらがそのスコープの外で実行されるときにそうします、

その後、それらは閉鎖ではありません。

以下は、クロージャーではないネストされた関数の例です。

def make_printer(msg):
    def printer(msg=msg):
        print msg
    return printer

printer = make_printer("Foo!")
printer()  #Output: Foo!

ここでは、パラメーターのデフォルト値に値をバインドしています。これは、関数printerが作成されたときに発生するので 、リターン後、msgexternal to の値への参照をprinter維持する必要はありませんmake_printer。このコンテキストでmsgは、関数の通常のローカル変数ですprinter


2
あなたの答えは私のものよりはるかに優れています、あなたは良い点を作りますが、もし私たちが最も厳密な関数型プログラミング定義に行くつもりなら、あなたの例は関数でさえあるのでしょうか?久しぶりで、厳密な関数型プログラミングで値を返さない関数が許可されているかどうか思い出せません。戻り値がNoneであると考える場合、要点は意味がありませんが、それはまったく別のトピックです。
mikerobi

6
@ mikerobi、Pythonは確かに関数型言語ではないので、関数型プログラミングを考慮に入れる必要があるかどうかはわかりません。しかし、いいえ、内部関数はその意味では副作用ではないので、その意味では関数ではありません。ポイントを示す関数も簡単に作成できます
aaronasterling

31
@mikerobi:コードのblobがクロージャであるかどうかは、それがその環境全体で閉じるかどうかに依存します。ルーチン、関数、プロシージャ、メソッド、ブロック、サブルーチンなど、何でもかまいません。Rubyでは、メソッドをクロージャにすることはできません。ブロックのみができます。Javaでは、メソッドをクロージャにすることはできませんが、クラスはクロージャにすることができます。だからといって閉鎖にはなりません。(ただし、一部の変数のみをクローズし、それらを変更することはできないため、それらはほとんど役に立たなくなります。)メソッドは単なるクローズされたプロシージャーであると主張できますself。(JavaScriptの/ Pythonではほとんど本当だということ。)
イェルクWミッターク

3
@JörgWMittag「終了」を定義してください。
Evgeni Sergeev

4
@EvgeniSergeevは「閉じる」、つまり「i囲んでいるスコープのローカル変数[たとえば]を参照する」です。つまりi、スコープが「実行を終了した」場合、つまりプログラムの実行がコードの他の部分に進んだ場合でも、その値を検査(または変更)できます。i定義されているブロックはもうありませんが、それでも参照する関数はまだiそうすることができます。これは一般的に「変数を閉じる」と説明されていますi。特定の変数を処理しないために、その変数が定義されている環境フレーム全体を閉じるように実装できます。
ネスは

103

質問はすでに aaronasterling によって回答されています

ただし、誰かが内部で変数がどのように格納されるかに興味があるかもしれません。

スニペットに来る前に:

クロージャーは、囲んでいる環境から変数を継承する関数です。I / Oを実行する別の関数に引数として関数コールバックを渡すと、このコールバック関数は後で呼び出され、この関数は(ほとんど魔法のように)宣言されたコンテキストと使用可能なすべての変数を記憶しますその文脈で。

  • 関数が自由変数を使用しない場合、クロージャーは形成されません。

  • 自由変数を使用する別の内部レベルがある場合- 以前のすべてのレベルで字句環境が保存されます(例:最後)

  • python <3.Xまたはpython> 3.Xの関数属性func_closure、自由変数を保存します。__closure__

  • Pythonのすべての関数にはこのクロージャー属性がありますが、無料の変数がない場合はコンテンツを保存しません。

例:クロージャの属性ですが、内部にコンテンツはありません。

>>> def foo():
...     def fii():
...         pass
...     return fii
...
>>> f = foo()
>>> f.func_closure
>>> 'func_closure' in dir(f)
True
>>>

注意:クロージャーを作成するには、無料変数が必要です。

上記と同じスニペットを使用して説明します。

>>> def make_printer(msg):
...     def printer():
...         print msg
...     return printer
...
>>> printer = make_printer('Foo!')
>>> printer()  #Output: Foo!

そして、すべてのPython関数にはクロージャー属性があるので、クロージャー関数に関連付けられている囲んでいる変数を調べてみましょう。

これfunc_closureは関数の属性ですprinter

>>> 'func_closure' in dir(printer)
True
>>> printer.func_closure
(<cell at 0x108154c90: str object at 0x108151de0>,)
>>>

このclosure属性は、外側のスコープで定義された変数の詳細を含むセルオブジェクトのタプルを返します。

func_closureの最初の要素。Noneまたは関数の自由変数のバインディングを含むセルのタプルであり、読み取り専用です。

>>> dir(printer.func_closure[0])
['__class__', '__cmp__', '__delattr__', '__doc__', '__format__', '__getattribute__',
 '__hash__', '__init__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', 
 '__setattr__',  '__sizeof__', '__str__', '__subclasshook__', 'cell_contents']
>>>

ここでは、上記の出力で確認できますcell_contents。何が格納されているかを見てみましょう。

>>> printer.func_closure[0].cell_contents
'Foo!'    
>>> type(printer.func_closure[0].cell_contents)
<type 'str'>
>>>

そのため、関数を呼び出すと、printer()内に保存されている値にアクセスしますcell_contents。これは、「Foo!」という出力を取得する方法です。

ここでも、上記のスニペットといくつかの変更を使用して説明します。

 >>> def make_printer(msg):
 ...     def printer():
 ...         pass
 ...     return printer
 ...
 >>> printer = make_printer('Foo!')
 >>> printer.func_closure
 >>>

上のスニペットでは、printer関数内でmsgを印刷しないので、フリー変数は作成されません。自由変数がないため、クロージャー内にコンテンツはありません。これがまさに上記のとおりです。

今、私はすべてをクリアするために、別の異なるスニペットを説明しますFree VariableClosure

>>> def outer(x):
...     def intermediate(y):
...         free = 'free'
...         def inner(z):
...             return '%s %s %s %s' %  (x, y, free, z)
...         return inner
...     return intermediate
...
>>> outer('I')('am')('variable')
'I am free variable'
>>>
>>> inter = outer('I')
>>> inter.func_closure
(<cell at 0x10c989130: str object at 0x10c831b98>,)
>>> inter.func_closure[0].cell_contents
'I'
>>> inn = inter('am')

したがって、func_closureプロパティはクロージャーセルのタプルであることがわかり、それらとその内容を明示的に参照できます。セルにはプロパティ "cell_contents"があります。

>>> inn.func_closure
(<cell at 0x10c9807c0: str object at 0x10c9b0990>, 
 <cell at 0x10c980f68: str object at   0x10c9eaf30>, 
 <cell at 0x10c989130: str object at 0x10c831b98>)
>>> for i in inn.func_closure:
...     print i.cell_contents
...
free
am 
I
>>>

ここでを呼び出すとinn、すべての保存自由変数が参照されるので、I am free variable

>>> inn('variable')
'I am free variable'
>>>

9
Python 3では、他のさまざまな属性と同様に、がfunc_closure呼び出されます。__closure__func_*
lvc 2014年

3
また、__closure_Pythonの3との互換性のためのPython 2.6+で利用可能です
ピエール・

クロージャとは、関数オブジェクトにアタッチされた、閉じられた変数を格納するレコードを指します。関数そのものではありません。Pythonでは、それ__closure__はクロージャであるオブジェクトです。
Martijn Pieters

明確にしてくれてありがとう@MartijnPieters。
James Sapam

71

Pythonは、クロージャのサポートが弱いです。JavaScriptでクロージャーを使用するカウンターの次の例を見てください。

function initCounter(){
    var x = 0;
    function counter  () {
        x += 1;
        console.log(x);
    };
    return counter;
}

count = initCounter();

count(); //Prints 1
count(); //Prints 2
count(); //Prints 3

クロージャは、このように記述された関数に「内部メモリ」を持たせることができるため、非常にエレガントです。Python 2.7では、これは不可能です。あなたがしようとすると

def initCounter():
    x = 0;
    def counter ():
        x += 1 ##Error, x not defined
        print x
    return counter

count = initCounter();

count(); ##Error
count();
count();

xが定義されていないというエラーが表示されます。しかし、あなたがそれを印刷できることが他の人に示されているとしたらどうでしょうか?これは、Pythonが関数変数スコープを管理する方法が原因です。内部関数は外部関数の変数を読み取ることができますが、それらを書き込むことはできません。

これは本当に残念です。ただし、読み取り専用のクロージャーだけで、少なくともPythonが構文シュガーを提供する関数デコレーターパターンを実装できます。

更新

指摘されているように、Pythonのスコープ制限に対処する方法がいくつかあります。

1.globalキーワードを使用します(一般的には推奨されません)。

2. Python 3.xでは、nonlocalキーワードを使用します(@unutbuおよび@leewzで推奨)

3.単純な変更可能なクラスを定義するObject

class Object(object):
    pass

そして作成 Object scope中にinitCounter変数を格納します

def initCounter ():
    scope = Object()
    scope.x = 0
    def counter():
        scope.x += 1
        print scope.x

    return counter

scopeは実際には単なる参照なので、そのフィールドで実行されるアクションは実際には変更されないscopeため、エラーは発生しません。

4.別の方法として、@ unutbuが指摘したように、各変数を配列(x = [0])として定義し、その最初の要素(x[0] += 1)を変更します。この場合も、xそれ自体が変更されていないため、エラーは発生しません。

5. @raxacoricofallapatoriusによって提案されているようにx、次のプロパティを作成できます。counter

def initCounter ():

    def counter():
        counter.x += 1
        print counter.x

    counter.x = 0
    return counter

27
これを回避する方法があります。Python2ではx = [0]、外側のスコープで作成し、内側のスコープで使用できx[0] += 1ます。Python3では、コードをそのままにして、nonlocalキーワードを使用できます。
unutbu 2014年

「内部関数は外部関数の変数を読み取ることができますが、それらを書き込むことはできません。」-これはunutbuのコメントによると不正確です。問題は、Pythonがx = ...のようなものに遭遇すると、xがローカル変数として解釈されることです。もちろん、その時点ではまだ定義されていません。OTOH、xが変更可能なメソッドを持つ変更可能なオブジェクトである場合、それは問題なく変更できます。たとえば、xがそれ自体を変更するinc()メソッドをサポートするオブジェクトである場合、x.inc()は問題なく機能します。
Thanh DK

@ThanhDK変数に書き込めないということではないですか?可変オブジェクトからメソッドの呼び出しを使用する場合、それはそれ自体を変更するように指示しているだけであり、実際には変数を変更しているわけではありません(オブジェクトへの参照を保持しているだけです)。言い換えると、変数がx指す参照は、たとえinc()何を呼び出してもまったく同じままであり、変数に効率的に書き込んでいません。
user193130

4
#2のimvよりも厳密に、のプロパティを作成xするcounter別のオプションがあります。
オロメ

9
Python 3にはnonlocalキーワードがありglobalます。これは、外部関数の変数に似ていますが。これにより、内部関数が外部関数から名前を再バインドできるようになります。「名前にバインドする」は「変数を変更する」よりも正確だと思います。
leewz 2016年

16

Python 2にはクロージャーがありませんでした- クロージャーにた回避策がありました。

すでに与えられた答えにはたくさんの例があります-変​​数を内部関数にコピーする、内部関数のオブジェクトを変更する、などです。

Python 3では、サポートがより明確になり、簡潔になっています。

def closure():
    count = 0
    def inner():
        nonlocal count
        count += 1
        print(count)
    return inner

使用法:

start = closure()
start() # prints 1
start() # prints 2
start() # prints 3

nonlocalキーワードは、それを囲む効果で、明示的に言及した外側の変数への内部機能をバインドします。したがって、より明確に「閉鎖」です。


1
参考までに、興味深い:docs.python.org/3/reference/…。なぜpython3のドキュメントでクロージャーに関する詳細情報(およびJSからのクロージャーの動作を期待できるか)を見つけるのが簡単ではないのか、わかりません。
user3773048

9

別の永続的な名前空間が必要な状況がありました。私はクラスを使いました。そうでなければ私はしません。分離されているが永続的な名前はクロージャです。

>>> class f2:
...     def __init__(self):
...         self.a = 0
...     def __call__(self, arg):
...         self.a += arg
...         return(self.a)
...
>>> f=f2()
>>> f(2)
2
>>> f(2)
4
>>> f(4)
8
>>> f(8)
16

# **OR**
>>> f=f2() # **re-initialize**
>>> f(f(f(f(2)))) # **nested**
16

# handy in list comprehensions to accumulate values
>>> [f(i) for f in [f2()] for i in [2,2,4,8]][-1] 
16

6
def nested1(num1): 
    print "nested1 has",num1
    def nested2(num2):
        print "nested2 has",num2,"and it can reach to",num1
        return num1+num2    #num1 referenced for reading here
    return nested2

与える:

In [17]: my_func=nested1(8)
nested1 has 8

In [21]: my_func(5)
nested2 has 5 and it can reach to 8
Out[21]: 13

これは、クロージャとは何か、どのように使用できるかの例です。


0

わかりやすくするために、PythonとJSの例を簡単に比較できるようにしたいと思います。

JS:

function make () {
  var cl = 1;
  function gett () {
    console.log(cl);
  }
  function sett (val) {
    cl = val;
  }
  return [gett, sett]
}

そして実行:

a = make(); g = a[0]; s = a[1];
s(2); g(); // 2
s(3); g(); // 3

Python:

def make (): 
  cl = 1
  def gett ():
    print(cl);
  def sett (val):
    cl = val
  return gett, sett

そして実行:

g, s = make()
g() #1
s(2); g() #1
s(3); g() #1

理由:他の多くの人が前述したように、Pythonでは、内部スコープに同じ名前の変数への割り当てがある場合、内部スコープに新しい参照が作成されます。varキーワードで明示的に宣言しない限り、JSではそうではありません。

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