Khi nào và làm thế nào tôi nên sử dụng ngoại lệ?


20

Cài đặt

Tôi thường gặp khó khăn trong việc xác định thời điểm và cách sử dụng ngoại lệ. Hãy xem xét một ví dụ đơn giản: giả sử tôi đang quét một trang web, nói " http://www.abevigoda.com/ ", để xác định xem Abe Vigoda có còn sống không. Để làm điều này, tất cả những gì chúng ta cần làm là tải xuống trang và tìm kiếm lần xuất hiện cụm từ "Abe Vigoda". Chúng tôi trả lại sự xuất hiện đầu tiên, vì điều đó bao gồm tình trạng của Abe. Về mặt khái niệm, nó sẽ trông như thế này:

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

Trường hợp parse_abe_status(s)lấy một chuỗi có dạng "Abe Vigoda là một cái gì đó " và trả về phần " cái gì đó ".

Trước khi bạn lập luận rằng có nhiều cách tốt hơn và mạnh mẽ hơn để quét trang này cho trạng thái của Abe, hãy nhớ rằng đây chỉ là một ví dụ đơn giản và dễ hiểu được sử dụng để làm nổi bật một tình huống phổ biến mà tôi gặp phải.

Bây giờ, mã này có thể gặp vấn đề ở đâu? Trong số các lỗi khác, một số lỗi "dự kiến" là:

  • download_pagecó thể không tải được trang và ném IOError.
  • URL có thể không trỏ đến trang bên phải hoặc trang được tải xuống không chính xác và do đó không có lần truy cập nào. hitslà danh sách trống, sau đó.
  • Trang web đã bị thay đổi, có thể làm cho các giả định của chúng tôi về trang bị sai. Có thể chúng tôi mong đợi 4 đề cập đến Abe Vigoda, nhưng bây giờ chúng tôi tìm thấy 5.
  • Vì một số lý do, hits[0]có thể không phải là một chuỗi có dạng "Abe Vigoda là một cái gì đó ", và vì vậy nó không thể được phân tích cú pháp chính xác.

Trường hợp đầu tiên thực sự không phải là vấn đề đối với tôi: một cú IOErrorném và có thể được xử lý bởi người gọi chức năng của tôi. Vì vậy, hãy xem xét các trường hợp khác và làm thế nào tôi có thể xử lý chúng. Nhưng trước tiên, hãy giả sử rằng chúng ta thực hiện parse_abe_statustheo cách ngu ngốc nhất có thể:

def parse_abe_status(s):
    return s[13:]

Cụ thể, nó không thực hiện bất kỳ kiểm tra lỗi. Bây giờ, vào các tùy chọn:

Cách 1: Trả lại None

Tôi có thể nói với người gọi rằng có gì đó không ổn bằng cách quay lại None:

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    if not hits:
        return None

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

Nếu người gọi nhận được Nonetừ chức năng của tôi, ông nên cho rằng không có đề cập đến Abe Vigoda, và do đó một cái gì đó đã đi sai. Nhưng điều này khá mơ hồ, phải không? Và nó không giúp ích gì cho trường hợp hits[0]không như chúng ta nghĩ.

Mặt khác, chúng ta có thể đưa ra một số ngoại lệ:

Tùy chọn 2: Sử dụng ngoại lệ

Nếu hitstrống, một IndexErrorsẽ được ném khi chúng ta cố gắng hits[0]. Nhưng người gọi không nên được yêu cầu xử lý một IndexErrorchức năng của tôi, vì anh ta không biết nó IndexErrorđến từ đâu; nó có thể bị ném bởi find_all_mentions, vì tất cả những gì anh biết. Vì vậy, chúng tôi sẽ tạo một lớp ngoại lệ tùy chỉnh để xử lý việc này:

class NotFoundError(Exception):
    """Throw this when something can't be found on a page."""

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    try:
        hits[0]
    except IndexError:
        raise NotFoundError("No mentions found.")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

Bây giờ nếu trang đã thay đổi và có số lần truy cập không mong muốn thì sao? Đây không phải là thảm họa, vì mã vẫn có thể làm việc, nhưng một người gọi có thể muốn có thêm cẩn thận, hoặc ông có thể muốn ghi lại một cảnh báo. Vì vậy, tôi sẽ đưa ra một cảnh báo:

