Phạm vi của các hàm lambda và các tham số của chúng?


89

Tôi cần một hàm gọi lại gần như giống hệt nhau cho một loạt các sự kiện gui. Hàm sẽ hoạt động hơi khác một chút tùy thuộc vào sự kiện nào đã gọi nó. Có vẻ như một trường hợp đơn giản với tôi, nhưng tôi không thể tìm ra hành vi kỳ lạ này của các hàm lambda.

Vì vậy, tôi có mã đơn giản sau đây:

def callback(msg):
    print msg

#creating a list of function handles with an iterator
funcList=[]
for m in ('do', 're', 'mi'):
    funcList.append(lambda: callback(m))
for f in funcList:
    f()

#create one at a time
funcList=[]
funcList.append(lambda: callback('do'))
funcList.append(lambda: callback('re'))
funcList.append(lambda: callback('mi'))
for f in funcList:
    f()

Đầu ra của mã này là:

mi
mi
mi
do
re
mi

Tôi mong đợi:

do
re
mi
do
re
mi

Tại sao việc sử dụng trình lặp lại làm mọi thứ rối tung lên?

Tôi đã thử sử dụng một nội soi sâu:

import copy
funcList=[]
for m in ('do', 're', 'mi'):
    funcList.append(lambda: callback(copy.deepcopy(m)))
for f in funcList:
    f()

Nhưng điều này có cùng một vấn đề.


3
Tiêu đề câu hỏi của bạn hơi gây hiểu lầm.
lispmachine

1
Tại sao lại sử dụng lambdas nếu bạn thấy chúng khó hiểu? Tại sao không sử dụng def để xác định các hàm? Vấn đề của bạn là gì khiến lambdas quan trọng đến vậy?
S.Lott

@ S. Lott Nested chức năng sẽ dẫn đến cùng một vấn đề (có thể nhiều hơn rõ ràng)
lispmachine

1
@agartland: Bạn có phải là tôi không? Tôi quá là làm việc trên giao diện sự kiện, và tôi đã viết bài kiểm tra gần như giống hệt nhau sau trước khi tìm trang này trong nghiên cứu nền: pastebin.com/M5jjHjFT
imallett

5
Hãy xem 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ả? trong Câu hỏi thường gặp về lập trình chính thức cho Python. Nó giải thích vấn đề khá độc đáo và đưa ra giải pháp.
abarnert

Câu trả lời:


79

Vấn đề ở đây là mbiến (một tham chiếu) được lấy từ phạm vi xung quanh. Chỉ các tham số được giữ trong phạm vi lambda.

Để giải quyết vấn đề này, bạn phải tạo một phạm vi khác cho lambda:

def callback(msg):
    print msg

def callback_factory(m):
    return lambda: callback(m)

funcList=[]
for m in ('do', 're', 'mi'):
    funcList.append(callback_factory(m))
for f in funcList:
    f()

Trong ví dụ trên, lambda cũng sử dụng phạm vi bao quanh để tìm m, nhưng lần này callback_factoryphạm vi được tạo một lần cho mỗi lần callback_factory gọi.

Hoặc với functools.partial :

from functools import partial

def callback(msg):
    print msg

funcList=[partial(callback, m) for m in ('do', 're', 'mi')]
for f in funcList:
    f()

2
Lời giải thích này hơi sai lầm. Vấn đề là sự thay đổi giá trị của m trong lần lặp, không phải phạm vi.
Ixx

Nhận xét ở trên đúng như đã được @abarnert ghi nhận trong phần nhận xét về câu hỏi trong đó liên kết cũng được đưa ra giải thích phenonimon và giải pháp. Phương thức factory mang lại hiệu quả tương tự như đối số của phương thức factory có tác dụng tạo một biến mới với phạm vi cục bộ cho lambda. Tuy nhiên, solition được đưa ra không hoạt động theo cú pháp vì không có đối số nào đối với lambda - và giải pháp lambda trong lamda bên dưới cũng mang lại hiệu quả tương tự mà không cần tạo một phương thức liên tục mới để tạo lambda
Mark Parris

132

Khi một lambda được tạo, nó không tạo một bản sao của các biến trong phạm vi bao quanh mà nó sử dụng. Nó duy trì một tham chiếu đến môi trường để nó có thể tra cứu giá trị của biến sau này. Chỉ có một m. Nó được gán cho mọi thời điểm thông qua vòng lặp. Sau vòng lặp, biến mcó giá trị 'mi'. Vì vậy, khi bạn thực sự chạy hàm bạn đã tạo sau này, nó sẽ tìm kiếm giá trị mtrong môi trường đã tạo ra nó, giá trị này sau đó sẽ có giá trị 'mi'.

