Mengapa fungsi bersarang python tidak disebut penutupan?

249

Saya telah melihat dan menggunakan fungsi bersarang di Python, dan mereka cocok dengan definisi penutupan. Jadi mengapa mereka disebut nested functionsbukan closures?

Apakah fungsi bersarang tidak tertutup karena tidak digunakan oleh dunia luar?

UPDATE: Saya membaca tentang penutupan dan membuat saya berpikir tentang konsep ini sehubungan dengan Python. Saya mencari dan menemukan artikel yang disebutkan oleh seseorang dalam komentar di bawah, tetapi saya tidak bisa sepenuhnya memahami penjelasan dalam artikel itu, jadi itu sebabnya saya menanyakan pertanyaan ini.

Srikar Appalaraju
sumber
8
Menariknya, beberapa googling menemukan saya ini, tertanggal Desember 2006: effbot.org/zone/closure.htm . Saya tidak yakin — apakah "duplikat eksternal" disukai SO?
hbw
1
PEP 227 - Ruang Lingkup Bersarang Statis untuk informasi lebih lanjut.
Jujur Abe

Jawaban:

394

Penutupan terjadi ketika suatu fungsi memiliki akses ke variabel lokal dari lingkup terlampir yang telah menyelesaikan eksekusi.

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

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

Ketika make_printerdipanggil, frame baru diletakkan di stack dengan kode yang dikompilasi untuk printerfungsi sebagai konstanta dan nilai msgsebagai lokal. Itu kemudian membuat dan mengembalikan fungsi. Karena fungsi printerreferensi msgvariabel, itu tetap hidup setelah make_printerfungsi telah kembali.

Jadi, jika fungsi bersarang Anda tidak

  1. variabel akses yang bersifat lokal untuk menyertakan lingkup,
  2. melakukannya ketika mereka dieksekusi di luar ruang lingkup itu,

maka mereka bukan penutupan.

Berikut adalah contoh fungsi bersarang yang bukan merupakan penutupan.

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

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

Di sini, kami mengikat nilai ke nilai default parameter. Ini terjadi ketika fungsi printerdibuat dan jadi tidak ada referensi ke nilai msgeksternal printer perlu dipertahankan setelah make_printerkembali. msghanya variabel lokal normal dari fungsi printerdalam konteks ini.

aaronasterling
sumber
2
Jawaban Anda jauh lebih baik daripada saya, Anda membuat poin yang baik, tetapi Jika kita akan pergi dengan definisi pemrograman fungsional yang paling ketat, apakah contoh Anda bahkan berfungsi? Sudah lama, dan saya tidak ingat apakah pemrograman fungsional yang ketat memungkinkan untuk fungsi yang tidak mengembalikan nilai. Intinya adalah bisa diperdebatkan, jika Anda menganggap nilai kembali menjadi Tidak Ada, tapi itu adalah topik lain.
mikerobi
6
@mikerobi, saya tidak yakin bahwa kita perlu mempertimbangkan pemrograman fungsional karena python sebenarnya bukan bahasa fungsional walaupun tentu saja dapat digunakan seperti itu. Tetapi, tidak, fungsi-fungsi batin bukanlah fungsi-fungsi dalam pengertian itu karena seluruh intinya adalah menciptakan efek-efek sampingan. Sangat mudah untuk membuat fungsi yang mengilustrasikan poin juga,
aaronasterling
31
@mikerobi: Apakah gumpalan kode merupakan penutupan atau tidak, tergantung pada apakah atau tidak menutup lingkungannya, bukan apa yang Anda sebut itu. Itu bisa berupa rutinitas, fungsi, prosedur, metode, blok, subrutin, apa pun. Di Ruby, metode tidak bisa berupa penutupan, hanya blok yang bisa. Di Jawa, metode tidak bisa menjadi penutupan, tetapi kelas bisa. Itu tidak membuat mereka kurang dari penutupan. (Meskipun fakta bahwa mereka hanya menutup beberapa variabel, dan mereka tidak dapat memodifikasinya, membuat mereka hampir tidak berguna.) Anda bisa berargumen bahwa suatu metode hanyalah sebuah prosedur yang ditutup self. (Dalam JavaScript / Python itu hampir benar.)
Jörg W Mittag
3
@ JörgWMittag Silakan tentukan "closes over".
Evgeni Sergeev
4
@EvgeniSergeev "menutup" yaitu merujuk "ke variabel lokal [katakanlah, i] dari lingkup melampirkan". merujuk, yaitu dapat memeriksa (atau mengubah) inilai, bahkan jika / ketika lingkup itu "telah menyelesaikan eksekusi", yaitu pelaksanaan suatu program telah maju ke bagian lain dari kode. Blok iyang didefinisikan tidak lebih, namun fungsi yang merujuk imasih dapat melakukannya. Ini biasanya digambarkan sebagai "penutupan variabel i". Untuk tidak berurusan dengan variabel spesifik, itu dapat diimplementasikan sebagai penutupan seluruh kerangka lingkungan di mana variabel itu didefinisikan.
Will Ness
103