class NotFoundError(Exception):
    """Throw this when something can't be found on a page."""

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    try:
        hits[0]
    except IndexError:
        raise NotFoundError("No mentions found.")

    # say we expect four hits...
    if len(hits) != 4:
        raise Warning("An unexpected number of hits.")
        logger.warning("An unexpected number of hits.")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    # he's either alive or dead
    return status == "alive"

Cuối cùng, chúng ta có thể thấy rằng statuskhông còn sống hay đã chết. Có thể, vì một số lý do kỳ lạ, hôm nay hóa ra là như vậy comatose. Sau đó tôi không muốn quay lại False, vì điều đó ngụ ý rằng Abe đã chết. Tôi nên làm gì ở đây? Ném một ngoại lệ, có lẽ. Nhưng loại nào? Tôi có nên tạo một lớp ngoại lệ tùy chỉnh?

class NotFoundError(Exception):
    """Throw this when something can't be found on a page."""

def get_abe_status(url):
    # download the page
    page = download_page(url)

    # get all mentions of Abe Vigoda
    hits = page.find_all_mentions("Abe Vigoda")

    try:
        hits[0]
    except IndexError:
        raise NotFoundError("No mentions found.")

    # say we expect four hits...
    if len(hits) != 4:
        raise Warning("An unexpected number of hits.")
        logger.warning("An unexpected number of hits.")

    # parse the first hit for his status
    status = parse_abe_status(hits[0])

    if status not in ['alive', 'dead']:
        raise SomeTypeOfError("Status is an unexpected value.")

    # he's either alive or dead
    return status == "alive"

Lựa chọn 3: Một nơi nào đó ở giữa

Tôi nghĩ rằng phương pháp thứ hai, với các ngoại lệ, là tốt hơn, nhưng tôi không chắc liệu tôi có sử dụng ngoại lệ một cách chính xác trong đó không. Tôi tò mò muốn xem các lập trình viên giàu kinh nghiệm hơn sẽ xử lý việc này như thế nào.

Câu trả lời:


17

Khuyến cáo trong Python là sử dụng các ngoại lệ để chỉ ra thất bại. Điều này đúng ngay cả khi bạn mong đợi thất bại một cách thường xuyên.

Nhìn vào nó từ quan điểm của người gọi mã của bạn:

my_status = get_abe_status(my_url)

Nếu chúng ta trở về Không thì sao? Nếu người gọi không xử lý cụ thể trường hợp get_abe_status không thành công, đơn giản là họ sẽ cố gắng tiếp tục với my_stats là Không có. Điều đó có thể tạo ra một lỗi khó chẩn đoán sau này. Ngay cả khi bạn kiểm tra Không có, mã này không có lý do tại sao get_abe_status () không thành công.

Nhưng nếu chúng ta nêu ra một ngoại lệ thì sao? Nếu người gọi không xử lý cụ thể trường hợp đó, ngoại lệ sẽ lan truyền lên cuối cùng đánh vào trình xử lý ngoại lệ mặc định. Đó có thể không phải là những gì bạn muốn, nhưng tốt hơn là giới thiệu một lỗi tinh vi ở nơi khác trong chương trình. Ngoài ra, ngoại lệ cung cấp thông tin về những gì đã sai trong phiên bản đầu tiên.

Từ quan điểm của người gọi, đơn giản là thuận tiện hơn để có được một ngoại lệ hơn là giá trị trả về. Và đó là kiểu trăn, để sử dụng các ngoại lệ để chỉ ra các điều kiện thất bại không trả về giá trị.

Một số người sẽ có quan điểm khác nhau và lập luận rằng bạn chỉ nên sử dụng ngoại lệ cho các trường hợp bạn không bao giờ thực sự mong đợi xảy ra. Họ cho rằng việc chạy bình thường không nên đưa ra bất kỳ ngoại lệ nào. Một lý do được đưa ra cho điều này là các ngoại lệ rất kém hiệu quả, nhưng điều đó không thực sự đúng với Python.

Một vài điểm trong mã của bạn:

try:
    hits[0]
except IndexError:
    raise NotFoundError("No mentions found.")

Đó là một cách thực sự khó hiểu để kiểm tra danh sách trống. Đừng tạo ra một ngoại lệ chỉ để kiểm tra một cái gì đó. Sử dụng nếu.

# say we expect four hits...
if len(hits) != 4:
    raise Warning("An unexpected number of hits.")
    logger.warning("An unexpected number of hits.")