Một giải pháp phổ biến và dễ hiểu cho vấn đề này là nắm bắt giá trị mtại thời điểm lambda được tạo bằng cách sử dụng nó làm đối số mặc định của một tham số tùy chọn. Bạn thường sử dụng một tham số có cùng tên để không phải thay đổi nội dung của mã:

for m in ('do', 're', 'mi'):
    funcList.append(lambda m=m: callback(m))

tôi có nghĩa là thông số tùy chọn với giá trị mặc định
lispmachine

6
Giải pháp tốt! Mặc dù phức tạp, tôi cảm thấy ý nghĩa ban đầu rõ ràng hơn so với các cú pháp khác.
Quantum7

3
Không có gì khó hiểu về điều này cả; nó chính xác là giải pháp mà Câu hỏi thường gặp chính thức của Python đề xuất. Xem tại đây .
abarnert

3
@abernert, "hackish và tricky" không nhất thiết là không tương thích với "là giải pháp mà Câu hỏi thường gặp chính thức của Python đề xuất". Cảm ơn đã tham khảo.
Don Hatch

1
Việc sử dụng lại cùng một tên biến là không rõ ràng đối với những người không quen thuộc với khái niệm này. Hình minh họa sẽ tốt hơn nếu nó là lambda n = m. Có, bạn phải thay đổi thông số gọi lại của mình, nhưng nội dung vòng lặp for có thể giữ nguyên như tôi nghĩ.
Nick

6

Tất nhiên, Python sử dụng các tham chiếu, nhưng nó không quan trọng trong bối cảnh này.

Khi bạn xác định lambda (hoặc một hàm, vì đây là cùng một hành vi chính xác), nó không đánh giá biểu thức lambda trước thời gian chạy:

# defining that function is perfectly fine
def broken():
    print undefined_var

broken() # but calling it will raise a NameError

Thậm chí còn đáng ngạc nhiên hơn so với ví dụ lambda của bạn:

i = 'bar'
def foo():
    print i

foo() # bar

i = 'banana'

foo() # you would expect 'bar' here? well it prints 'banana'

Tóm lại, hãy nghĩ động: không có gì được đánh giá trước khi diễn giải, đó là lý do tại sao mã của bạn sử dụng giá trị mới nhất của m.

Khi nó tìm kiếm m trong thực thi lambda, m được lấy từ phạm vi trên cùng, có nghĩa là, như những người khác đã chỉ ra; bạn có thể giải quyết vấn đề đó bằng cách thêm một phạm vi khác:

def factory(x):
    return lambda: callback(x)

for m in ('do', 're', 'mi'):
    funcList.append(factory(m))

Ở đây, khi lambda được gọi, nó sẽ tìm x trong phạm vi định nghĩa của lambda. X này là một biến cục bộ được xác định trong phần thân của nhà máy. Do đó, giá trị được sử dụng khi thực thi lambda sẽ là giá trị đã được truyền dưới dạng tham số trong quá trình gọi đến nhà máy. Và doremi!

Như một lưu ý, tôi có thể đã định nghĩa nhà máy là nhà máy (m) [thay x bằng m], hành vi giống nhau. Tôi đã sử dụng một tên khác cho rõ ràng :)

Bạn có thể thấy rằng Andrej Bauer cũng gặp các vấn đề tương tự về lambda. Điều thú vị trên blog đó là các bình luận, nơi bạn sẽ tìm hiểu thêm về việc đóng python :)


1

Tuy nhiên, không liên quan trực tiếp đến vấn đề đang bàn, nhưng là một phần trí tuệ vô giá: Python Objects của Fredrik Lundh.


1
Không liên quan trực tiếp đến câu trả lời của bạn, nhưng một tìm kiếm cho mèo con: google.com/search?q=kitten
Singletoned

@Singletoned: nếu OP tìm kiếm bài viết mà tôi cung cấp liên kết, họ sẽ không đặt câu hỏi ngay từ đầu; đó là lý do tại sao nó liên quan gián tiếp. Tôi chắc chắn bạn sẽ được vui mừng để giải thích cho tôi làm thế nào chú mèo con đang gián tiếp liên quan đến câu trả lời của tôi (thông qua một cách tiếp cận toàn diện, tôi đoán;)
tzot