Pertanyaannya sudah dijawab oleh aaronasterling

Namun, seseorang mungkin tertarik pada bagaimana variabel disimpan di bawah tenda.

Sebelum datang ke cuplikan:

Penutupan adalah fungsi yang mewarisi variabel dari lingkungan terlampirnya. Ketika Anda melewatkan fungsi panggilan balik sebagai argumen ke fungsi lain yang akan melakukan I / O, fungsi panggilan balik ini akan dipanggil nanti, dan fungsi ini akan - hampir secara ajaib - mengingat konteks di mana ia dinyatakan, bersama dengan semua variabel yang tersedia dalam konteks itu.

  • Jika suatu fungsi tidak menggunakan variabel bebas itu tidak membentuk penutupan.

  • Jika ada level dalam lain yang menggunakan variabel bebas - semua level sebelumnya menyimpan lingkungan leksikal (contoh di akhir)

  • atribut fungsi func_closuredalam python <3.X atau __closure__dalam python> 3.X menyimpan variabel gratis.

  • Setiap fungsi dalam python memiliki atribut penutupan ini, tetapi tidak menyimpan konten apa pun jika tidak ada variabel gratis.

contoh: atribut penutupan tetapi tidak ada konten di dalamnya karena tidak ada variabel gratis.

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

NB: VARIABEL GRATIS HARUS MENCIPTAKAN PENUTUPAN.

Saya akan menjelaskan menggunakan cuplikan yang sama seperti di atas:

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

Dan semua fungsi Python memiliki atribut closure jadi mari kita periksa variabel-variabel penutup yang terkait dengan fungsi closure.

Berikut ini adalah atribut func_closureuntuk fungsi tersebutprinter

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

The closureatribut mengembalikan tuple benda sel yang berisi rincian dari variabel didefinisikan dalam lingkup melampirkan.

Elemen pertama dalam func_closure yang bisa berupa None atau tuple sel yang berisi binding untuk variabel bebas fungsi dan hanya baca.

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

Di sini, di output di atas Anda dapat melihat cell_contents, mari kita lihat apa yang disimpannya:

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

Jadi, ketika kita memanggil fungsi printer(), ia mengakses nilai yang disimpan di dalam cell_contents. Beginilah cara kami mendapatkan output sebagai 'Foo!'

Sekali lagi saya akan menjelaskan menggunakan cuplikan di atas dengan beberapa perubahan:

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

Dalam cuplikan di atas, saya tidak mencetak pesan di dalam fungsi printer, sehingga tidak membuat variabel gratis. Karena tidak ada variabel gratis, tidak akan ada konten di dalam penutupan. Itulah tepatnya yang kita lihat di atas.

Sekarang saya akan menjelaskan cuplikan lain untuk membersihkan semuanya Free Variabledengan 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')

Jadi, kita melihat bahwa func_closureproperti adalah kumpulan sel penutupan , kita dapat merujuknya dan isinya secara eksplisit - sel memiliki properti "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
>>>

Di sini ketika kita menelepon inn, itu akan merujuk semua variabel bebas simpan sehingga kita dapatkanI am free variable

>>> inn('variable')
'I am free variable'
>>>
James Sapam
sumber
9
Dalam Python 3, func_closuresekarang disebut __closure__, mirip dengan berbagai func_*atribut lainnya .
lvc
3
Juga __closure_tersedia dalam Python 2.6+ untuk kompatibilitas dengan Python 3.
Pierre
Penutupan mengacu pada catatan yang menyimpan variabel tertutup, yang melekat pada objek fungsi. Bukan fungsi itu sendiri. Dalam Python, itu adalah __closure__objek yang merupakan penutupan.
Martijn Pieters
Terima kasih @MartijnPieters atas klarifikasi Anda.
James Sapam
71

Python memiliki dukungan yang lemah untuk penutupan. Untuk melihat apa yang saya maksud, ambil contoh berikut dari penghitung yang menggunakan penutupan dengan 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

Penutupan cukup elegan karena memberikan fungsi yang ditulis seperti ini kemampuan untuk memiliki "memori internal". Pada Python 2.7 ini tidak mungkin. Jika kamu mencoba

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

count = initCounter();

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

Anda akan mendapatkan kesalahan dengan mengatakan bahwa x tidak didefinisikan. Tetapi bagaimana itu bisa terjadi jika telah ditunjukkan oleh orang lain bahwa Anda dapat mencetaknya? Ini karena bagaimana Python mengelola ruang lingkup fungsi variabel. Sementara fungsi dalam dapat membaca variabel fungsi luar, ia tidak dapat menuliskannya .

Ini sungguh memalukan. Tetapi dengan hanya read-only closure Anda setidaknya dapat menerapkan pola dekorator fungsi yang Python menawarkan gula sintaksis.

Memperbarui