Bạn có nhận ra rằng dòng logger.warning sẽ không bao giờ chạy phải không?


1
Cảm ơn (muộn màng) cho phản ứng của bạn. Nó, cùng với việc nhìn vào mã được xuất bản, đã cải thiện cảm giác của tôi về thời điểm và cách ném ngoại lệ.
jme

4

Câu trả lời được chấp nhận xứng đáng được chấp nhận và trả lời câu hỏi, tôi chỉ viết điều này để cung cấp thêm một chút nền tảng.

Một trong những điều đáng tin cậy của Python là: dễ dàng yêu cầu sự tha thứ hơn là sự cho phép. Điều này có nghĩa là thông thường bạn chỉ cần làm mọi thứ và nếu bạn mong đợi ngoại lệ, bạn xử lý chúng. Trái ngược với làm nếu kiểm tra trước khi ra tay để đảm bảo bạn sẽ không gặp ngoại lệ.

Tôi muốn cung cấp một ví dụ để cho bạn thấy sự khác biệt đáng kinh ngạc về tâm lý từ C ++ / Java. Một vòng lặp for trong C ++ thường trông giống như:

for(int i = 0; i != myvector.size(); ++i) ...

Một cách để suy nghĩ về điều này: truy cập vào myvector[k]nơi k> = myvector.size () sẽ gây ra ngoại lệ. Vì vậy, về nguyên tắc bạn có thể viết điều này (rất vụng về) như là một thử bắt.

    for(int i = 0; ; ++i)  {
        try {
           ...
        } catch (& std::out_of_range)
             break

Hoặc một cái gì đó tương tự. Bây giờ, hãy xem xét những gì xảy ra trong một vòng lặp python:

for i in range(1):
    ...

Làm thế nào là làm việc này? Vòng lặp for lấy kết quả của phạm vi (1) và gọi iter () trên nó, lấy một trình vòng lặp đến nó.

b = range(1).__iter__()

Sau đó, nó gọi tiếp theo trên mỗi lần lặp, cho đến khi ...:

>>> next(b)
0
>>> next(b)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Nói cách khác, một vòng lặp for trong python thực sự là một thử - ngoại trừ ngụy trang.

Theo như câu hỏi cụ thể, hãy nhớ rằng các ngoại lệ dừng thực thi chức năng bình thường và phải được xử lý riêng. Trong Python, bạn nên tự do ném chúng bất cứ khi nào không có điểm nào thực thi phần còn lại của mã trong hàm của bạn và / hoặc không có trả về nào phản ánh chính xác những gì đã xảy ra trong hàm. Lưu ý rằng việc trả về sớm từ một hàm là khác nhau: trả về sớm có nghĩa là bạn đã tìm ra câu trả lời và không cần phần còn lại của mã để tìm ra câu trả lời. Tôi đang nói rằng các ngoại lệ nên được ném khi không biết câu trả lời và phần còn lại của mã để xác định câu trả lời không thể chạy một cách hợp lý. Bây giờ, "phản ánh chính xác" chính nó, giống như trường hợp ngoại lệ bạn chọn để ném, tất cả chỉ là vấn đề tài liệu.

Trong trường hợp mã cụ thể của bạn, tôi sẽ nói bất kỳ tình huống nào khiến các lần truy cập là một danh sách trống nên ném. Tại sao? Vâng, cách chức năng của bạn được thiết lập, không có cách nào để xác định câu trả lời mà không phân tích cú pháp. Vì vậy, nếu các lần truy cập không thể phân tích cú pháp, vì URL xấu hoặc do các lần truy cập trống, thì hàm không thể trả lời câu hỏi và thực tế thậm chí không thể thực sự cố gắng.

Trong trường hợp cụ thể này, tôi sẽ lập luận rằng ngay cả khi bạn xoay sở để phân tích và không nhận được câu trả lời hợp lý (còn sống hay đã chết), thì bạn vẫn nên ném. Tại sao? Bởi vì, hàm trả về một boolean. Trả lại Không có gì là rất nguy hiểm cho khách hàng của bạn. Nếu họ thực hiện nếu kiểm tra trên Không, sẽ không có thất bại, nó sẽ chỉ âm thầm được coi là Sai. Vì vậy, về cơ bản, khách hàng của bạn sẽ luôn phải thực hiện nếu không có kiểm tra dù thế nào đi nữa nếu anh ta không muốn thất bại thầm lặng ... vì vậy bạn có lẽ chỉ nên ném.


2

Bạn nên sử dụng ngoại lệ khi một cái gì đó đặc biệt xảy ra. Đó là, một cái gì đó không nên xảy ra khi sử dụng ứng dụng đúng cách. Nếu người tiêu dùng phương pháp của bạn cho phép tìm kiếm thứ gì đó sẽ không được tìm thấy, thì "không tìm thấy" không phải là trường hợp ngoại lệ. Trong trường hợp này, bạn nên trả về null hoặc "Không" hoặc {}, hoặc một cái gì đó chỉ ra một bộ trả về trống.

Mặt khác, nếu bạn thực sự mong đợi người tiêu dùng phương pháp của bạn luôn luôn (trừ khi họ làm hỏng bằng cách nào đó) tìm thấy những gì đang được tìm kiếm, thì không tìm thấy nó sẽ là một ngoại lệ và bạn nên đi theo điều đó.

Điều quan trọng là việc xử lý ngoại lệ có thể tốn kém - ngoại lệ được cho là thu thập thông tin về trạng thái của ứng dụng của bạn khi chúng xảy ra, chẳng hạn như dấu vết ngăn xếp, để giúp mọi người giải mã lý do tại sao chúng xảy ra. Tôi không nghĩ đó là những gì bạn đang cố gắng làm.


1
Nếu bạn quyết định rằng không tìm thấy giá trị là cho phép, hãy cẩn thận về những gì bạn sử dụng để chỉ ra đó là những gì đã xảy ra. Nếu phương thức của bạn được yêu cầu trả về a Stringvà bạn chọn "Không" làm chỉ báo của mình, điều này có nghĩa là bạn phải cẩn thận rằng "Không" sẽ không bao giờ là giá trị hợp lệ. Cũng lưu ý rằng có một sự khác biệt giữa việc xem dữ liệu và không tìm thấy giá trị và không thể truy xuất dữ liệu, do đó chúng tôi không thể tìm thấy dữ liệu. Có cùng kết quả cho hai trường hợp này có nghĩa là bạn không có khả năng hiển thị một khi bạn không nhận được giá trị khi bạn mong muốn có một trường hợp.
unolysampler

Các khối mã nội tuyến được đánh dấu bằng backticks (`), có lẽ đó là những gì bạn muốn làm với" Không "?
Izkata

3
Tôi sợ điều này hoàn toàn sai trong Python. Bạn đang áp dụng lý luận kiểu C ++ / Java cho ngôn ngữ khác. Python sử dụng các ngoại lệ để chỉ ra kết thúc của vòng lặp for; đó là khá ngoại lệ.
Nir Friedman

2

Nếu tôi đang viết một chức năng

 def abe_is_alive():

Tôi sẽ viết nó cho return Truehoặc Falsetrong các trường hợp tôi hoàn toàn chắc chắn về cái này hay cái khác, và raisemột lỗi trong bất kỳ trường hợp nào khác (ví dụ raise ValueError("Status neither 'dead' nor 'alive'")). Điều này là do hàm gọi hàm của tôi đang mong đợi một boolean và nếu tôi không thể cung cấp điều đó một cách chắc chắn thì dòng chương trình thông thường không nên tiếp tục.

Một cái gì đó giống như ví dụ của bạn về việc nhận được một số "lần truy cập" khác so với dự kiến, tôi có thể sẽ bỏ qua; miễn là một trong những bản hit vẫn phù hợp với mẫu của tôi "Abe Vigoda là {chết | còn sống}", điều đó tốt. Điều này cho phép trang được sắp xếp lại nhưng vẫn có được thông tin phù hợp.

Thay vì

try:
    hits[0] 
except IndexError:
    raise NotFoundError

Tôi sẽ kiểm tra rõ ràng:

if not hits:
    raise NotFoundError

vì điều này có xu hướng "rẻ hơn" sau đó thiết lập try.

Tôi đồng ý với bạn về IOError; Tôi cũng sẽ không thử xử lý lỗi khi kết nối với trang web - nếu chúng tôi không thể, vì một số lý do, đây không phải là nơi thích hợp để xử lý nó (vì nó không giúp chúng tôi trả lời câu hỏi của chúng tôi) và nó sẽ vượt qua ra chức năng gọ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.