คุณสามารถอธิบายการปิด (ที่เกี่ยวข้องกับ Python) ได้หรือไม่?


85

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

คำตอบ:


97

การปิดการปิด

ออบเจ็กต์เป็นข้อมูลที่มีวิธีการแนบการปิดเป็นฟังก์ชันที่แนบข้อมูล

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
โปรดทราบว่าnonlocalมีการเพิ่มเข้ามาใน python 3 python 2.x ไม่มีการปิดแบบเต็มอ่าน - เขียน (กล่าวคือคุณสามารถอ่านปิดเหนือตัวแปร แต่ไม่สามารถเปลี่ยนค่าได้)
James Porter

6
@JamesPorter: หมายเหตุ: คุณสามารถเลียนแบบnonlocalคีย์เวิร์ดใน Python 2 ได้โดยใช้อ็อบเจกต์ที่เปลี่ยนแปลงได้เช่นL = [0] \n def counter(): L[0] += 1; return L[0]คุณไม่สามารถเปลี่ยนชื่อได้ (ผูกกับอ็อบเจ็กต์อื่น) ในกรณีนี้ แต่คุณสามารถเปลี่ยนอ็อบเจกต์ที่เปลี่ยนแปลงได้เองที่ชื่ออ้างถึง ถึง. รายการนี้จำเป็นเนื่องจากจำนวนเต็มไม่เปลี่ยนรูปใน Python
jfs

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 # - สามารถใช้การปิดเพื่อนำโครงสร้างการควบคุมใหม่มาใช้ (จำนวนเท่าใด) การขาดการบล็อกที่ไม่ระบุชื่อเป็นหนึ่งในข้อ จำกัด ของการปิดในหลาม


15

พูดตามตรงฉันเข้าใจการปิดดีอย่างสมบูรณ์ยกเว้นฉันไม่เคยชัดเจนว่าอะไรคือสิ่งที่เป็น "การปิด" และ "การปิด" เกี่ยวกับเรื่องนี้ ฉันขอแนะนำให้คุณเลิกมองหาตรรกะที่อยู่เบื้องหลังการเลือกคำศัพท์

อย่างไรก็ตามนี่คือคำอธิบายของฉัน:

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

bar = foo()
bar()   # print 5

แนวคิดหลักในที่นี้คืออ็อบเจ็กต์ฟังก์ชันที่ส่งคืนจาก foo ยังคงยึด hook ไว้ที่ var 'x' ในโลคัลแม้ว่า 'x' จะอยู่นอกขอบเขตและควรจะเลิกใช้ เบ็ดนี้เป็นของ var เองไม่ใช่แค่ค่าที่ var มีในเวลานั้นดังนั้นเมื่อเรียก bar มันจะพิมพ์ 5 ไม่ใช่ 3

โปรดทราบด้วยว่า Python 2.x มีการปิดแบบ จำกัด : ไม่มีทางที่ฉันจะแก้ไข 'x' inside 'bar' ได้เนื่องจากการเขียน 'x = bla' จะประกาศ 'x' ในแถบท้องถิ่นไม่ใช่กำหนดให้ 'x' ของ foo . นี่คือผลข้างเคียงของการกำหนดของ Python = การประกาศ เพื่อหลีกเลี่ยงปัญหานี้ Python 3.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

ฉันไม่เคยได้ยินว่ามีการใช้ธุรกรรมในบริบทเดียวกับการอธิบายว่าการปิดคืออะไรและไม่มีความหมายของธุรกรรมใด ๆ ที่นี่

เรียกว่าการปิดเนื่องจาก "ปิด" ตัวแปรภายนอก (ค่าคงที่) - กล่าวคือไม่ใช่แค่ฟังก์ชัน แต่เป็นสิ่งที่แนบมาของสภาพแวดล้อมที่ฟังก์ชันถูกสร้างขึ้น

ในตัวอย่างต่อไปนี้การเรียกการปิด g หลังจากเปลี่ยน x จะเปลี่ยนค่าของ x ภายใน g ด้วยเนื่องจาก 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

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'

นอกจากนี้วัตถุเซลล์จะอธิบายไว้ในเอกสาร C API:

ออบเจ็กต์ "เซลล์" ถูกใช้เพื่อใช้ตัวแปรที่อ้างอิงโดยขอบเขตต่างๆ

และเราสามารถแสดงให้เห็นถึงการใช้งานการปิดของเราโดยสังเกตว่า'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

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

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"  

มันใช้งานได้แม้ว่าทั้งสองวิธี "คูณ" และตัวแปร "x" จะไม่มีอยู่อีกต่อไป ทั้งหมดเป็นเพราะความสามารถในการปิดที่จะจำ


0

เราทุกคนเคยใช้Decoratorsใน 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

ที่นี่ฟังก์ชัน wrapper สามารถเข้าถึงวัตถุ func ได้เนื่องจาก wrapper เป็น "การปิดศัพท์" จึงสามารถเข้าถึงแอตทริบิวต์หลักได้ นั่นคือเหตุผลที่มันสามารถเข้าถึงวัตถุ func


0

ฉันต้องการแบ่งปันตัวอย่างและคำอธิบายเกี่ยวกับการปิด ฉันสร้างตัวอย่าง python และตัวเลขสองตัวเพื่อแสดงสถานะสแต็ก

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

นี่คือตัวเลขสองตัวที่จะแสดงสแต็กและการปิดที่แนบมากับอ็อบเจ็กต์ฟังก์ชัน

เมื่อฟังก์ชันถูกส่งคืนจากผู้ผลิต

เมื่อฟังก์ชันถูกเรียกใช้ในภายหลัง

เมื่อฟังก์ชันถูกเรียกผ่านพารามิเตอร์หรือตัวแปรที่ไม่ใช่โลคัลโค้ดจะต้องมีการโยงตัวแปรโลคัลเช่น margin_top, padding รวมถึง a, b, n เพื่อให้แน่ใจว่ารหัสฟังก์ชันทำงานได้ควรเข้าถึงสแต็กเฟรมของฟังก์ชันตัวสร้างที่หายไปนานแล้วซึ่งสำรองไว้ในการปิดที่เราสามารถพบได้พร้อมกับ 'วัตถุฟังก์ชันของข้อความ'


-2

คำอธิบายที่ดีที่สุดที่ฉันเคยเห็นเกี่ยวกับการปิดคือการอธิบายกลไก มันเป็นแบบนี้:

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

ตอนนี้ผ่อนคลายข้อ จำกัด ที่แต่ละโหนดสามารถมีลูกได้เพียงคนเดียว

หากคุณทำเช่นนี้คุณสามารถมีโครงสร้าง ('ผลตอบแทน') ที่สามารถส่งคืนจากโพรซีเดอร์ได้โดยไม่ต้องละทิ้งบริบทภายในเครื่อง (กล่าวคือจะไม่แตกออกจากสแต็กเมื่อคุณส่งคืน) ในครั้งต่อไปที่มีการเรียกใช้โพรซีเดอร์การเรียกใช้จะดึงเฟรมสแต็กเก่า (ทรี) และดำเนินการต่อจากจุดที่ค้างไว้


นั่นไม่ใช่คำอธิบายของการปิด
Jules

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