ทำไมฟังก์ชั่นซ้อนกันของไพ ธ อนจึงไม่เรียกว่าการปิด


249

ฉันได้เห็นและใช้ฟังก์ชั่นที่ซ้อนกันใน Python และมันตรงกับคำจำกัดความของการปิด ดังนั้นทำไมพวกเขาเรียกว่าnested functionsแทนclosures?

ฟังก์ชั่นที่ซ้อนกันไม่ปิดเพราะไม่ได้ใช้งานจากโลกภายนอกหรือไม่?

UPDATE:ฉันกำลังอ่านเกี่ยวกับการปิดและมันทำให้ฉันคิดถึงแนวคิดนี้เกี่ยวกับ Python ฉันค้นหาและพบบทความที่มีคนพูดถึงในความคิดเห็นด้านล่าง แต่ฉันไม่สามารถเข้าใจคำอธิบายในบทความนั้นได้อย่างสมบูรณ์ดังนั้นนี่คือเหตุผลที่ฉันถามคำถามนี้


8
ที่น่าสนใจบาง googling พบฉันนี้ลงวันที่ธันวาคม 2006: 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คอล จากนั้นจะสร้างและส่งคืนฟังก์ชัน เนื่องจากฟังก์ชั่นprinterอ้างอิงmsgตัวแปรมันจะยังคงมีชีวิตอยู่หลังจากmake_printerฟังก์ชั่นกลับมา

ดังนั้นหากฟังก์ชันที่ซ้อนกันของคุณทำไม่ได้

  1. ตัวแปรการเข้าถึงที่อยู่ภายในขอบเขตการปิดล้อม
  2. ทำเช่นนั้นเมื่อพวกเขาถูกดำเนินการนอกขอบเขตนั้น

ถ้าเช่นนั้นพวกเขาจะไม่ปิด

นี่คือตัวอย่างของฟังก์ชั่นที่ซ้อนกันซึ่งไม่ใช่การปิด

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

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

ที่นี่เราจะผูกค่ากับค่าเริ่มต้นของพารามิเตอร์ สิ่งนี้เกิดขึ้นเมื่อprinterมีการสร้างฟังก์ชั่นดังนั้นจึงไม่มีการอ้างอิงถึงค่าmsgภายนอกที่จะprinter ต้องมีการปรับปรุงหลังจากmake_printerส่งคืน msgเป็นเพียงตัวแปรโลคัลปกติของฟังก์ชันprinterในบริบทนี้


2
คำตอบของคุณดีกว่าของฉันมากคุณสร้างจุดที่ดี แต่ถ้าเราจะไปด้วยคำจำกัดความการเขียนโปรแกรมเชิงฟังก์ชันที่เข้มงวดที่สุด ไม่นานมานี้และฉันจำไม่ได้ว่าการเขียนโปรแกรมเชิงฟังก์ชันที่เข้มงวดอนุญาตให้ฟังก์ชันที่ไม่ส่งคืนค่าได้หรือไม่ จุดคือ moot หากคุณพิจารณาว่าค่าส่งคืนเป็น None แต่นั่นเป็นหัวข้ออื่นทั้งหมด
mikerobi

6
@mikerobi ฉันไม่แน่ใจว่าเราจำเป็นต้องคำนึงถึงการเขียนโปรแกรมแบบใช้งานได้เนื่องจาก python ไม่ใช่ภาษาที่ใช้งานได้จริงถึงแม้ว่ามันจะสามารถใช้งานได้อย่างแน่นอน แต่ไม่ฟังก์ชั่นด้านในไม่ได้ทำงานในแง่นั้นเพราะจุดรวมของพวกเขาคือการสร้างผลข้างเคียง มันง่ายที่จะสร้างฟังก์ชั่นที่แสดงถึงจุดต่าง ๆ เช่นกัน
aaronasterling