1

Vâng, đó là vấn đề về phạm vi, nó liên kết với m bên ngoài, cho dù bạn đang sử dụng lambda hay hàm cục bộ. Thay vào đó, hãy sử dụng một functor:

class Func1(object):
    def __init__(self, callback, message):
        self.callback = callback
        self.message = message
    def __call__(self):
        return self.callback(self.message)
funcList.append(Func1(callback, m))

1

soluiton thành lambda nhiều lambda hơn

In [0]: funcs = [(lambda j: (lambda: j))(i) for i in ('do', 're', 'mi')]

In [1]: funcs
Out[1]: 
[<function __main__.<lambda>>,
 <function __main__.<lambda>>,
 <function __main__.<lambda>>]

In [2]: [f() for f in funcs]
Out[2]: ['do', 're', 'mi']

bên ngoài lambdađược sử dụng để ràng buộc giá trị hiện tại của iđể j

mỗi khi bên ngoài lambdađược gọi, nó tạo ra một thể hiện của bên trong lambdajràng buộc với giá trị hiện tại của ias i's value


0

Đầu tiên, những gì bạn đang thấy không phải là vấn đề và không liên quan đến việc gọi theo tham chiếu hoặc theo giá trị.

Cú pháp lambda mà bạn đã xác định không có tham số và như vậy, phạm vi bạn đang thấy với tham số mnằm ngoài hàm lambda. Đây là lý do tại sao bạn thấy những kết quả này.

Cú pháp Lambda, trong ví dụ của bạn là không cần thiết và bạn muốn sử dụng một lệnh gọi hàm đơn giản:

for m in ('do', 're', 'mi'):
    callback(m)

Một lần nữa, bạn phải rất chính xác về những tham số lambda bạn đang sử dụng và phạm vi chính xác của chúng bắt đầu và kết thúc ở đâu.

Như một lưu ý phụ, liên quan đến việc truyền tham số. Các tham số trong python luôn là tham chiếu đến các đối tượng. Để trích dẫn Alex Martelli:

Vấn đề thuật ngữ có thể là do thực tế là, trong python, giá trị của tên là một tham chiếu đến một đối tượng. Vì vậy, bạn luôn chuyển giá trị (không sao chép ngầm) và giá trị đó luôn là tham chiếu. [...] Bây giờ nếu bạn muốn đặt một tên cho điều đó, chẳng hạn như "theo đối tượng tham chiếu", "theo giá trị chưa được mở", hoặc bất cứ điều gì, hãy là khách của tôi. Cố gắng sử dụng lại thuật ngữ thường được áp dụng cho các ngôn ngữ mà "biến là hộp" sang ngôn ngữ mà "biến là thẻ post-it", IMHO, có nhiều khả năng gây nhầm lẫn hơn là giúp đỡ.


0

Biến mđang được nắm bắt, vì vậy biểu thức lambda của bạn luôn nhìn thấy giá trị "hiện tại" của nó.

Nếu bạn cần nắm bắt hiệu quả giá trị tại một thời điểm, hãy viết một hàm lấy giá trị bạn muốn làm tham số và trả về biểu thức lambda. Tại thời điểm đó, lambda sẽ nắm bắt giá trị của tham số, giá trị này sẽ không thay đổi khi bạn gọi hàm nhiều lần:

def callback(msg):
    print msg

def createCallback(msg):
    return lambda: callback(msg)

#creating a list of function handles with an iterator
funcList=[]
for m in ('do', 're', 'mi'):
    funcList.append(createCallback(m))
for f in funcList:
    f()

Đầu ra:

do
re
mi

0

thực sự không có biến nào theo nghĩa cổ điển trong Python, chỉ là những tên đã được ràng buộc bởi các tham chiếu đến đối tượng áp dụng. Ngay cả các hàm là một số loại đối tượng trong Python và lambdas không tạo ngoại lệ cho quy tắc :)


Khi bạn nói "theo nghĩa cổ điển", bạn có nghĩa là "như C đã nói." Nhiều ngôn ngữ, bao gồm Python, thực hiện các biến khác so với C.
Ned Batchelder

0

Một lưu ý phụ map, mặc dù bị một số nhân vật Python nổi tiếng coi thường, nhưng buộc phải xây dựng để ngăn chặn cạm bẫy này.

fs = map (lambda i: lambda: callback (i), ['do', 're', 'mi'])

NB: đầu tiên lambda ihoạt động giống như nhà máy trong các câu trả lời khác.

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.