Làm gì chức năng (lambda) đóng cửa chụp?


249

Gần đây tôi bắt đầu chơi xung quanh với Python và tôi đã bắt gặp một thứ gì đó đặc biệt trong cách đóng cửa hoạt động. Hãy xem xét các mã sau đây:

adders=[0,1,2,3]

for i in [0,1,2,3]:
   adders[i]=lambda a: i+a

print adders[1](3)

Nó xây dựng một mảng đơn giản các hàm lấy một đầu vào và trả về đầu vào đó được thêm bởi một số. Các hàm được xây dựng theo forvòng lặp trong đó trình vòng lặp ichạy từ 0đến 3. Đối với mỗi số này, một lambdahàm được tạo để bắt ivà thêm nó vào đầu vào của hàm. Dòng cuối cùng gọi lambdahàm thứ hai với 3tham số. Tôi ngạc nhiên đầu ra là 6.

Tôi mong đợi a 4. Lý do của tôi là: trong Python mọi thứ đều là một đối tượng và do đó, mỗi biến là một con trỏ cần thiết cho nó. Khi tạo các bao lambdađóng cho i, tôi dự kiến ​​nó sẽ lưu một con trỏ tới đối tượng số nguyên hiện được trỏ tới i. Điều đó có nghĩa là khi iđược gán một đối tượng số nguyên mới, nó sẽ không ảnh hưởng đến các bao đóng được tạo trước đó. Đáng buồn thay, kiểm tra addersmảng trong trình gỡ lỗi cho thấy rằng nó làm. Tất cả các lambdahàm đề cập đến giá trị cuối cùng của i, 3kết quả là adders[1](3)trả về 6.

Điều này làm tôi tự hỏi về những điều sau đây:

  • Những gì đóng cửa chụp chính xác?
  • Cách thanh lịch nhất để thuyết phục các lambdachức năng để nắm bắt giá trị hiện tại củai tại theo cách sẽ không bị ảnh hưởng khi ithay đổi giá trị của nó là gì?

35
Tôi đã có vấn đề này trong mã UI. Thả tôi hạt dẻ. Bí quyết là hãy nhớ rằng các vòng lặp không tạo ra phạm vi mới.
gièm pha

3
@TimMB Làm thế nào iđể rời khỏi không gian tên?
gièm pha

3
@detly Vâng, tôi sẽ nói rằng print isẽ không hoạt động sau vòng lặp. Nhưng tôi đã thử nó cho chính mình và bây giờ tôi thấy ý của bạn - nó hoạt động. Tôi không có ý tưởng rằng các biến vòng lặp kéo dài sau thân vòng lặp trong python.
Tim MB

1
@TimMB - Vâng, đó là những gì tôi muốn nói. Tương tự cho if, with, try, vv
detly

13
Đây là trong Câu hỏi thường gặp về Python chính thức, bên dưới Tại sao lambdas được xác định trong một vòng lặp với các giá trị khác nhau đều trả về cùng một kết quả? , với cả một lời giải thích và cách giải quyết thông thường.
abarnert

Câu trả lời:


161

Câu hỏi thứ hai của bạn đã được trả lời, nhưng đối với câu hỏi đầu tiên của bạn:

đóng cửa chụp chính xác những gì?

Phạm vi trong Python là động và từ vựng. Một bao đóng sẽ luôn nhớ tên và phạm vi của biến, chứ không phải đối tượng mà nó trỏ tới. Vì tất cả các hàm trong ví dụ của bạn được tạo trong cùng một phạm vi và sử dụng cùng một tên biến, chúng luôn tham chiếu đến cùng một biến.