Seperti yang telah ditunjukkan, ada beberapa cara untuk mengatasi batasan lingkup python dan saya akan mengungkap beberapa.

1. Gunakan globalkata kunci (secara umum tidak disarankan).

2. Dalam Python 3.x, gunakan nonlocalkata kunci (disarankan oleh @unutbu dan @leewz)

3. Tentukan kelas modifikasi sederhanaObject

class Object(object):
    pass

dan membuat bagian Object scopedalam initCounteruntuk menyimpan variabel

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

    return counter

Karena scopebenar-benar hanya referensi, tindakan yang diambil dengan bidangnya tidak benar-benar mengubah scopedirinya sendiri, sehingga tidak ada kesalahan muncul.

4. Cara alternatif, seperti yang ditunjukkan @unutbu, adalah mendefinisikan setiap variabel sebagai array ( x = [0]) dan memodifikasi elemen pertama ( x[0] += 1). Sekali lagi tidak ada kesalahan muncul karena xitu sendiri tidak dimodifikasi.

5. Seperti yang disarankan oleh @raxacoricofallapatorius, Anda dapat membuat xproperticounter

def initCounter ():

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

    counter.x = 0
    return counter
Cristian Garcia
sumber
27
Ada beberapa cara untuk mengatasi hal ini. Di Python2, Anda bisa membuat x = [0]di lingkup luar, dan menggunakannya x[0] += 1di lingkup dalam. Di Python3, Anda dapat menyimpan kode seperti apa adanya dan menggunakan kata kunci nonlokal .
unutbu
"Sementara fungsi dalam dapat membaca variabel fungsi luar, ia tidak dapat menuliskannya." - Ini tidak akurat sesuai komentar unutbu. Masalahnya adalah ketika Python menemukan sesuatu seperti x = ..., x ditafsirkan sebagai variabel lokal, yang tentu saja belum didefinisikan pada saat itu. OTOH, jika x adalah objek yang dapat diubah dengan metode yang dapat diubah, ia dapat dimodifikasi dengan baik, misalnya jika x adalah objek yang mendukung metode inc () yang bermutasi itu sendiri, x.inc () akan bekerja tanpa hambatan.
Thanh DK
@ ThanhDK Bukankah itu berarti Anda tidak dapat menulis ke variabel? Ketika Anda menggunakan panggilan metode dari objek yang bisa diubah, Anda hanya mengatakannya untuk memodifikasi sendiri, Anda sebenarnya tidak memodifikasi variabel (yang hanya menyimpan referensi ke objek). Dengan kata lain, referensi yang xditunjuk variabel tetap sama persis meskipun Anda menelepon inc()atau apa pun, dan Anda tidak secara efektif menulis ke variabel.
user193130
4
Ada pilihan lain, lebih baik daripada # 2, imv, untuk membuat xproperticounter .
orome
9
Python 3 memiliki nonlocalkata kunci, yang seperti globaltetapi untuk variabel fungsi luar. Ini akan memungkinkan fungsi dalam untuk mengubah nama dari fungsi luarnya. Saya pikir "ikat nama" lebih akurat daripada "ubah variabel".
leewz
16

Python 2 tidak memiliki penutupan - memiliki solusi yang menyerupai penutupan.

Ada banyak contoh dalam jawaban yang sudah diberikan - menyalin variabel ke fungsi bagian dalam, memodifikasi objek pada fungsi bagian dalam, dll.

Dalam Python 3, dukungan lebih eksplisit - dan ringkas:

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

Pemakaian:

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

Kata nonlocalkunci mengikat fungsi dalam ke variabel luar yang disebutkan secara eksplisit, akibatnya melampirkannya. Oleh karena itu lebih eksplisit 'penutupan'.

Lee Benson
sumber
1
Menarik, untuk referensi: docs.python.org/3/reference/… . Saya tidak tahu mengapa tidak mudah untuk menemukan info lebih lanjut tentang penutupan (dan bagaimana Anda mungkin mengharapkan mereka berperilaku, berasal dari JS) dalam dokumentasi python3?
user3773048
9

Saya memiliki situasi di mana saya membutuhkan ruang nama yang terpisah namun persisten. Saya menggunakan kelas. Saya tidak sebaliknya. Nama-nama yang dipisahkan tetapi tetap adalah penutupan.

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

Memberi:

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

Ini adalah contoh dari apa penutupan itu dan bagaimana itu bisa digunakan.

Krcn U
sumber
0

Saya ingin menawarkan perbandingan sederhana antara contoh python dan JS, jika ini membantu memperjelas.

JS:

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

dan mengeksekusi:

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

dan mengeksekusi:

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

Alasan: Seperti banyak orang lain katakan di atas, dengan python, jika ada tugas dalam lingkup dalam untuk variabel dengan nama yang sama, referensi baru dalam lingkup dalam dibuat. Tidak demikian halnya dengan JS, kecuali jika Anda secara eksplisit mendeklarasikannya dengan varkata kunci.

forumulator
sumber