Là ngoại lệ cho kiểm soát luồng thực hành tốt nhất trong Python?


15

Tôi đang đọc "Học Python" và đã gặp những điều sau:

Các ngoại lệ do người dùng xác định cũng có thể báo hiệu các điều kiện không giới hạn. Chẳng hạn, một thói quen tìm kiếm có thể được mã hóa để đưa ra một ngoại lệ khi tìm thấy kết quả khớp thay vì trả về một cờ trạng thái để người gọi giải thích. Trong phần sau đây, trình xử lý ngoại lệ try / trừ / khác thực hiện công việc của trình kiểm tra giá trị trả về if / other:

class Found(Exception): pass

def searcher():
    if ...success...:
        raise Found()            # Raise exceptions instead of returning flags
    else:
        return

Bởi vì Python được gõ động và đa hình vào lõi, các trường hợp ngoại lệ, thay vì các giá trị trả về sentinel, là cách thường được ưa thích để báo hiệu các điều kiện như vậy.

Tôi đã thấy loại điều này được thảo luận nhiều lần trên các diễn đàn khác nhau và tham chiếu đến Python bằng StopIteration để kết thúc các vòng lặp, nhưng tôi không thể tìm thấy nhiều trong các hướng dẫn kiểu chính thức (PEP 8 có một tham chiếu trực tiếp đến các ngoại lệ để kiểm soát luồng) hoặc tuyên bố từ các nhà phát triển. Có bất cứ điều gì chính thức nói rằng đây là cách thực hành tốt nhất cho Python?

Điều này ( Các trường hợp ngoại lệ như luồng điều khiển được coi là một phản hạt nghiêm trọng? Nếu vậy, Tại sao? ) Cũng có một số ý kiến ​​cho rằng phong cách này là Pythonic. Điều này dựa trên cái gì?

TIA

Câu trả lời:


24

Đồng thuận chung, không sử dụng ngoại lệ! Phần lớn đến từ các ngôn ngữ khác và thậm chí đôi khi còn lỗi thời.

  • Trong C ++, việc ném một ngoại lệ rất tốn kém do stack stack Uninding của. Mỗi khai báo biến cục bộ giống như một withcâu lệnh trong Python và đối tượng trong biến đó có thể chạy các hàm hủy. Các hàm hủy này được thực thi khi ném ngoại lệ, nhưng cũng có khi trở về từ hàm. Thành ngữ RAII này là một tính năng ngôn ngữ không thể thiếu và cực kỳ quan trọng để viết mã chính xác, mạnh mẽ - vì vậy RAII so với các ngoại lệ rẻ tiền là một sự đánh đổi mà C ++ đã quyết định đối với RAII.

  • Vào đầu C ++, rất nhiều mã không được viết theo cách an toàn ngoại lệ: trừ khi bạn thực sự sử dụng RAII, rất dễ bị rò rỉ bộ nhớ và các tài nguyên khác. Vì vậy, ném ngoại lệ sẽ khiến mã đó không chính xác. Điều này không còn hợp lý vì ngay cả thư viện chuẩn C ++ cũng sử dụng ngoại lệ: bạn không thể giả vờ ngoại lệ không tồn tại. Tuy nhiên, ngoại lệ vẫn là một vấn đề khi kết hợp mã C với C ++.

  • Trong Java, mọi ngoại lệ đều có dấu vết ngăn xếp liên quan. Theo dõi ngăn xếp rất có giá trị khi gỡ lỗi, nhưng lãng phí công sức khi ngoại lệ không bao giờ được in, ví dụ vì nó chỉ được sử dụng cho luồng điều khiển.

Vì vậy, trong các ngôn ngữ đó, ngoại lệ là quá đắt để sử dụng làm dòng điều khiển. Trong Python đây không phải là một vấn đề và ngoại lệ rẻ hơn rất nhiều. Ngoài ra, ngôn ngữ Python đã phải chịu một số chi phí khiến cho chi phí ngoại lệ không đáng chú ý so với các cấu trúc luồng điều khiển khác: ví dụ: kiểm tra xem một mục nhập có tồn tại với kiểm tra thành viên rõ ràng if key in the_dict: ...thường nhanh như truy cập vào mục the_dict[key]; ...và kiểm tra nếu bạn lấy KeyError. Một số tính năng ngôn ngữ tách rời (ví dụ: máy phát điện) được thiết kế theo các trường hợp ngoại lệ.