31
@ Mikerobi: ไม่ว่าจะเป็นการปิดโค้ดหรือไม่นั้นขึ้นอยู่กับว่ามันจะปิดตัวลงหรือไม่ในสิ่งที่คุณเรียก มันอาจเป็นกิจวัตรประจำวันฟังก์ชั่นขั้นตอนวิธีการบล็อกรูทีนย่อยอะไรก็ตาม ใน Ruby วิธีการไม่สามารถปิดได้มีเพียงบล็อกเท่านั้น ใน Java วิธีการไม่สามารถปิดได้ แต่ชั้นเรียนสามารถ นั่นไม่ได้ทำให้พวกเขาปิดน้อยลง (แม้ว่าความจริงที่ว่าพวกเขาเท่านั้นที่ใกล้กว่าบางตัวแปรและพวกเขาก็ไม่สามารถแก้ไขได้ทำให้พวกเขาติดกับไร้ประโยชน์.) selfคุณสามารถยืนยันว่าวิธีการที่เป็นเพียงขั้นตอนปิดมากกว่า (ใน JavaScript / Python เกือบจะเป็นจริง)
Jörg W Mittag

3
@ JörgWMittagโปรดกำหนด "ปิด"
Evgeni Sergeev

4
@EvgeniSergeev "ปิดทับ" ie refer "กับตัวแปรท้องถิ่น [พูด, i] จากขอบเขตการปิดล้อม" หมายถึงคือสามารถตรวจสอบ (หรือเปลี่ยนแปลง) iค่าของแม้ว่า / เมื่อขอบเขตนั้น "เสร็จสิ้นการดำเนินการ" กล่าวคือการดำเนินการของโปรแกรมได้ออกไปยังส่วนอื่น ๆ ของรหัส บล็อกที่iไม่ได้กำหนดไว้มากขึ้น แต่ฟังก์ชั่นที่อ้างถึงiยังสามารถทำได้ นี่คือคำอธิบายทั่วไปว่า "ปิดตัวแปรi" เพื่อไม่ให้จัดการกับตัวแปรเฉพาะมันสามารถนำไปใช้เป็นปิดกรอบสภาพแวดล้อมทั้งหมดที่กำหนดตัวแปรนั้น
Will Ness

103

aaronasterling ตอบคำถามนี้แล้ว

อย่างไรก็ตามบางคนอาจสนใจวิธีการจัดเก็บตัวแปรภายใต้ประทุน

ก่อนที่จะมาถึงตัวอย่าง:

การปิดคือฟังก์ชันที่รับค่าตัวแปรจากสภาพแวดล้อมที่ล้อมรอบ เมื่อคุณส่งการเรียกกลับฟังก์ชันเป็นอาร์กิวเมนต์ไปยังฟังก์ชันอื่นที่จะทำ I / O ฟังก์ชันการเรียกกลับนี้จะถูกเรียกใช้ในภายหลังและฟังก์ชั่นนี้จะเกือบน่าอัศจรรย์ - จำบริบทที่มีการประกาศพร้อมกับตัวแปรทั้งหมดที่มี ในบริบทนั้น

  • หากฟังก์ชั่นไม่ใช้ตัวแปรอิสระมันจะไม่ทำการปิด

  • หากมีอีกระดับภายในซึ่งใช้ตัวแปรอิสระ - ระดับก่อนหน้านี้ทั้งหมดบันทึกสภาพแวดล้อมคำ (ตัวอย่างในตอนท้าย)

  • แอตทริบิวต์ฟังก์ชั่นfunc_closureในหลาม <3.X หรือ__closure__ในหลาม> 3.X บันทึกตัวแปรอิสระ

  • ทุกฟังก์ชั่นใน python มีคุณสมบัติการปิดนี้ แต่จะไม่บันทึกเนื้อหาใด ๆ หากไม่มีตัวแปรอิสระ

ตัวอย่าง: ของคุณลักษณะการปิด แต่ไม่มีเนื้อหาภายในเนื่องจากไม่มีตัวแปรอิสระ

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

NB: ตัวแปรฟรีจะต้องสร้างการปิด

ฉันจะอธิบายโดยใช้ตัวอย่างข้อมูลเดียวกับด้านบน:

>>> 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แอตทริบิวต์ส่งกลับ tuple ของวัตถุเซลล์ซึ่งมีรายละเอียดของตัวแปรที่กำหนดไว้ในขอบเขตล้อมรอบ

องค์ประกอบแรกใน func_closure ซึ่งอาจเป็น None หรือ tuple ของเซลล์ที่มีการเชื่อมสำหรับตัวแปรอิสระของฟังก์ชันและเป็นแบบอ่านอย่างเดียว

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

ในตัวอย่างด้านบนฉันไม่ได้พิมพ์ msg ภายในฟังก์ชันเครื่องพิมพ์ดังนั้นจึงไม่สร้างตัวแปรอิสระใด ๆ เนื่องจากไม่มีตัวแปรอิสระจึงไม่มีเนื้อหาภายในการปิด นั่นคือสิ่งที่เราเห็นด้านบน

