Python iç içe işlevleri neden kapanış olarak adlandırmıyor?


249

Python'da iç içe geçmiş işlevleri gördüm ve kullandım ve bir kapatma tanımıyla eşleşiyorlar. Peki neden nested functionsbunun yerine çağrılıyorlar closures?

İç dünya işlevleri dış dünya tarafından kullanılmadığı için kapanmıyor mu?

GÜNCELLEME: Kapanışları okuyordum ve Python ile ilgili bu konsepti düşünmemi sağladı. Aşağıdaki yorumda birisi tarafından bahsedilen makaleyi araştırdım ve buldum, ancak bu makaledeki açıklamayı tam olarak anlayamadım, bu yüzden bu soruyu soruyorum.


8
İlginç bir şekilde, bazı Google çalışanları beni Aralık 2006 tarihli buldu: effbot.org/zone/closure.htm . Emin değilim — "harici kopyalar" SO üzerinde kaşlarını çattı mı?
hbw

Yanıtlar:


394

Bir işlev, yürütme işlemini tamamlayan bir çevreleme kapsamından yerel bir değişkene erişime sahip olduğunda bir kapatma oluşur.

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

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

Zaman make_printerolarak adlandırılır, yeni bir çerçeve için derlenmiş kodu ile yığın alınır printerbir sabit ve değeri olarak işlev msgyerel olarak. Daha sonra işlevi oluşturur ve döndürür. İşlev değişkene printerbaşvurduğundan msg, make_printerişlev döndükten sonra canlı tutulur .

Yani, iç içe geçmiş işlevleriniz

  1. çevreleyen kapsamlara yerel erişim değişkenleri,
  2. bu kapsam dışında yürütüldüklerinde,

o zaman onlar kapanmazlar.

İşte kapanış olmayan bir iç içe işlev örneği.

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

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

Burada, değeri bir parametrenin varsayılan değerine bağlarız. Bu, işlev printeryaratıldığında oluşur ve bu nedenle, geri dönüşlerden sonra msgharici değerin printerkorunması gerekmez make_printer. bu bağlamda msgişlevin normal bir yerel değişkeni printer.


2
Cevabınız benimkinden çok daha iyi, iyi bir noktaya değindiniz, ama en katı fonksiyonel programlama tanımlarına gidecek olursak, örnekleriniz fonksiyonlar mı? Bir süre oldu ve katı fonksiyonel programlamanın değer döndürmeyen işlevlere izin verip vermediğini hatırlayamıyorum. Dönüş değeri Yok olarak kabul edilirse, bu nokta tartışmalıdır, ancak bu başka bir konudur.
mikerobi

6
mikerobi, python gerçekten işlevsel bir dil olmadığı için fonksiyonel programlamayı dikkate almamız gerektiğinden emin değilim, ancak bu şekilde kesinlikle kullanılabilir. Ancak, hayır, iç işlevler bu anlamda işlev değildir, çünkü tüm noktaları yan etkiler yaratmaktır. Gerçi sadece de puan gösteren bir işlevi oluşturmak kolaydır
aaronasterling

