Khả năng hiểu danh sách nhắc lại tên ngay cả sau phạm vi hiểu. Thê nay đung không?


118

Tính năng tổng hợp đang có một số tương tác không mong muốn với việc xác định phạm vi. Đây có phải là hành vi dự kiến?

Tôi có một phương pháp:

def leave_room(self, uid):
  u = self.user_by_id(uid)
  r = self.rooms[u.rid]

  other_uids = [ouid for ouid in r.users_by_id.keys() if ouid != u.uid]
  other_us = [self.user_by_id(uid) for uid in other_uids]

  r.remove_user(uid) # OOPS! uid has been re-bound by the list comprehension above

  # Interestingly, it's rebound to the last uid in the list, so the error only shows
  # up when len > 1

Với nguy cơ than vãn, đây là một nguồn sai sót tàn bạo. Khi tôi viết mã mới, tôi chỉ thỉnh thoảng phát hiện thấy những lỗi rất kỳ lạ do việc đóng lại - ngay cả khi tôi biết đó là một vấn đề. Tôi cần phải thực hiện một quy tắc như "luôn mở đầu các vars tạm thời trong phần hiểu danh sách với dấu gạch dưới", nhưng ngay cả điều đó cũng không dễ bị đánh lừa.

Thực tế là có kiểu chờ bom hẹn giờ ngẫu nhiên này đã phủ nhận tất cả sự "dễ sử dụng" của việc hiểu danh sách.


7
-1: "nguồn lỗi tàn bạo"? Khó khăn. Tại sao lại chọn một thuật ngữ gây tranh cãi như vậy? Nói chung các lỗi đắt nhất là hiểu sai yêu cầu và lỗi logic đơn giản. Loại lỗi này đã là một vấn đề tiêu chuẩn trong nhiều ngôn ngữ lập trình. Tại sao gọi nó là 'tàn bạo'?
S.Lott

44
Nó vi phạm nguyên tắc ít bất ngờ nhất. Nó cũng không được đề cập trong tài liệu python về khả năng hiểu danh sách, tuy nhiên, nó đề cập nhiều lần đến mức độ dễ dàng và thuận tiện của chúng. Về cơ bản, đó là một mỏ đất tồn tại bên ngoài mô hình ngôn ngữ của tôi, và do đó tôi không thể đoán trước được.
Jabavu Adams

33
+1 cho "nguồn lỗi tàn bạo". Từ 'tàn bạo' là hoàn toàn hợp lý.
Nathaniel

3
Điều "tàn bạo" duy nhất mà tôi thấy ở đây là quy ước đặt tên của bạn. Đây không phải là những năm 80 nữa, bạn không bị giới hạn ở 3 tên biến ký tự.
UloPe

5
Lưu ý: các documention làm nhà nước rằng danh sách-hiểu tương đương với rõ ràng forcấu trúc -loop và for-loops biến rò rỉ . Vì vậy, nó không rõ ràng nhưng đã được tuyên bố ngầm.
Bakuriu

Câu trả lời:


172

Danh sách dễ hiểu rò rỉ biến điều khiển vòng lặp trong Python 2 nhưng không phải trong Python 3. Dưới đây là Guido van Rossum (người tạo ra Python) giải thích lịch sử đằng sau điều này:

Chúng tôi cũng đã thực hiện một thay đổi khác trong Python 3, để cải thiện sự tương đương giữa khả năng hiểu danh sách và biểu thức trình tạo. Trong Python 2, khả năng hiểu danh sách "rò rỉ" biến điều khiển vòng lặp vào phạm vi xung quanh:

x = 'before'
a = [x for x in 1, 2, 3]
print x # this prints '3', not 'before'

Đây là một tạo tác của việc triển khai ban đầu của việc hiểu danh sách; nó là một trong những "bí mật nhỏ bẩn thỉu" của Python trong nhiều năm. Nó bắt đầu như một sự thỏa hiệp có chủ ý để làm cho việc hiểu danh sách trở nên nhanh chóng đến chóng mặt, và mặc dù nó không phải là một cạm bẫy phổ biến cho người mới bắt đầu, nhưng nó chắc chắn đôi khi khiến mọi người đau đớn. Đối với biểu thức trình tạo, chúng tôi không thể làm điều này. Biểu thức trình tạo được thực hiện bằng cách sử dụng trình tạo, mà việc thực thi của nó yêu cầu một khung thực thi riêng biệt. Do đó, các biểu thức trình tạo (đặc biệt nếu chúng lặp lại trong một chuỗi ngắn) kém hiệu quả hơn so với các biểu thức danh sách.