ตอนนี้ฉันจะอธิบายตัวอย่างอื่นเพื่อล้างข้อมูลทุกอย่างFree Variableด้วย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')

ดังนั้นเราจะเห็นว่าfunc_closureคุณสมบัติเป็น tuple ของเซลล์ปิดเราสามารถอ้างอิงได้และเนื้อหาของเซลล์เหล่านั้นอย่างชัดเจน - เซลล์มีคุณสมบัติ "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

3
นอกจากนี้ยัง__closure_มีใน Python 2.6+ เพื่อให้เข้ากันได้กับ Python 3
Pierre

การปิดหมายถึงบันทึกที่จัดเก็บตัวแปรปิดเกินที่แนบมากับวัตถุฟังก์ชั่น มันไม่ใช่ฟังก์ชั่นของตัวเอง ใน 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 เสนอน้ำตาลแบบซินแทติกติกได้

ปรับปรุง

ตามที่อธิบายไว้มีวิธีจัดการกับข้อ จำกัด ขอบเขตของงูใหญ่และฉันจะเปิดเผยบางอย่าง

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 คุณสามารถเก็บรหัสของคุณเป็นมันและใช้คำหลักต่างแดน
unutbu

"ในขณะที่ฟังก์ชั่นด้านในสามารถอ่านตัวแปรของฟังก์ชั่นด้านนอกได้ แต่ก็ไม่สามารถเขียนได้" - นี่ไม่ถูกต้องตามความคิดเห็นของ unutbu ปัญหาคือเมื่อ Python พบบางอย่างเช่น x = ... , x ถูกตีความว่าเป็นตัวแปรท้องถิ่นซึ่งแน่นอนยังไม่ได้กำหนดไว้ที่จุดนั้น OTOH ถ้า x เป็นวัตถุที่ไม่แน่นอนด้วยวิธีการที่ไม่แน่นอนก็สามารถปรับเปลี่ยนได้เช่นถ้า x เป็นวัตถุที่สนับสนุนวิธี inc () ซึ่งกลายพันธุ์ตัวเอง x.inc () จะทำงานโดยไม่ต้องผูกปม
Thanh DK

@ThanhDK ไม่ได้หมายความว่าคุณไม่สามารถเขียนตัวแปรได้ใช่ไหม เมื่อคุณใช้การเรียกเมธอดจากออบเจ็กต์ที่ไม่แน่นอนคุณจะบอกให้แก้ไขตัวเองคุณไม่ได้ปรับเปลี่ยนตัวแปรจริง กล่าวอีกนัยหนึ่งการอ้างอิงที่ตัวแปรxชี้ไปยังคงเหมือนเดิมแม้ว่าคุณจะโทรinc()หรืออะไรก็ตามและคุณไม่ได้เขียนลงไปในตัวแปรอย่างมีประสิทธิภาพ
user193130

4
มีตัวเลือกอื่นอย่างเคร่งครัดดีกว่า # 2, IMV ของเป็นทำให้xcounterทรัพย์สินของ
orome

9
Python 3 มีnonlocalคำหลักซึ่งมีลักษณะเหมือนglobalแต่สำหรับตัวแปรของฟังก์ชันภายนอก สิ่งนี้จะช่วยให้ฟังก์ชั่นด้านในสามารถ rebind ชื่อจากฟังก์ชั่นด้านนอก ฉันคิดว่า "ผูกกับชื่อ" แม่นยำกว่า "แก้ไขตัวแปร"
leewz

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/... ฉันไม่รู้ว่าทำไมการหาข้อมูลเพิ่มเติมเกี่ยวกับการปิด (และวิธีที่คุณอาจคาดหวังให้พวกเขาประพฤติตนมาจาก JS) ในเอกสาร python3 ง่ายไหม?
3773048

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

งูหลาม:

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

เหตุผล:ตามที่คนอื่นหลายคนกล่าวไว้ข้างต้นในไพ ธ อนถ้ามีการมอบหมายในขอบเขตภายในให้กับตัวแปรที่มีชื่อเดียวกันการอ้างอิงใหม่ในขอบเขตภายในจะถูกสร้างขึ้น ไม่ใช่กับ JS เว้นแต่คุณจะประกาศอย่างชัดเจนด้วยvarคำหลัก

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.