31
@mikerobi: Bir kod bloğunun bir kapanış olup olmadığı, adını verdiğiniz şeye değil, çevresine kapanıp kapanmamasına bağlıdır. Rutin, fonksiyon, prosedür, yöntem, blok, altyordam, her neyse olabilir. Ruby'de yöntemler kapatılamaz, sadece bloklar olabilir. Java'da yöntemler kapatılamaz, ancak sınıflar olabilir. Bu onları daha az bir kapanış yapmaz. (Her ne kadar sadece bazı değişkenlerin üzerinde kapanmaları ve bunları değiştirememeleri gerçeği onları işe yaramaz hale getirir.) Bir yöntemin sadece kapalı bir prosedür olduğunu iddia edebilirsiniz self. (JavaScript / Python'da bu neredeyse doğrudur.)
Jörg W Mittag

3
@ JörgWMittag Lütfen "kapanır" ı tanımlayın.
Evgeni Sergeev

4
@EvgeniSergeev "kapatır" yani ibir kapatma kapsamından yerel bir değişkeni [örneğin ] ifade eder ". " iKapsam" yürütme işlemini bitirmiş olsa da / olmasa bile, yani bir programın yürütülmesi kodun diğer bölümlerine yayılmış olsa bile, değerini denetleyebilir (veya değiştirebilir) . iTanımlanan blok artık değil, iyine de atıfta bulunan işlev (ler) bunu yapabilir. Bu genellikle "değişkenin kapatılması" olarak tanımlanır i. Belirli değişkenlerle ilgilenmemek için, değişkenin tanımlandığı tüm ortam çerçevesi üzerinde kapanma olarak uygulanabilir.
Ness Ness

103

Soru zaten aaronasterling tarafından cevaplandı

Bununla birlikte, birisi değişkenlerin kaputun altında nasıl saklandığıyla ilgilenebilir.

Snippet'e gelmeden önce:

Kapaklar, değişkenleri çevreleyen ortamlarından devralan işlevlerdir. Bir fonksiyon geri aramasını G / Ç yapacak başka bir fonksiyona argüman olarak ilettiğinizde, bu geri arama fonksiyonu daha sonra çağrılır ve bu fonksiyon - neredeyse sihirli bir şekilde - mevcut tüm değişkenlerle birlikte bildirildiği bağlamı hatırlar bu bağlamda.

  • Bir işlev serbest değişkenler kullanmıyorsa, bir kapatma oluşturmaz.

  • Serbest değişkenler kullanan başka bir iç seviye varsa - önceki tüm seviyeler sözlük ortamını korur (örnek sonunda)

  • fonksiyon nitelikleri func_closurede piton <3.X veya __closure__serbest değişkenler tasarrufu python> 3.X.

  • Python'daki her işlevin bu kapatma öznitelikleri vardır, ancak serbest değişken yoksa herhangi bir içerik kaydetmez.

örnek: closure öznitelikleri, ancak serbest değişken olmadığı için içeride içerik yok.

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

NOT: ÜCRETSİZ DEĞİŞİM KAPANIŞ OLMALIDIR.

Yukarıdaki pasajı kullanarak açıklayacağım:

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

Ve tüm Python işlevlerinin bir kapatma özelliği vardır, bu yüzden bir kapatma işleviyle ilişkili çevreleyen değişkenleri inceleyelim.

İşte func_closureişlevin özniteliğiprinter

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

closureÖzelliği, parça kapsamında tanımlanan değişken ayrıntılarını içeren hücre nesnelerin bir demet döndürür.

Func_closure içinde None ya da işlevin serbest değişkenleri için bağlar içeren bir hücre dizisi olan ve salt okunur olan ilk öğe.

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

Yukarıdaki çıktıda görebildiğiniz gibi cell_contents, ne depoladığını görelim:

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

Bu nedenle, işlevi çağırdığımızda, printer()içinde saklanan değere erişir cell_contents. Bu şekilde 'Foo!'

Yine yukarıdaki pasajı bazı değişikliklerle kullanmayı açıklayacağım:

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

Yukarıdaki snippet'te, msg'yi yazıcı işlevinin içine yazdırmıyorum, bu yüzden herhangi bir serbest değişken oluşturmuyor. Serbest değişken olmadığından, kapak içinde içerik olmayacaktır. Bu tam olarak yukarıda gördüğümüz şey.

Şimdi her şeyi temizlemek için başka farklı parçacığını açıklayacağız Free Variableile Closure:

>>> 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')

Yani, bir func_closureözelliğin bir dizi kapatma hücresi olduğunu görüyoruz görüyoruz, onları ve içeriklerini açıkça ifade edebiliriz - bir hücrenin "cell_contents" özelliği vardır

>>> 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
>>>

Burada aradığımızda inn, tüm kaydedilmiş serbest değişkenleri gösterecektir, böyleceI am free variable

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

9
Python 3'te, func_closureşimdi __closure__diğer çeşitli func_*özelliklere benzer şekilde çağrılmaktadır .
lvc

3
Ayrıca __closure_Python 3 ile uyumluluk için Python 2.6+ sürümünde mevcuttur
Pierre

Kapatma , işlev nesnesine eklenen kapalı değişkenleri depolayan kaydı ifade eder. Bu işlevin kendisi değildir. Python'da __closure__kapanan nesne budur.
Martijn Pieters

Açıklama için @MartijnPieters'a teşekkürler.
James Sapam

71

Python'un kapanma için zayıf bir desteği var. Ne demek istediğimi görmek için JavaScript ile kapatma kullanarak aşağıdaki sayaç örneğini alın:

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

Kapatma oldukça zariftir, çünkü bu şekilde yazılan fonksiyonlara "dahili hafızaya" sahip olma yeteneği verir. Python 2.7'den itibaren bu mümkün değildir. Eğer denersen

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

count = initCounter();

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

X'in tanımlanmadığını belirten bir hata mesajı alırsınız. Ancak başkaları tarafından yazdırabileceğiniz gösterilmişse bu nasıl olabilir? Bunun nedeni, Python'un işlevler değişken kapsamını nasıl yönettiğidir. İç işlev dış işlevin değişkenlerini okuyabilirken , yazamaz bunları .

Bu gerçekten bir utanç. Ancak sadece salt okunur kapama ile en azından fonksiyon dekoratör desenini uygulayabilirsiniz Python'un sözdizimsel şeker sunduğu .

Güncelleme

Belirtildiği gibi, python'un kapsam sınırlamaları ile başa çıkmanın yolları vardır ve bazılarını ortaya koyacağım.

1. kullanınglobal Anahtar kelimeyi (genel olarak önerilmez).

2. Python 3.x'te, nonlocalanahtar kelimeyi kullanın (@unutbu ve @leewz tarafından önerilir)

3. Değiştirilebilir basit bir sınıf tanımlayınObject

class Object(object):
    pass

ve değişkenleri saklamak için bir Object scopeinitCounter

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

    return counter

Yana scopegerçekten sadece bir referanstır, onun alanları ile yapılan işlemler gerçekten değişiklik yapmayınscope hiçbir hata ortaya çıkar, böylece kendini.

4. @unutbu'nun işaret ettiği gibi alternatif bir yol, her değişkeni bir dizi ( x = [0]) olarak tanımlamak ve ilk öğesini ( x[0] += 1) değiştirmek olacaktır . Yine hata oluşmaz çünküx kendisi değiştirilmez.

5. @raxacoricofallapatorius önerdiği gibi, yapabilir xbir özelliğinicounter

def initCounter ():

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

    counter.x = 0
    return counter

27
Bunun etrafında yollar var. Python2'de, x = [0]dış kapsamda yapabilir x[0] += 1ve iç kapsamda kullanabilirsiniz. Python3'te, kodunuzu olduğu gibi koruyabilir ve yerel olmayan anahtar sözcüğü kullanabilirsiniz .
unutbu

"İç işlev dış işlevin değişkenlerini okuyabilse de, bunları yazamaz." - Bu unutbu'nun yorumuna göre yanlış. Sorun, Python x = ... gibi bir şeyle karşılaştığında, x'in yerel bir değişken olarak yorumlanmasıdır, ki bu elbette o noktada henüz tanımlanmamıştır. OTOH, eğer x değişebilir bir yönteme sahip değişken bir nesne ise, gayet iyi değiştirilebilir, örneğin x kendisini değiştiren inc () yöntemini destekleyen bir nesne ise, x.inc () bir aksama olmadan çalışır.
Thanh DK

@ThanhDK Bu, değişkene yazamayacağınız anlamına gelmiyor mu? Değişken bir nesneden bir yöntem çağırdığınızda, yalnızca kendisini değiştirmesini söylersiniz, aslında değişkeni değiştirmezsiniz (yalnızca nesneye bir başvuru içerir). Başka bir deyişle, değişkenin xişaret ettiği referans, aradığınız inc()veya herhangi bir şey yapsanız bile tam olarak aynı kalır ve değişkene etkili bir şekilde yazmadınız.
user193130

4
Bir mülk yapma xkonusundacounter # 2, imv'den kesinlikle daha iyi başka bir seçenek var .
orome

9
Python 3, bir dış fonksiyonun değişkenleri için olan fakat nonlocalanahtar kelimeye sahiptir global. Bu, bir iç işlevin, dış işlev (ler) den bir ismi yeniden hatırlamasına olanak tanır. Ben "isme bağlanmak" "değişkeni değiştirmek" daha doğru olduğunu düşünüyorum.
leewz

16

Python 2'nin kapanışları yoktu - benzer geçici çözümlere sahipti kapanışlara .

Halihazırda verilen cevaplarda çok sayıda örnek vardır - değişkenlerde iç fonksiyona kopyalama, iç fonksiyondaki bir nesneyi değiştirme, vb.

Python 3'te destek daha açık ve özlü:

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

Kullanımı:

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

nonlocalAnahtar iliştirilen etkisi, açık bir şekilde bahsedilen dış değişken iç fonksiyonu bağlanır. Dolayısıyla daha açık bir şekilde bir 'kapanış'.


1
İlginç, referans için: docs.python.org/3/reference/… . Python3 belgelerinde kapaklar hakkında daha fazla bilgi (ve JS'den gelen davranışlarını nasıl bekleyebilirsiniz) neden kolay değil bilmiyorum?
user3773048

9

Ayrı ama kalıcı bir isim alanına ihtiyaç duyduğum bir durumum vardı. Sınıflar kullandım. Aksi halde değilim. Ayrılmış ancak kalıcı isimler kapaklardır.

>>> 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

verir:

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

Bu, bir kapağın ne olduğuna ve nasıl kullanılabileceğine bir örnektir.


0

İşleri daha net hale getirmeye yardımcı olursa, python ve JS örneği arasında başka bir basit karşılaştırma sunmak istiyorum.

JS:

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

ve yürütme:

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

ve yürütme:

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

Sebep: Yukarıda belirtildiği gibi, python'da, iç kapsamda aynı ada sahip bir değişkene bir atama varsa, iç kapsamda yeni bir referans oluşturulur. varAnahtar kelimeyle açıkça beyan etmediğiniz sürece JS'de böyle değildir .

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.