2012-06-19 26 views
13

Tôi có một mã đơn giản sau đây:Weird hành vi đóng cửa trong python

def get(): 
    return [lambda: i for i in [1, 2, 3]] 

for f in get(): 
    print(f()) 

Đúng như dự đoán từ kiến ​​thức python của tôi, đầu ra là 3 - danh sách toàn bộ sẽ chứa giá trị cuối cùng của i. Nhưng làm thế nào điều này hoạt động nội bộ?

AFAIK, biến python chỉ đơn giản là tham chiếu đến đối tượng, vì vậy đóng cửa đầu tiên phải kèm theo đối tượng đầu tiên i tham chiếu - và đối tượng này chắc chắn là 1, không phải 3 O_O. Làm thế nào nó xảy ra rằng đóng cửa python bao quanh biến thay vì đối tượng tham chiếu biến này? Liệu nó có lưu tên biến là văn bản thuần túy, một số "tham chiếu đến biến" hay không?

Trả lời

9

Đóng cửa không tham chiếu đến biến số nhưng thay vào đó là phạm vi. Vì giá trị cuối cùng của i trong phạm vi của nó là '3', cả ba kết thúc trả về giống nhau. Để "khóa" các giá trị hiện tại của một biến, tạo ra một phạm vi mới chỉ dành riêng cho nó:

def get() : return [ (lambda x: lambda: x)(i) for i in [ 1, 2, 3 ] ] 
for f in get() : print(f()) 
+0

Có một số thông tin bổ sung có sẵn về chủ đề này để tôi có thể xem các biến ẩn được sử dụng để "tiết kiệm" phạm vi, truy cập, v.v ... không? – grigoryvp

+2

'(lambda x: lambda: x) (i)' là một trong những điều xấu nhất mà tôi đã thấy trong Python. Ick. (Không phải câu trả lời của bạn là sai hay xấu, mỗi lần, chỉ cần nói - thật khó đọc). –

+2

@EyeofHell: Tôi nghĩ [pep-227] (http://www.python.org/dev/peps/pep-0227/) là tài liệu chuẩn về quy tắc phạm vi python. Ngoài ra, có một số câu trả lời hay về điều này ở đây trên SO, ví dụ: [ở đây] (http://stackoverflow.com/questions/291978/short-description-of-python-scoping-rules) – georg

11

Như @ thg435 chỉ ra, một lambda sẽ không đóng gói các giá trị vào thời điểm đó, nhưng thay vì phạm vi. Có nhiều cách quá nhỏ, bạn có thể giải quyết này:

lambda đối số mặc định "hack"

[ lambda v=i: v for i in [ 1, 2, 3 ] ] 

Hoặc sử dụng functools.partial

from functools import partial 
[ partial(lambda v: v, i) for i in [ 1, 2, 3 ] ] 

Về cơ bản bạn phải di chuyển phạm vi được địa phương chức năng bạn đang tạo. Nói chung tôi thích sử dụng partial thường xuyên hơn kể từ khi bạn có thể vượt qua nó một cuộc gọi, và bất kỳ args và kargs để tạo ra một cuộc gọi với một đóng cửa thích hợp. Trong nội bộ, nó là gói ban đầu của bạn có thể gọi để phạm vi được chuyển cho bạn.

+5

+1 Để sử dụng một phần ở đây, tôi nghĩ rằng đó là một cách tiếp cận sạch hơn nhiều. Tuy nhiên, bạn có thể muốn chỉnh sửa mã của mình, các dấu cách bên trong dấu ngoặc ôm danh sách là xấu theo PEP-8. (Tôi biết bạn đang theo dõi người hỏi, tôi đã chỉnh sửa nó ở đó). –

+0

@Lattyware: Cảm ơn! lambdas là mát mẻ nhưng tôi luôn luôn tìm thấy chúng lộn xộn hơn để đọc. một phần chỉ cảm thấy dễ đọc hơn và di động hơn. – jdi

+0

100% đồng ý, tôi cố gắng tránh lambdas bất cứ nơi nào tôi có thể, và đây là một trường hợp chính mà một phần là cả hai rõ ràng hơn và đơn giản hơn. –

4

Mỗi lambda thực sự đề cập đến cùng một số i, là biến được tạo bởi danh sách hiểu. Khi chấm dứt việc hiểu danh sách, i duy trì giá trị của phần tử cuối cùng mà nó được gán cho đến khi nó nằm ngoài phạm vi (được ngăn chặn bằng cách đóng gói nó trong một hàm và trả về, cụ thể là lambda). Như những người khác đã chỉ ra, đóng cửa không duy trì bản sao của các giá trị, mà là duy trì các tham chiếu đến các biến được xác định trong phạm vi của chúng.