EDIT: Liên quan đến câu hỏi khác của bạn về cách khắc phục điều này, có hai cách bạn nghĩ đến:

  1. Cách ngắn gọn nhất, nhưng không hoàn toàn tương đương là cách được đề xuất bởi Adrien Plisson . Tạo lambda với một đối số phụ và đặt giá trị mặc định của đối số phụ cho đối tượng bạn muốn bảo tồn.

  2. Một chút dài dòng hơn nhưng ít hack hơn sẽ tạo ra một phạm vi mới mỗi khi bạn tạo lambda:

    >>> adders = [0,1,2,3]
    >>> for i in [0,1,2,3]:
    ...     adders[i] = (lambda b: lambda a: b + a)(i)
    ...     
    >>> adders[1](3)
    4
    >>> adders[2](3)
    5

    Phạm vi ở đây được tạo bằng cách sử dụng một hàm mới (lambda, để đơn giản), liên kết đối số của nó và truyền giá trị bạn muốn liên kết làm đối số. Tuy nhiên, trong mã thực, rất có thể bạn sẽ có một hàm thông thường thay vì lambda để tạo phạm vi mới:

    def createAdder(x):
        return lambda y: y + x
    adders = [createAdder(i) for i in range(4)]

1
Tối đa, nếu bạn thêm câu trả lời cho câu hỏi khác của tôi (câu hỏi đơn giản hơn), tôi có thể đánh dấu đây là câu trả lời được chấp nhận. Cám ơn!
Boaz

3
Python có phạm vi tĩnh, không phải phạm vi động .. tất cả các biến đều là tham chiếu, vì vậy khi bạn đặt biến cho một đối tượng mới, chính biến đó (tham chiếu) có cùng vị trí, nhưng nó chỉ đến một thứ khác. điều tương tự xảy ra trong Đề án nếu bạn set!. xem ở đây để biết phạm vi động thực sự là gì: voidspace.org.uk/python/articles/code_blocks.shtml .
Claudiu

6
Tùy chọn 2 giống với ngôn ngữ chức năng sẽ gọi là "Hàm bị cong".
Crashworks

205

bạn có thể buộc bắt một biến bằng một đối số có giá trị mặc định:

>>> for i in [0,1,2,3]:
...    adders[i]=lambda a,i=i: i+a  # note the dummy parameter with a default value
...
>>> print( adders[1](3) )
4

ý tưởng là khai báo một tham số (được đặt tên khéo léo i) và đặt cho nó một giá trị mặc định của biến bạn muốn nắm bắt (giá trị của i)


7
+1 để sử dụng các giá trị mặc định. Được đánh giá khi lambda được xác định làm cho chúng hoàn hảo cho việc sử dụng này.
quornian

21
+1 cũng bởi vì đây là giải pháp được xác nhận bởi Câu hỏi thường gặp chính thức .
abarnert

23
Thật đáng kinh ngạc. Tuy nhiên, hành vi Python mặc định là không.
Cecil Curry

1
Đây dường như không phải là một giải pháp tốt mặc dù ... bạn thực sự đang thay đổi chữ ký hàm chỉ để chụp một bản sao của biến. Và cả những người gọi hàm có thể gây rối với biến i, phải không?
David Callanan

@DavidCallanan chúng ta đang nói về lambda: một loại chức năng đặc biệt mà bạn thường xác định trong mã của riêng mình để cắm một lỗ, không phải là thứ bạn chia sẻ thông qua toàn bộ sdk. nếu bạn cần một chữ ký mạnh hơn, bạn nên sử dụng một chức năng thực sự.
Adrien Plisson

33

Để hoàn thành một câu trả lời khác cho câu hỏi thứ hai của bạn: Bạn có thể sử dụng một phần trong mô-đun funcools .

Với việc nhập thêm từ toán tử như Chris Lutz đã đề xuất, ví dụ trở thành:

from functools import partial
from operator import add   # add(a, b) -- Same as a + b.

adders = [0,1,2,3]
for i in [0,1,2,3]:
   # store callable object with first argument given as (current) i
   adders[i] = partial(add, i) 

print adders[1](3)

24

Hãy xem xét các mã sau đây:

x = "foo"

def print_x():
    print x

x = "bar"

print_x() # Outputs "bar"

Tôi nghĩ rằng hầu hết mọi người sẽ không thấy điều này khó hiểu cả. Đó là hành vi dự kiến.