Tuy nhiên, trong Python 3, chúng tôi đã quyết định khắc phục "bí mật nhỏ bẩn thỉu" của việc hiểu danh sách bằng cách sử dụng chiến lược triển khai tương tự như đối với các biểu thức trình tạo. Do đó, trong Python 3, ví dụ trên (sau khi sửa đổi để sử dụng print (x) :-) sẽ in 'before', chứng minh rằng 'x' trong danh sách hiểu tạm thời đổ bóng nhưng không ghi đè 'x' ở xung quanh phạm vi.


14
Tôi sẽ nói thêm rằng mặc dù Guido gọi nó là một "bí mật nhỏ bẩn thỉu", nhưng nhiều người coi đó là một tính năng chứ không phải lỗi.
Steven Rumbalski vào

38
Cũng xin lưu ý rằng bây giờ trong 2.7, bộ và từ điển hiểu (và trình tạo) có phạm vi riêng, nhưng khả năng hiểu danh sách vẫn không. Mặc dù điều này có ý nghĩa ở chỗ tất cả trước đây đều được chuyển sang lại từ Python 3, nhưng nó thực sự làm cho sự tương phản với các phần hiểu danh sách gây chói tai.
Matt B.

7
Tôi biết đây là một câu hỏi cực kỳ cũ, nhưng tại sao một số người lại coi nó là một tính năng của ngôn ngữ? Có điều gì có lợi cho loại rò rỉ biến này không?
Mathias Müller

2
for: rò rỉ vòng lặp có lý do chính đáng, đặc biệt. để truy cập giá trị cuối cùng sau sớm break- nhưng không liên quan đến việc bắt buộc. Tôi nhớ lại một số cuộc thảo luận comp.lang.python nơi mọi người muốn gán các biến ở giữa biểu thức. Cách ít điên rồ hơn được tìm thấy là một giá trị cho các mệnh đề, ví dụ. sum100 = [s for s in [0] for i in range(1, 101) for s in [s + i]][-1], nhưng chỉ cần một var địa phương dễ hiểu và hoạt động tốt trong Python 3. Tôi nghĩ "rò rỉ" là cách duy nhất để đặt biến hiển thị bên ngoài một biểu thức. Mọi người đều đồng ý những kỹ thuật này là khủng khiếp :-)
Beni Cherniavsky-Paskin

1
Vấn đề ở đây là không có quyền truy cập vào phạm vi xung quanh của toàn bộ danh sách, mà là ràng buộc trong phạm vi hiểu danh sách ảnh hưởng đến phạm vi xung quanh.
Felipe Gonçalves Marques,

48

Có, danh sách dễ hiểu "rò rỉ" biến của chúng trong Python 2.x, giống như vòng lặp for.

Nhìn lại, điều này được nhận ra là một sai lầm và nó đã được tránh với các biểu thức trình tạo. CHỈNH SỬA: Như Matt B. lưu ý, nó cũng bị tránh khi các cú pháp thiết lập và đọc hiểu từ điển được báo cáo ngược từ Python 3.

Hành vi của danh sách hiểu phải được giữ nguyên như trong Python 2, nhưng nó hoàn toàn được sửa trong Python 3.

Điều này có nghĩa là trong tất cả:

