การปิดคำศัพท์ทำงานอย่างไร


149

ขณะที่ฉันกำลังตรวจสอบปัญหาที่ฉันมีเกี่ยวกับการปิดคำศัพท์ในรหัส Javascript ฉันมาพร้อมปัญหานี้ใน Python:

flist = []

for i in xrange(3):
    def func(x): return x * i
    flist.append(func)

for f in flist:
    print f(2)

lambdaหมายเหตุว่าตัวอย่างนี้สติหลีกเลี่ยง มันพิมพ์ "4 4 4" ซึ่งน่าแปลกใจ ฉันคาดหวัง "0 2 4"

รหัส Perl ที่เทียบเท่านี้ไม่ถูกต้อง:

my @flist = ();

foreach my $i (0 .. 2)
{
    push(@flist, sub {$i * $_[0]});
}

foreach my $f (@flist)
{
    print $f->(2), "\n";
}

พิมพ์ "0 2 4"

คุณช่วยอธิบายความแตกต่างได้มั้ย


ปรับปรุง:

ปัญหาไม่ได้เกิดจากiความเป็นสากล สิ่งนี้แสดงพฤติกรรมที่เหมือนกัน:

flist = []

def outer():
    for i in xrange(3):
        def inner(x): return x * i
        flist.append(inner)

outer()
#~ print i   # commented because it causes an error

for f in flist:
    print f(2)

ในขณะที่บรรทัดแสดงความคิดเห็นiไม่เป็นที่รู้จัก ณ จุดนั้น ถึงกระนั้นมันก็พิมพ์ "4 4 4"



3
นี่เป็นบทความที่ดีเกี่ยวกับเรื่องนี้ me.veekun.com/blog/2011/04/24/gotcha-python-scoping-closures
updogliu

คำตอบ:


151

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

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

flist = []

for i in xrange(3):
    def funcC(j):
        def func(x): return x * j
        return func
    flist.append(funcC(i))

for f in flist:
    print f(2)

นี่คือสิ่งที่เกิดขึ้นเมื่อคุณผสมผลข้างเคียงและการตั้งโปรแกรมการทำงาน


5
โซลูชันของคุณเป็นโซลูชันที่ใช้ใน Javascript
Eli Bendersky

9
นี่ไม่ใช่การประพฤติตัวไม่เหมาะสม มันมีพฤติกรรมตรงตามที่กำหนดไว้
Alex Coventry

6
IMO piro มีโซลูชันที่ดีกว่าstackoverflow.com/questions/233673/…
jfs

2
ฉันอาจจะเปลี่ยน 'i' ที่อยู่ด้านในสุดเป็น 'j' เพื่อความชัดเจน
Eggyntax

7
แล้วนิยามมันเป็นแบบนี้:def inner(x, i=i): return x * i
dashesy

152

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

เพื่อประเมินiและใช้ค่าของมันรูปแบบทั่วไปคือการตั้งค่าให้เป็นค่าเริ่มต้นของพารามิเตอร์: ค่าเริ่มต้นของพารามิเตอร์จะถูกประเมินเมื่อdefคำสั่งถูกเรียกใช้และทำให้ค่าของตัวแปรลูปถูกแช่แข็ง

งานต่อไปนี้เป็นไปตามที่คาดไว้:

flist = []

for i in xrange(3):
    def func(x, i=i): # the *value* of i is copied in func() environment
        return x * i
    flist.append(func)

for f in flist:
    print f(2)

7
s / ที่เวลารวบรวม / ในขณะที่defคำสั่งถูกดำเนินการ /
jfs

23
นี่เป็นวิธีที่ชาญฉลาดซึ่งทำให้มันน่ากลัว
Stavros Korokithakis

