Làm thế nào để đóng cửa từ vựng làm việc?


149

Trong khi tôi đang điều tra một vấn đề tôi gặp phải với việc đóng từ vựng trong mã Javascript, tôi đã gặp vấn đề này trong Python:

flist = []

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

for f in flist:
    print f(2)

Lưu ý rằng ví dụ này lưu tâm tránh lambda. Nó in "4 4 4", thật đáng ngạc nhiên. Tôi mong đợi "0 2 4".

Mã Perl tương đương này thực hiện đúng:

my @flist = ();

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

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

"0 2 4" được in.

Bạn có thể vui lòng giải thích sự khác biệt?


Cập nhật:

Vấn đề không phải làiđược toàn cầu. Điều này hiển thị hành vi tương tự:

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)

Như dòng bình luận cho thấy, ikhông rõ tại thời điểm đó. Tuy nhiên, nó in "4 4 4".



3
Đây là một bài viết khá tốt về vấn đề này. me.veekun.com/blog/2011/04/24/gotcha-python-scoping-closures
updogliu

Câu trả lời:


151

Python thực sự hoạt động như được định nghĩa. Ba hàm riêng biệt được tạo, nhưng mỗi hàm đều có môi trường mà chúng được xác định - trong trường hợp này là môi trường toàn cầu (hoặc môi trường của hàm ngoài nếu vòng lặp được đặt bên trong hàm khác). Đây chính xác là vấn đề, mặc dù - trong môi trường này, tôi bị đột biến và tất cả các lần đóng đều đề cập đến cùng một i .

Đây là giải pháp tốt nhất tôi có thể đưa ra - thay vào đó hãy tạo ra một hàm và gọi . Điều này sẽ buộc các môi trường khác nhau cho mỗi chức năng được tạo, với một i khác nhau trong mỗi chức năng .

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)

Đây là những gì xảy ra khi bạn trộn lẫn các tác dụng phụ và lập trình chức năng.


5
Giải pháp của bạn cũng là giải pháp được sử dụng trong Javascript.
Eli Bendersky

9
Đây không phải là hành vi sai trái. Nó hoạt động chính xác như được định nghĩa.
Alex Coventry

6
IMO piro có giải pháp stackoverflow.com/questions/233673/ trên
jfs

2
Tôi có lẽ sẽ thay đổi 'i' trong cùng thành 'j' cho rõ ràng.
eggsyntax

7
những gì về việc chỉ định nghĩa nó như thế này:def inner(x, i=i): return x * i
bảnh bao

152

Các hàm được định nghĩa trong vòng lặp tiếp tục truy cập cùng một biến itrong khi giá trị của nó thay đổi. Ở cuối vòng lặp, tất cả các hàm đều trỏ đến cùng một biến, đang giữ giá trị cuối cùng trong vòng lặp: hiệu ứng là những gì được báo cáo trong ví dụ.

Để đánh giá ivà sử dụng giá trị của nó, một mẫu chung là đặt nó làm mặc định tham số: mặc định tham số được ước tính khi defcâu lệnh được thực thi và do đó giá trị của biến vòng lặp bị đóng băng.

Các công việc sau đây như mong đợi:

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 / tại thời gian biên dịch / tại thời điểm khi defcâu lệnh được thực thi /
jfs

23
Đây là một giải pháp khéo léo, làm cho nó kinh khủng.
Stavros Korokithakis

Có một vấn đề với giải pháp này: func hiện có hai tham số. Điều đó có nghĩa là nó không hoạt động với một lượng tham số khác nhau. Tệ hơn, nếu bạn gọi func với tham số thứ hai, nó sẽ ghi đè lên bản gốc itừ định nghĩa. :-(
Pascal

34

Đây là cách bạn thực hiện bằng functoolsthư viện (điều mà tôi không chắc là có sẵn tại thời điểm câu hỏi được đặt ra).

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)

Đầu ra 0 2 4, như mong đợi.


Tôi thực sự muốn sử dụng điều này nhưng chức năng của tôi thực sự là một phương thức lớp và giá trị đầu tiên được truyền là tự. Có cách nào để khắc phục điều này?
Michael David Watson

1
Chắc chắn rồi. Giả sử bạn có một lớp Toán với phương thức add (self, a, b) và bạn muốn đặt a = 1 để tạo phương thức 'gia tăng'. Sau đó, tạo một thể hiện của lớp bạn 'my_math' và phương thức tăng của bạn sẽ là 'tăng = một phần (my_math.add, 1)'.
Luca Invernizzi

2
Để áp dụng kỹ thuật này cho một phương pháp, bạn cũng có thể sử dụng functools.partialmethod()kể từ python 3.4
Matt Eding

13

nhìn này

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ều đó có nghĩa là tất cả chúng đều trỏ đến cùng một thể hiện biến i, sẽ có giá trị là 2 khi vòng lặp kết thúc.

Một giải pháp dễ đọc:

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

1
Câu hỏi của tôi là "chung chung" hơn. Tại sao Python có lỗ hổng này? Tôi mong đợi một ngôn ngữ hỗ trợ việc đóng từ vựng (như Perl và toàn bộ triều đại Lisp) để giải quyết vấn đề này một cách chính xác.
Eli Bendersky

2
Hỏi lý do tại sao một cái gì đó có một lỗ hổng là giả định rằng nó không phải là một lỗ hổng.
Null303