list(x for x in a if x>32)
set(x//4 for x in a if x>32)         # just another generator exp.
dict((x, x//16) for x in a if x>32)  # yet another generator exp.
{x//4 for x in a if x>32}            # 2.7+ syntax
{x: x//16 for x in a if x>32}        # 2.7+ syntax

các xluôn là địa phương để các biểu hiện trong khi những:

[x for x in a if x>32]
set([x//4 for x in a if x>32])         # just another list comp.
dict([(x, x//16) for x in a if x>32])  # yet another list comp.

trong Python 2.x tất cả đều rò rỉ xbiến ra phạm vi xung quanh.


CẬP NHẬT cho Python 3.8 (?) : PEP 572 sẽ giới thiệu :=toán tử gán cố tình làm rò rỉ các biểu thức và biểu thức trình tạo. Về cơ bản, nó được thúc đẩy bởi 2 trường hợp sử dụng: bắt một "nhân chứng" từ các chức năng kết thúc sớm như any()all():

if any((comment := line).startswith('#') for line in lines):
    print("First comment:", comment)
else:
    print("There are no comments")

và cập nhật trạng thái có thể thay đổi:

total = 0
partial_sums = [total := total + v for v in values]

Xem Phụ lục B để biết phạm vi chính xác. Biến được gán ở xung quanh gần nhất defhoặc lambda, trừ khi hàm đó khai báo nó nonlocalhoặc global.


7

Có, sự phân công xảy ra ở đó, giống như nó sẽ xảy ra trong một forvòng lặp. Không có phạm vi mới nào đang được tạo.

Đây chắc chắn là hành vi mong đợi: trên mỗi chu kỳ, giá trị được liên kết với tên bạn chỉ định. Ví dụ,

>>> x=0
>>> a=[1,54,4,2,32,234,5234,]
>>> [x for x in a if x>32]
[54, 234, 5234]
>>> x
5234

Một khi điều đó được công nhận, có vẻ dễ dàng để tránh: không sử dụng các tên hiện có cho các biến trong phạm vi hiểu.


2

Điều thú vị là điều này không ảnh hưởng đến từ điển hoặc cách hiểu.

>>> [x for x in range(1, 10)]
[1, 2, 3, 4, 5, 6, 7, 8, 9]
>>> x
9
>>> {x for x in range(1, 5)}
set([1, 2, 3, 4])
>>> x
9
>>> {x:x for x in range(1, 100)}
{1: 1, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9, 10: 10, 11: 11, 12: 12, 13: 13, 14: 14, 15: 15, 16: 16, 17: 17, 18: 18, 19: 19, 20: 20, 21: 21, 22: 22, 23: 23, 24: 24, 25: 25, 26: 26, 27: 27, 28: 28, 29: 29, 30: 30, 31: 31, 32: 32, 33: 33, 34: 34, 35: 35, 36: 36, 37: 37, 38: 38, 39: 39, 40: 40, 41: 41, 42: 42, 43: 43, 44: 44, 45: 45, 46: 46, 47: 47, 48: 48, 49: 49, 50: 50, 51: 51, 52: 52, 53: 53, 54: 54, 55: 55, 56: 56, 57: 57, 58: 58, 59: 59, 60: 60, 61: 61, 62: 62, 63: 63, 64: 64, 65: 65, 66: 66, 67: 67, 68: 68, 69: 69, 70: 70, 71: 71, 72: 72, 73: 73, 74: 74, 75: 75, 76: 76, 77: 77, 78: 78, 79: 79, 80: 80, 81: 81, 82: 82, 83: 83, 84: 84, 85: 85, 86: 86, 87: 87, 88: 88, 89: 89, 90: 90, 91: 91, 92: 92, 93: 93, 94: 94, 95: 95, 96: 96, 97: 97, 98: 98, 99: 99}
>>> x
9

Tuy nhiên nó đã được sửa trong 3 như đã nói ở trên.


Cú pháp đó hoàn toàn không hoạt động trong Python 2.6. Bạn đang nói về Python 2.7?
Paul Hollingsworth

Python 2.6 chỉ có khả năng hiểu danh sách cũng như Python 3.0. 3.1 đã thêm tập hợp và hiểu từ điển và chúng đã được chuyển sang 2.7. Xin lỗi nếu điều đó không rõ ràng. Nó có nghĩa là để lưu ý một giới hạn cho một câu trả lời khác và phiên bản nó áp dụng cho không hoàn toàn đơn giản.
Chris Travers,

Mặc dù tôi có thể hình dung ra lập luận rằng có những trường hợp sử dụng python 2.7 cho mã mới có ý nghĩa, nhưng tôi không thể nói điều tương tự đối với python 2.6 ... Ngay cả khi 2.6 là thứ đi kèm với hệ điều hành của bạn, bạn vẫn không gặp khó khăn với nó. Cân nhắc cài đặt virtualenv và sử dụng 3.6 cho mã mới!
Alex L

Tuy nhiên, quan điểm về Python 2.6 có thể được đưa ra trong việc duy trì các hệ thống kế thừa hiện có. Vì vậy, như một ghi chép lịch sử, nó không hoàn toàn không liên quan. Tương tự với 3.0 (ick)
Chris Travers

Xin lỗi nếu tôi nghe có vẻ thô lỗ, nhưng điều này không trả lời câu hỏi theo bất kỳ cách nào. Nó phù hợp hơn như một bình luận.
0xc0de

1

một số cách giải quyết, đối với python 2.6, khi hành vi này không mong muốn

# python
Python 2.6.6 (r266:84292, Aug  9 2016, 06:11:56)
Type "help", "copyright", "credits" or "license" for more information.
>>> x=0
>>> a=list(x for x in xrange(9))
>>> x
0
>>> a=[x for x in xrange(9)]
>>> x
8

-1

Trong python3 khi ở trong danh sách hiểu, biến sẽ không thay đổi sau khi hết phạm vi nhưng khi chúng ta sử dụng vòng lặp for đơn giản, biến sẽ bị gán lại ngoài phạm vi.

i = 1 print (i) print ([i in range (5)]) print (i) Giá trị của i sẽ chỉ còn 1.

Bây giờ chỉ cần sử dụng đơn giản vòng lặp for, giá trị của i sẽ được gán lại.

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.