มีปัญหาหนึ่งสำหรับโซลูชันนี้: func มีพารามิเตอร์สองตัวในขณะนี้ ซึ่งหมายความว่าจะไม่ทำงานกับพารามิเตอร์จำนวนตัวแปร ที่แย่กว่านั้นถ้าคุณเรียกใช้ func ด้วยพารามิเตอร์ที่สองสิ่งนี้จะเขียนทับต้นฉบับiจากคำจำกัดความ :-(
Pascal

34

นี่คือวิธีที่คุณใช้functoolsห้องสมุด (ซึ่งฉันไม่แน่ใจว่ามีให้ในเวลาที่ตั้งคำถาม)

from functools import partial

flist = []

def func(i, x): return x * i

for i in xrange(3):
    flist.append(partial(func, i))

for f in flist:
    print f(2)

เอาต์พุต 0 2 4 ตามที่คาดไว้


ฉันต้องการใช้สิ่งนี้จริงๆ แต่ฟังก์ชั่นของฉันคือวิธีการเรียนที่ไม่ดีและค่าแรกที่ส่งมาคือตัวเอง อย่างไรก็ตามมีรอบนี้หรือไม่
Michael David Watson

1
อย่างแน่นอน สมมติว่าคุณมีคลาสคณิตศาสตร์พร้อมเมธอด add (self, a, b) และคุณต้องการตั้งค่า = 1 เพื่อสร้างเมธอด 'increment' จากนั้นสร้างอินสแตนซ์ของคลาสของคุณ 'my_math' และวิธีการเพิ่มของคุณจะเป็น 'increment = partial (my_math.add, 1)'
Luca Invernizzi

2
ในการใช้เทคนิคนี้กับวิธีการคุณสามารถใช้functools.partialmethod()เป็น python 3.4
Matt Eding

13

ดูนี่สิ:

for f in flist:
    print f.func_closure


(<cell at 0x00C980B0: int object at 0x009864B4>,)
(<cell at 0x00C980B0: int object at 0x009864B4>,)
(<cell at 0x00C980B0: int object at 0x009864B4>,)

มันหมายความว่าพวกมันชี้ไปที่อินสแตนซ์ตัวแปร i เดียวกันซึ่งจะมีค่าเป็น 2 เมื่อวนรอบจบลง

โซลูชันที่อ่านได้:

for i in xrange(3):
        def ffunc(i):
            def func(x): return x * i
            return func
        flist.append(ffunc(i))

1
คำถามของฉันคือ "ทั่วไป" มากกว่า Python มีข้อบกพร่องนี้ทำไม ฉันคาดหวังว่าภาษาที่สนับสนุนการปิดคำศัพท์ (เช่น Perl และราชวงศ์ Lisp ทั้งหมด) ในการทำงานอย่างถูกต้อง
Eli Bendersky

2
ถามว่าทำไมบางอย่างมีข้อบกพร่องสมมติว่าไม่ใช่ข้อบกพร่อง
Null303

7

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

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

(let ((ii 1)) (
  (do ((i 1 (+ 1 i)))
      ((>= i 4))
    (set! flist 
      (cons (lambda (x) (* ii x)) flist))
    (set! ii i))
))

ลองดูที่นี่สำหรับการอภิปรายเพิ่มเติมเกี่ยวกับเรื่องนี้

[แก้ไข] อาจเป็นวิธีที่ดีกว่าในการอธิบายก็คือให้คิดถึงลูปดูเป็นแมโครซึ่งทำตามขั้นตอนต่อไปนี้:

  1. กำหนดแลมบ์ดารับพารามิเตอร์เดียว (i) โดยมีเนื้อหาที่กำหนดโดยเนื้อความของลูป
  2. การเรียกแลมบ์ดานั้นในทันทีด้วยค่าที่เหมาะสมของ i เป็นพารามิเตอร์

กล่าวคือ เทียบเท่ากับงูหลามด้านล่าง:

flist = []

def loop_body(i):      # extract body of the for loop to function
    def func(x): return x*i
    flist.append(func)

map(loop_body, xrange(3))  # for i in xrange(3): body

i ไม่ได้อยู่ในขอบเขตของ parent อีกต่อไป แต่เป็นตัวแปรใหม่ในขอบเขตของตัวเอง (เช่นพารามิเตอร์กับแลมบ์ดา) ดังนั้นคุณจะได้รับพฤติกรรมที่คุณสังเกตเห็น Python ไม่มีขอบเขตใหม่ที่ชัดเจนนี้ดังนั้นเนื้อความของ for loop จะแชร์ตัวแปร i เท่านั้น


น่าสนใจ ฉันไม่ได้ตระหนักถึงความแตกต่างในความหมายของลูปการทำ ขอบคุณ
Eli Bendersky

4

ฉันยังไม่มั่นใจเลยว่าทำไมในบางภาษาจึงใช้งานได้ในทางเดียวและอีกวิธีหนึ่ง Common LISP เหมือน Python:

(defvar *flist* '())

(dotimes (i 3 t)
  (setf *flist* 
    (cons (lambda (x) (* x i)) *flist*)))

(dolist (f *flist*)  
  (format t "~a~%" (funcall f 2)))

พิมพ์ "6 6 6" (โปรดทราบว่านี่คือรายการจาก 1 ถึง 3 และสร้างขึ้นในแบบย้อนกลับ ") ในขณะที่อยู่ใน Scheme จะทำงานเหมือนใน Perl:

(define flist '())

(do ((i 1 (+ 1 i)))
    ((>= i 4))
  (set! flist 
    (cons (lambda (x) (* i x)) flist)))

(map 
  (lambda (f)
    (printf "~a~%" (f 2)))
  flist)

พิมพ์ "6 4 2"

และอย่างที่ฉันได้กล่าวไปแล้ว Javascript อยู่ในค่าย Python / CL ปรากฏว่ามีการตัดสินใจดำเนินการที่นี่ซึ่งภาษาที่แตกต่างเข้าหาในรูปแบบที่แตกต่างกัน ฉันชอบที่จะเข้าใจว่าการตัดสินใจคืออะไรกันแน่


8
ความแตกต่างอยู่ใน (ทำ ... ) มากกว่ากฎการกำหนดขอบเขต ในโครงร่างจะสร้างตัวแปรใหม่ทุกการส่งผ่านลูปในขณะที่ภาษาอื่น ๆ จะใช้การโยงที่มีอยู่อีกครั้ง ดูคำตอบของฉันสำหรับรายละเอียดเพิ่มเติมและตัวอย่างของแบบแผนที่มีพฤติกรรมคล้ายกับ lisp / python
Brian

2

ปัญหาคือว่าทุกฟังก์ชั่นท้องถิ่นผูกกับสภาพแวดล้อมเดียวกันและทำให้iตัวแปรเดียวกัน วิธีแก้ปัญหา (วิธีแก้ปัญหา) คือการสร้างสภาพแวดล้อมที่แยกต่างหาก (กรอบสแต็ก) สำหรับแต่ละฟังก์ชั่น (หรือแลมบ์ดา):

t = [ (lambda x: lambda y : x*y)(x) for x in range(5)]

>>> t[1](2)
2
>>> t[2](2)
4

1

ตัวแปรiคือโกลบอลซึ่งมีค่าเป็น 2 ในแต่ละครั้งที่ฟังก์ชันfเรียก

ฉันมีแนวโน้มที่จะใช้พฤติกรรมของคุณหลังจากนี้:

>>> class f:
...  def __init__(self, multiplier): self.multiplier = multiplier
...  def __call__(self, multiplicand): return self.multiplier*multiplicand
... 
>>> flist = [f(i) for i in range(3)]
>>> [g(2) for g in flist]
[0, 2, 4]

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


0

เหตุผลเบื้องหลังพฤติกรรมนั้นได้รับการอธิบายแล้วและมีการโพสต์วิธีแก้ปัญหาหลายอย่างแล้ว แต่ฉันคิดว่านี่เป็น pythonic ที่สุด (จำไว้ว่าทุกอย่างใน Python เป็นวัตถุ!):

flist = []

for i in xrange(3):
    def func(x): return x * func.i
    func.i=i
    flist.append(func)

for f in flist:
    print f(2)

คำตอบของ Claudiu นั้นค่อนข้างดีการใช้ตัวสร้างฟังก์ชั่น แต่คำตอบของ piro คือแฮ็คพูดตรงๆเพราะมันทำให้ฉันกลายเป็นอาร์กิวเมนต์ "ซ่อน" ที่มีค่าเริ่มต้น (มันจะทำงานได้ดี แต่มันไม่ใช่ "pythonic") .


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

1
สิ่งนี้จะไม่ทำงานบน Python 2 หรือ 3 (ทั้งคู่เอาท์พุท "4 4 4") funcในx * func.iมักจะอ้างถึงฟังก์ชั่นล่าสุดที่กำหนดไว้ ดังนั้นแม้ว่าแต่ละฟังก์ชั่นจะมีหมายเลขที่ถูกต้องติดอยู่ แต่มันก็จบลงด้วยการอ่านจากฟังก์ชั่นสุดท้าย
Lambda Fairy
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.