Vì vậy, tại sao mọi người nghĩ rằng nó sẽ khác nhau khi nó được thực hiện trong một vòng lặp? Tôi biết tôi đã tự làm sai, nhưng tôi không biết tại sao. Đó là vòng lặp? Hoặc có lẽ là lambda?

Xét cho cùng, vòng lặp chỉ là một phiên bản ngắn hơn của:

adders= [0,1,2,3]
i = 0
adders[i] = lambda a: i+a
i = 1
adders[i] = lambda a: i+a
i = 2
adders[i] = lambda a: i+a
i = 3
adders[i] = lambda a: i+a

11
Đó là vòng lặp, bởi vì trong nhiều ngôn ngữ khác, vòng lặp có thể tạo ra một phạm vi mới.
gièm pha

1
Câu trả lời này là tốt bởi vì nó giải thích tại sao cùng một ibiến được truy cập cho mỗi hàm lambda.
David Callanan

3

Để trả lời câu hỏi thứ hai của bạn, cách thức thanh lịch nhất để làm điều này là sử dụng một hàm có hai tham số thay vì một mảng:

add = lambda a, b: a + b
add(1, 3)

Tuy nhiên, sử dụng lambda ở đây là một chút ngớ ngẩn. Python cung cấp cho chúng ta operatormô-đun, cung cấp giao diện chức năng cho các toán tử cơ bản. Lambda ở trên có chi phí không cần thiết chỉ để gọi toán tử cộng:

from operator import add
add(1, 3)

Tôi hiểu rằng bạn đang chơi xung quanh, cố gắng khám phá ngôn ngữ, nhưng tôi không thể tưởng tượng được một tình huống tôi sẽ sử dụng một loạt các chức năng mà sự kỳ quặc của Python sẽ cản trở.

Nếu bạn muốn, bạn có thể viết một lớp nhỏ sử dụng cú pháp lập chỉ mục mảng của bạn:

class Adders(object):
    def __getitem__(self, item):
        return lambda a: a + item

adders = Adders()
adders[1](3)

2
Chris, tất nhiên đoạn mã trên không liên quan gì đến vấn đề ban đầu của tôi. Nó được xây dựng để minh họa quan điểm của tôi một cách đơn giản. Nó là tất nhiên vô nghĩa và ngớ ngẩn.
Boaz

3

Đây là một ví dụ mới làm nổi bật cấu trúc dữ liệu và nội dung của một bao đóng, để giúp làm rõ khi bối cảnh kèm theo được "lưu".

def make_funcs():
    i = 42
    my_str = "hi"

    f_one = lambda: i

    i += 1
    f_two = lambda: i+1

    f_three = lambda: my_str
    return f_one, f_two, f_three

f_1, f_2, f_3 = make_funcs()

Những gì trong một đóng cửa?

>>> print f_1.func_closure, f_1.func_closure[0].cell_contents
(<cell at 0x106a99a28: int object at 0x7fbb20c11170>,) 43 

Đáng chú ý, my_str không đóng cửa của F1.

Có gì trong đóng cửa của f2?

>>> print f_2.func_closure, f_2.func_closure[0].cell_contents
(<cell at 0x106a99a28: int object at 0x7fbb20c11170>,) 43

Lưu ý (từ các địa chỉ bộ nhớ) rằng cả hai bao đóng chứa cùng một đối tượng. Vì vậy, bạn có thể bắt đầu nghĩ về hàm lambda như có một tham chiếu đến phạm vi. Tuy nhiên, my_str không nằm trong bao đóng cho f_1 hoặc f_2 và tôi không ở trong bao đóng cho f_3 (không hiển thị), điều này cho thấy bản thân các đối tượng đóng là các đối tượng riêng biệt.

Là các đối tượng đóng cửa chính họ cùng một đối tượng?

>>> print f_1.func_closure is f_2.func_closure
False

NB Đầu ra int object at [address X]>khiến tôi nghĩ rằng việc đóng cửa đang lưu trữ [địa chỉ X] AKA một tham chiếu. Tuy nhiên, [địa chỉ X] sẽ thay đổi nếu biến được gán lại sau câu lệnh lambda.
Jeff
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.