Vì vậy, trong khi không có lý do kỹ thuật để đặc biệt tránh các ngoại lệ trong Python, vẫn còn câu hỏi liệu bạn có nên sử dụng chúng thay vì trả về giá trị hay không. Các vấn đề ở cấp thiết kế với các ngoại lệ là:

  • chúng không rõ ràng Bạn không thể dễ dàng nhìn vào một chức năng và xem nó có thể ngoại lệ, vì vậy bạn không biết phải bắt cái gì. Giá trị trả về có xu hướng được xác định rõ hơn.

  • ngoại lệ là luồng kiểm soát không cục bộ làm phức tạp mã của bạn. Khi bạn ném một ngoại lệ, bạn không biết luồng điều khiển sẽ tiếp tục ở đâu. Đối với các lỗi không thể xử lý ngay thì đây có lẽ là một ý kiến ​​hay, khi thông báo cho người gọi của bạn về một điều kiện thì điều này là hoàn toàn không cần thiết.

Văn hóa Python nói chung nghiêng về các trường hợp ngoại lệ, nhưng thật dễ dàng để vượt qua. Hãy tưởng tượng một list_contains(the_list, item)hàm kiểm tra xem danh sách có chứa một mục bằng với mục đó không. Nếu kết quả được truyền đạt thông qua các ngoại lệ hoàn toàn gây khó chịu, bởi vì chúng ta phải gọi nó như thế này:

try:
  list_contains(invited_guests, person_at_door)
except Found:
  print("Oh, hello {}!".format(person_at_door))
except NotFound:
  print("Who are you?")

Trả lại một bool sẽ rõ ràng hơn nhiều:

if list_contains(invited_guests, person_at_door):
  print("Oh, hello {}!".format(person_at_door))
else:
  print("Who are you?")

Nếu hàm được cho là trả về một giá trị, thì việc trả về một giá trị đặc biệt cho các điều kiện đặc biệt khá dễ bị lỗi, bởi vì mọi người sẽ quên kiểm tra giá trị này (đó có thể là nguyên nhân của 1/3 vấn đề trong C). Một ngoại lệ thường đúng hơn.