7

Điều đang xảy ra là biến i được bắt giữ và các hàm đang trả về giá trị mà nó bị ràng buộc tại thời điểm nó được gọi. Trong các ngôn ngữ chức năng, loại tình huống này không bao giờ phát sinh, vì tôi sẽ không hồi phục. Tuy nhiên với python, và cũng như bạn đã thấy với lisp, điều này không còn đúng nữa.

Sự khác biệt với ví dụ lược đồ của bạn là làm với ngữ nghĩa của vòng lặp do. Lược đồ đang tạo hiệu quả một biến i mới mỗi lần qua vòng lặp, thay vì sử dụng lại một ràng buộc i hiện có như với các ngôn ngữ khác. Nếu bạn sử dụng một biến khác được tạo bên ngoài vào vòng lặp và biến đổi nó, bạn sẽ thấy hành vi tương tự trong sơ đồ. Hãy thử thay thế vòng lặp của bạn bằng:

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

Hãy xem ở đây để thảo luận thêm về điều này.

[Chỉnh sửa] Có thể là một cách tốt hơn để mô tả nó là nghĩ về vòng lặp do là một macro thực hiện các bước sau:

  1. Xác định lambda lấy một tham số (i), với phần thân được xác định bởi phần thân của vòng lặp,
  2. Một cuộc gọi ngay lập tức của lambda với các giá trị thích hợp của i là tham số của nó.

I E. tương đương với con trăn dưới đây:

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

Chữ i không còn là một từ phạm vi cha mà là một biến hoàn toàn mới trong phạm vi của chính nó (tức là tham số cho lambda) và do đó bạn có được hành vi bạn quan sát. Python không có phạm vi mới ẩn này, vì vậy phần thân của vòng lặp for chỉ chia sẻ biến i.


Hấp dẫn. Tôi đã không nhận thức được sự khác biệt về ngữ nghĩa của vòng lặp do. Cảm ơn
Eli Bendersky

4

Tôi vẫn chưa hoàn toàn bị thuyết phục tại sao trong một số ngôn ngữ, cách này hoạt động theo một cách, và theo một cách khác. Trong Common Lisp, nó giống như Python:

(defvar *flist* '())

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

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

In "6 6 6" (lưu ý rằng ở đây danh sách là từ 1 đến 3 và được xây dựng ngược "). Trong khi trong Lược đồ, nó hoạt động như trong 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)

In "6 4 2"

Và như tôi đã đề cập, Javascript nằm trong trại Python / CL. Có vẻ như có một quyết định thực hiện ở đây, mà các ngôn ngữ khác nhau tiếp cận theo những cách riêng biệt. Tôi rất muốn hiểu chính xác quyết định là gì.


8
Sự khác biệt là ở (làm ...) chứ không phải là quy tắc phạm vi. Trong lược đồ tạo ra một biến mới mỗi lần đi qua vòng lặp, trong khi các ngôn ngữ khác sử dụng lại ràng buộc hiện có. Xem câu trả lời của tôi để biết thêm chi tiết và một ví dụ về phiên bản lược đồ có hành vi tương tự như lisp / python.
Brian

2

Vấn đề là tất cả các hàm cục bộ liên kết với cùng một môi trường và do đó với cùng một ibiến. Giải pháp (giải pháp thay thế) là tạo các môi trường riêng biệt (khung xếp chồng) cho từng chức năng (hoặc lambda):

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

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

1

Biến ilà toàn cục, có giá trị là 2 tại mỗi lần hàmf được gọi.

Tôi sẽ có khuynh hướng thực hiện hành vi mà bạn theo sau như sau:

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

Đáp lại cập nhật của bạn : Đây không phải là globalness của i mỗi gia nhập mà gây ra hành vi này, đó là một thực tế rằng đó là một biến từ một phạm vi kèm theo trong đó có một giá trị cố định so với thời điểm khi f được gọi. Trong ví dụ thứ hai của bạn, giá trị của iđược lấy từ phạm vi của kkkhàm và không có gì thay đổi khi bạn gọi các hàm trên flist.


0

Lý do đằng sau hành vi đã được giải thích và nhiều giải pháp đã được đăng, nhưng tôi nghĩ đây là điều khó hiểu nhất (hãy nhớ rằng, mọi thứ trong Python đều là một đối tượng!):

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)

Câu trả lời của Claudiu khá hay, sử dụng một trình tạo hàm, nhưng câu trả lời của piro là hack, thành thật mà nói, vì nó biến tôi thành một đối số "ẩn" với một giá trị mặc định (nó sẽ hoạt động tốt, nhưng nó không phải là "pythonic") .


Tôi nghĩ rằng nó phụ thuộc vào phiên bản python của bạn. Bây giờ tôi có nhiều kinh nghiệm hơn và tôi sẽ không còn đề xuất cách làm này nữa. Claudiu là cách thích hợp để đóng cửa trong Python.
darkfeline

1
Điều này sẽ không hoạt động trên Python 2 hoặc 3 (cả hai đều xuất "4 4 4"). Các funcin x * func.isẽ luôn đề cập đến chức năng cuối cùng được xác định. Vì vậy, mặc dù mỗi chức năng riêng lẻ có số chính xác bị mắc kẹt, nhưng cuối cùng tất cả chúng đều đọc từ cái cuối cùng.
Tiên nữ Lambda
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.