Một ví dụ điển hình là một pos = find_string(haystack, needle)hàm tìm kiếm sự xuất hiện đầu tiên của needlechuỗi trong chuỗi `haystack và trả về vị trí bắt đầu. Nhưng nếu chúng haystack-string không chứa chuỗi kim thì sao?

Giải pháp của C và bắt chước bởi Python là trả về một giá trị đặc biệt. Trong C đây là một con trỏ null, trong Python đây là -1. Điều này sẽ dẫn đến kết quả đáng ngạc nhiên khi vị trí được sử dụng làm chỉ mục chuỗi mà không cần kiểm tra, đặc biệt -1là chỉ mục hợp lệ trong Python. Trong C, con trỏ NULL của bạn ít nhất sẽ cung cấp cho bạn một segfault.

Trong PHP, một giá trị đặc biệt của một loại khác được trả về: boolean FALSEthay vì một số nguyên. Vì hóa ra điều này thực sự không tốt hơn do các quy tắc chuyển đổi ngầm định của ngôn ngữ (nhưng lưu ý rằng trong Python cũng như các booleans có thể được sử dụng như ints!). Các hàm không trả về một kiểu nhất quán thường được coi là rất khó hiểu.

Một biến thể mạnh hơn sẽ có một ngoại lệ khi không thể tìm thấy chuỗi, điều này đảm bảo rằng trong luồng điều khiển thông thường, không thể vô tình sử dụng giá trị đặc biệt thay cho giá trị thông thường:

 try:
   pos = find_string(haystack, needle)
   do_something_with(pos)
 except NotFound:
   ...

Ngoài ra, luôn luôn trả về một loại không thể được sử dụng trực tiếp nhưng trước tiên phải được mở khóa có thể được sử dụng, ví dụ: bộ dữ liệu kết quả trong đó boolean cho biết liệu có ngoại lệ xảy ra hay không nếu kết quả có thể sử dụng được. Sau đó:

pos, ok = find_string(haystack, needle)
if not ok:
  ...
do_something_with(pos)

Điều này buộc bạn phải xử lý vấn đề ngay lập tức, nhưng nó gây khó chịu rất nhanh. Nó cũng ngăn bạn khỏi chức năng xích dễ dàng. Mỗi cuộc gọi chức năng bây giờ cần ba dòng mã. Golang là một ngôn ngữ cho rằng phiền toái này là giá trị an toàn.

Vì vậy, để tóm tắt, các trường hợp ngoại lệ không hoàn toàn không có vấn đề và chắc chắn có thể bị lạm dụng quá mức, đặc biệt là khi chúng thay thế một giá trị trả về bình thường. Nhưng khi được sử dụng để báo hiệu các điều kiện đặc biệt (không nhất thiết chỉ là lỗi), thì các ngoại lệ có thể giúp bạn phát triển các API sạch, trực quan, dễ sử dụng và khó sử dụng.


1
Cảm ơn câu trả lời sâu sắc. Tôi đến từ một nền tảng của các ngôn ngữ khác như Java, nơi "ngoại lệ chỉ dành cho các điều kiện đặc biệt", vì vậy đây là điều mới đối với tôi. Có nhà phát triển lõi Python nào đã từng nói điều này theo cách họ có với các hướng dẫn Python khác như EAFP, EIBTI, v.v. (trên danh sách gửi thư, bài đăng trên blog, v.v.) Tôi muốn có thể trích dẫn một cái gì đó chính thức nếu một dev / sếp hỏi. Cảm ơn!
JB

Bằng cách nạp chồng phương thức fillInStackTrace của lớp ngoại lệ tùy chỉnh của bạn để trả về ngay lập tức, bạn có thể làm cho nó rất rẻ để nâng cao, vì nó là stack-walk đắt tiền và đây là nơi nó được thực hiện.
John Cowan

Viên đạn đầu tiên của bạn trong phần giới thiệu của bạn lúc đầu thật khó hiểu. Có lẽ bạn có thể thay đổi nó thành một cái gì đó như "Mã xử lý ngoại lệ trong C ++ hiện đại là chi phí bằng không, nhưng ném một ngoại lệ là tốn kém"? (dành cho lurker: stackoverflow.com/questions/13835817/ trên ) [chỉnh sửa: chỉnh sửa đề xuất]
phresnel

Lưu ý rằng đối với các hoạt động tra cứu từ điển bằng cách sử dụng một collections.defaultdicthoặc my_dict.get(key, default)làm cho mã rõ ràng hơn nhiềutry: my_dict[key] except: return default
Steve Barnes

11

KHÔNG! - không nói chung - các trường hợp ngoại lệ không được coi là thực hành kiểm soát luồng tốt ngoại trừ lớp mã duy nhất. Một nơi mà các trường hợp ngoại lệ được coi là một cách hợp lý, hoặc thậm chí tốt hơn, để báo hiệu một điều kiện là các hoạt động của trình tạo hoặc trình lặp. Các hoạt động này có thể trả về bất kỳ giá trị có thể nào là kết quả hợp lệ do đó cần có cơ chế để báo hiệu kết thúc.

Cân nhắc đọc một tệp nhị phân của luồng một byte mỗi lần - hoàn toàn bất kỳ giá trị nào là kết quả hợp lệ tiềm năng nhưng chúng ta vẫn cần phải báo hiệu kết thúc tệp. Vì vậy, chúng tôi có một lựa chọn, trả về hai giá trị, (giá trị byte & cờ hợp lệ), mỗi lần hoặc đưa ra một ngoại lệ khi không còn việc gì để làm. Trong hai trường hợp, mã tiêu thụ có thể trông như sau:

# Using validity flag
valid, val = readbyte(source)
while valid:
    processbyte(val)
    valid, val = readbyte(source)
tidy_up()

cách khác:

# With exceptions
try:
   val = readbyte(source)
   processbyte(val) # Note if a problem occurs here it will also raise an exception
except Exception: # Use a specific exception here!
   tidy_up()

Nhưng điều này có, kể từ khi PEP 343 được triển khai và chuyển trở lại, tất cả đã được gói gọn trong withtuyên bố. Ở trên trở thành, rất pythonic:

with open(source) as input:
    for val in input.readbyte(): # This line will raise a StopIteration exception an call input.__exit__()
        processbyte(val) # Not called if there is nothing read

Trong python3, điều này đã trở thành:

for val in open(source, 'rb').read():
     processbyte(val)

Tôi đặc biệt khuyến khích bạn đọc PEP 343 , đưa ra nền tảng, lý do, ví dụ, v.v.

Người ta cũng thường sử dụng một ngoại lệ để báo hiệu kết thúc xử lý khi sử dụng các chức năng của trình tạo để báo hiệu kết thúc.

Tôi muốn nói thêm rằng ví dụ về người tìm kiếm của bạn gần như chắc chắn là ngược, các chức năng đó phải là máy phát điện, trả lại trận đấu đầu tiên trong cuộc gọi đầu tiên, sau đó các cuộc gọi thay thế trả về trận đấu tiếp theo và đưa ra một NotFoundngoại lệ khi không còn trận đấu nữa .


Bạn nói: "KHÔNG! - không nói chung - các trường hợp ngoại lệ không được coi là thực hành kiểm soát luồng tốt ngoại trừ lớp mã duy nhất". Đâu là lý do cho tuyên bố nhấn mạnh này? Câu trả lời này dường như tập trung hẹp vào trường hợp lặp. Có nhiều trường hợp khác mà mọi người có thể nghĩ để sử dụng ngoại lệ. Vấn đề với việc làm như vậy trong những trường hợp khác là gì?
Daniel Waltrip

@DanielWaltrip Tôi thấy quá nhiều mã, trong các ngôn ngữ lập trình khác nhau, nơi các ngoại lệ được sử dụng, thường là quá rộng rãi, khi một đơn giản ifsẽ tốt hơn. Khi nói đến việc gỡ lỗi & kiểm tra hoặc thậm chí suy luận về hành vi mã của bạn, tốt hơn là không nên dựa vào các ngoại lệ - chúng nên là ngoại lệ. Tôi đã thấy, trong mã sản xuất, một phép tính được trả về qua một lần ném / bắt chứ không phải là trả lại đơn giản, điều này có nghĩa là khi phép tính đạt một phép chia cho số 0, nó trả về một giá trị ngẫu nhiên thay vì bị lỗi trong đó xảy ra vài giờ gỡ lỗi.
Steve Barnes

Xin chào @SteveBarnes, ví dụ chia cho số không bắt lỗi nghe có vẻ như lập trình kém (với cách bắt quá chung chung không làm tăng ngoại lệ).
wTyeRogers

Câu trả lời của bạn dường như sôi lên: A) "KHÔNG!" B) có các ví dụ hợp lệ về nơi luồng điều khiển thông qua các ngoại lệ hoạt động tốt hơn các lựa chọn thay thế C) điều chỉnh mã câu hỏi để sử dụng các trình tạo (sử dụng các ngoại lệ làm luồng điều khiển và làm rất tốt). Mặc dù câu trả lời của bạn nói chung là nhiều thông tin, nhưng không có đối số nào tồn tại trong nó để hỗ trợ mục A, trong đó, được in đậm và dường như là tuyên bố chính, tôi sẽ mong đợi một số hỗ trợ. Nếu nó được hỗ trợ, tôi sẽ không đánh giá thấp câu trả lời của bạn. Than ôi ...
wTyeRogers
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.