ฉันควรใช้ข้อยกเว้นเมื่อใดและอย่างไร


20

การตั้งค่า

ฉันมักจะมีปัญหาในการกำหนดเวลาและวิธีใช้ข้อยกเว้น ลองพิจารณาตัวอย่างง่ายๆ: สมมติว่าฉันกำลังขูดหน้าเว็บให้พูดว่า " http://www.abevigoda.com/ " เพื่อพิจารณาว่า Abe Vigoda ยังมีชีวิตอยู่หรือไม่ ในการทำสิ่งนี้สิ่งที่เราต้องทำคือดาวน์โหลดหน้าและค้นหาเวลาที่วลี "Abe Vigoda" ปรากฏขึ้น เรากลับมาปรากฏตัวครั้งแรกเนื่องจากมีสถานะของ Abe ตามแนวคิดแล้วจะมีลักษณะดังนี้:

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"

ไหนparse_abe_status(s)จะใช้เวลาสตริงของรูปแบบ "เอ็บ Vigoda เป็นบางสิ่งบางอย่าง " และผลตอบแทน " อะไรบางอย่าง " ส่วนหนึ่ง

ก่อนที่คุณจะยืนยันว่ามีวิธีที่ดีกว่าและแข็งแกร่งกว่าในการคัดลอกหน้านี้สำหรับสถานะของ Abe โปรดจำไว้ว่านี่เป็นเพียงตัวอย่างที่เรียบง่าย

ตอนนี้รหัสนี้จะพบปัญหาที่ไหน ท่ามกลางข้อผิดพลาดอื่น ๆ บางคน "คาดหวัง" คือ:

  • download_pageIOErrorอาจจะไม่สามารถที่จะดาวน์โหลดหน้าและพ่น
  • URL อาจไม่ชี้ไปที่หน้าขวาหรือหน้าถูกดาวน์โหลดอย่างไม่ถูกต้องดังนั้นจึงไม่มีความนิยม hitsเป็นรายการที่ว่างเปล่าแล้ว
  • หน้าเว็บมีการเปลี่ยนแปลงซึ่งอาจทำให้สมมติฐานของเราเกี่ยวกับหน้าเว็บผิด บางทีเราอาจคาดหวังถึง 4 กล่าวถึง Abe Vigoda แต่ตอนนี้เราพบ 5
  • ด้วยเหตุผลบางอย่างhits[0]อาจไม่ใช่สตริงของรูปแบบ "Abe Vigoda คือบางสิ่ง " และดังนั้นจึงไม่สามารถแยกวิเคราะห์ได้อย่างถูกต้อง

กรณีแรกไม่ใช่ปัญหาสำหรับฉันจริง ๆ : การIOErrorโยนถูกและสามารถจัดการได้โดยผู้เรียกใช้ฟังก์ชันของฉัน ลองพิจารณากรณีอื่น ๆ และฉันจะจัดการกับมันอย่างไร แต่ก่อนอื่นสมมติว่าเราใช้งานparse_abe_statusด้วยวิธีที่โง่ที่สุดเท่าที่จะทำได้:

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

กล่าวคือมันไม่ได้ทำการตรวจสอบข้อผิดพลาดใด ๆ ตอนนี้ไปที่ตัวเลือก:

ตัวเลือกที่ 1: การกลับมา None

ฉันสามารถบอกผู้โทรได้ว่ามีบางอย่างผิดปกติจากการส่งคืน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"

หากผู้โทรได้รับNoneจากการทำงานของฉันเขาควรคิดว่าไม่มีการเอ่ยถึง Abe Vigoda และมีบางอย่างผิดปกติ แต่นี่มันคลุมเครือใช่มั้ย และมันไม่ได้ช่วยกรณีที่hits[0]ไม่ใช่สิ่งที่เราคิดว่ามันเป็น

ในทางกลับกันเราสามารถใส่ข้อยกเว้นบางอย่าง:

ตัวเลือกที่ 2: การใช้ข้อยกเว้น

ถ้าhitsเป็นที่ว่างเปล่าเป็นจะถูกโยนเมื่อเราพยายามIndexError hits[0]แต่ผู้เรียกไม่ควรคาดหวังว่าจะจัดการกับIndexErrorหน้าที่ของฉันเพราะเขาไม่รู้ว่ามันIndexErrorมาจากไหน; มันอาจถูกโยนทิ้งไปfind_all_mentionsเพราะทุกสิ่งที่เขารู้ ดังนั้นเราจะสร้างคลาสยกเว้นที่กำหนดเองเพื่อจัดการกับสิ่งนี้:

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"

ตอนนี้จะเกิดอะไรขึ้นหากหน้าเว็บมีการเปลี่ยนแปลงและมีจำนวนครั้งที่เข้าชมที่ไม่คาดคิด นี่ไม่ใช่ความหายนะเนื่องจากรหัสอาจยังใช้งานได้ แต่ผู้โทรอาจต้องระมัดระวังเป็นพิเศษหรือเขาอาจต้องการบันทึกคำเตือน ดังนั้นฉันจะโยนคำเตือน:

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"

สุดท้ายเราอาจพบstatusว่าไม่ได้มีชีวิตอยู่หรือตายไป comatoseอาจจะด้วยเหตุผลบางอย่างที่แปลกวันนี้มันเปิดออกมาเป็น ถ้าอย่างนั้นฉันก็ไม่อยากกลับFalseเพราะนั่นหมายความว่า Abe นั้นตายแล้ว ฉันควรทำอะไรที่นี่ โยนข้อยกเว้นอาจ แต่อะไรล่ะ ฉันควรสร้างคลาสยกเว้นที่กำหนดเองได้หรือไม่

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"

ตัวเลือก 3: อยู่ระหว่าง

ฉันคิดว่าวิธีที่สองที่มีข้อยกเว้นดีกว่า แต่ฉันไม่แน่ใจว่าฉันใช้ข้อยกเว้นอย่างถูกต้องภายในหรือไม่ ฉันอยากรู้ว่าโปรแกรมเมอร์ผู้มากประสบการณ์จะจัดการกับสิ่งนี้ได้อย่างไร

คำตอบ:


17

คำแนะนำใน Python คือการใช้ข้อยกเว้นเพื่อระบุความล้มเหลว สิ่งนี้เป็นจริงแม้ว่าคุณจะคาดหวังความล้มเหลวเป็นประจำ

ดูจากมุมมองของผู้โทรของรหัสของคุณ:

my_status = get_abe_status(my_url)

ถ้าเราคืนไม่มี หากผู้เรียกไม่ได้จัดการกับกรณีที่ get_abe_status ล้มเหลวเป็นพิเศษมันจะพยายามดำเนินการต่อโดยที่ my_stats เป็น None ซึ่งอาจสร้างปัญหาในการวินิจฉัยข้อผิดพลาดได้ในภายหลัง แม้ว่าคุณจะตรวจสอบไม่มีรหัสนี้ไม่มีเงื่อนงำทำไม get_abe_status () ล้มเหลว

แต่ถ้าเรายกข้อยกเว้นขึ้นมาล่ะ? หากผู้เรียกไม่ได้จัดการกับเคสเป็นพิเศษข้อยกเว้นจะแพร่กระจายขึ้นด้านบนในที่สุดก็กดปุ่มตัวจัดการข้อยกเว้นเริ่มต้น นั่นอาจไม่ใช่สิ่งที่คุณต้องการ แต่ก็ควรที่จะแนะนำบั๊กที่ละเอียดอ่อนที่อื่นในโปรแกรม นอกจากนี้ข้อยกเว้นให้ข้อมูลเกี่ยวกับสิ่งที่ผิดพลาดซึ่งหายไปในรุ่นแรก

จากมุมมองของผู้โทรมันง่ายกว่าที่จะได้รับข้อยกเว้นมากกว่าค่าส่งคืน และนั่นคือรูปแบบของงูหลามเพื่อใช้ข้อยกเว้นเพื่อบ่งบอกถึงสภาพความล้มเหลวที่ไม่ส่งคืนค่า

บางคนจะใช้มุมมองที่แตกต่างและยืนยันว่าคุณควรใช้ข้อยกเว้นสำหรับกรณีที่คุณไม่เคยคาดหวังว่าจะเกิดขึ้น พวกเขายืนยันว่าการวิ่งตามปกติไม่ควรเพิ่มข้อยกเว้นใด ๆ เหตุผลหนึ่งที่ให้ไว้สำหรับสิ่งนี้คือข้อยกเว้นนั้นไม่มีประสิทธิภาพอย่างมาก แต่นั่นไม่จริงสำหรับ Python

สองสามคะแนนในรหัสของคุณ:

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.")

คุณรู้ไหมว่าตัวบันทึกรายได้บรรทัดจะไม่ทำงานใช่ไหม?


1
ขอบคุณ (ล่าช้า) สำหรับคำตอบของคุณ มันพร้อมกับการดูรหัสที่เผยแพร่ได้ปรับปรุงความรู้สึกของฉันสำหรับเวลาและวิธีการที่จะทำให้เกิดข้อยกเว้น
jme

4

คำตอบที่ได้รับการยอมรับนั้นสมควรที่จะได้รับการยอมรับและตอบคำถามฉันเขียนสิ่งนี้เพื่อให้พื้นหลังเพิ่มเติมเล็กน้อย

หนึ่งในความน่าเชื่อถือของไพ ธ อนคือง่ายกว่าที่จะขอการให้อภัยมากกว่าการอนุญาต ซึ่งหมายความว่าโดยทั่วไปคุณเพียงแค่ทำสิ่งต่าง ๆ และถ้าคุณคาดว่าจะมีข้อยกเว้น ตรงข้ามกับการทำถ้าตรวจสอบก่อนมือเพื่อให้แน่ใจว่าคุณจะไม่ได้รับข้อยกเว้น

ฉันต้องการให้ตัวอย่างเพื่อแสดงให้คุณเห็นว่าความแตกต่างอย่างมากในความคิดจาก C ++ / Java โดยทั่วไปแล้ว for for c ++ จะมีลักษณะดังนี้:

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

วิธีคิดเกี่ยวกับสิ่งนี้: การเข้าถึงmyvector[k]โดยที่ k> = myvector.size () จะทำให้เกิดข้อยกเว้น โดยหลักการแล้วคุณสามารถเขียนสิ่งนี้ (อย่างเชื่องช้ามาก) ในฐานะลองดู

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

หรือสิ่งที่คล้ายกัน ตอนนี้ให้พิจารณาสิ่งที่เกิดขึ้นในหลามสำหรับลูป:

for i in range(1):
    ...

มันทำงานอย่างไร สำหรับวงจะใช้เวลาในช่วงที่ผล (1) และเรียกเราเตอร์ () บนมันโลภ iterator กับมัน

b = range(1).__iter__()

จากนั้นจะเรียกถัดไปที่แต่ละรอบการวนซ้ำจนกระทั่ง ... :

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

กล่าวอีกนัยหนึ่งการวนซ้ำในไพ ธ อนเป็นการลองยกเว้นในการปลอมตัว

เท่าที่เป็นรูปธรรมไปคำถามโปรดจำไว้ว่าข้อยกเว้นหยุดการทำงานของฟังก์ชั่นปกติและจะต้องจัดการแยกต่างหาก ใน Python คุณควรโยนมันอย่างอิสระทุกครั้งที่ไม่มีจุดดำเนินการส่วนที่เหลือของรหัสในฟังก์ชั่นของคุณและ / หรือไม่มีผลตอบแทนที่ถูกต้องสะท้อนถึงสิ่งที่เกิดขึ้นในฟังก์ชั่น โปรดทราบว่าการกลับมาก่อนจากฟังก์ชั่นจะแตกต่างกัน: การกลับมาเร็วหมายถึงคุณได้คำตอบแล้วและไม่ต้องการรหัสที่เหลือเพื่อหาคำตอบ ฉันกำลังบอกว่าควรจะโยนข้อยกเว้นเมื่อคำตอบไม่เป็นที่รู้จักและส่วนที่เหลือของรหัสเพื่อตรวจสอบคำตอบไม่สามารถทำงานได้อย่างสมเหตุสมผล ตอนนี้ "สะท้อนให้เห็นอย่างถูกต้อง" ตัวเองเช่นเดียวกับข้อยกเว้นที่คุณเลือกที่จะโยนเป็นเรื่องของเอกสาร

ในกรณีของรหัสเฉพาะของคุณฉันจะบอกว่าสถานการณ์ใด ๆ ที่ทำให้ฮิตเป็นรายการว่างเปล่าควรจะโยน ทำไม? วิธีการตั้งค่าฟังก์ชั่นของคุณไม่มีวิธีที่จะหาคำตอบได้โดยไม่ต้องแยกคำตอบ ดังนั้นหากการเข้าชมไม่สามารถแยกวิเคราะห์ได้เนื่องจาก URL ไม่ดีหรือเนื่องจากการเข้าชมว่างเปล่าฟังก์ชันจะไม่สามารถตอบคำถามได้และที่จริงแล้วไม่สามารถลองได้

ในกรณีพิเศษนี้ฉันจะโต้แย้งว่าแม้ว่าคุณจะแยกวิเคราะห์และไม่ได้รับคำตอบที่สมเหตุสมผล (มีชีวิตอยู่หรือตายไปแล้ว) คุณก็ควรโยนทิ้ง ทำไม? เนื่องจากฟังก์ชันจะส่งคืนบูลีน การกลับมาไม่มีไม่มีเป็นอันตรายต่อลูกค้าของคุณ หากพวกเขาทำถ้าตรวจสอบในไม่มีจะไม่มีความล้มเหลวก็จะถือว่าเป็นเท็จ ดังนั้นลูกค้าของคุณโดยทั่วไปจะต้องทำถ้าไม่มีตรวจสอบใด ๆ ถ้าเขาไม่ต้องการความล้มเหลวเงียบ ... ดังนั้นคุณควรจะโยน


2

คุณควรใช้ข้อยกเว้นเมื่อมีสิ่งพิเศษเกิดขึ้น นั่นคือสิ่งที่ไม่ควรเกิดขึ้นเนื่องจากการใช้แอปพลิเคชันอย่างเหมาะสม หากได้รับอนุญาตและคาดว่าผู้บริโภคของวิธีการของคุณเพื่อค้นหาสิ่งที่จะไม่พบแล้ว "ไม่พบ" ไม่ใช่กรณีพิเศษ ในกรณีนี้คุณควรส่งคืนค่าว่างหรือ "ไม่มี" หรือ {} หรือสิ่งที่ระบุชุดส่งคืนที่ว่างเปล่า

ในทางกลับกันหากคุณคาดหวังว่าผู้บริโภคของวิธีการของคุณจะเสมอ (เว้นแต่พวกเขาทำให้ตกใจอย่างใด) ค้นหาสิ่งที่ถูกค้นหาแล้วไม่พบว่ามันจะเป็นข้อยกเว้นและคุณควรไปด้วย

กุญแจสำคัญคือการจัดการข้อยกเว้นอาจมีราคาแพง - ข้อยกเว้นควรรวบรวมข้อมูลเกี่ยวกับสถานะของแอปพลิเคชันของคุณเมื่อเกิดขึ้นเช่นการติดตามสแต็กเพื่อช่วยให้ผู้ใช้ถอดรหัสสาเหตุที่เกิดขึ้น ฉันไม่คิดว่าเป็นสิ่งที่คุณพยายามจะทำ


1
หากคุณตัดสินใจว่าไม่อนุญาตให้ค้นหาค่าควรใช้ความระมัดระวังเกี่ยวกับสิ่งที่คุณใช้เพื่อระบุว่าเกิดอะไรขึ้น หากวิธีการของคุณควรจะส่งคืนStringและคุณเลือก "ไม่มี" เป็นตัวบ่งชี้ของคุณซึ่งหมายความว่าคุณจะต้องระมัดระวังว่า "ไม่มี" จะไม่เป็นค่าที่ถูกต้อง โปรดทราบว่ามีความแตกต่างระหว่างการดูข้อมูลและไม่พบค่าและไม่สามารถเรียกคืนข้อมูลได้ดังนั้นเราจึงไม่สามารถค้นหาข้อมูลได้ การมีผลลัพธ์แบบเดียวกันสำหรับสองกรณีนี้หมายความว่าคุณไม่มีทัศนวิสัยเมื่อคุณไม่ได้รับค่าเมื่อคุณคาดหวังว่าจะมี
unholysampler

บล็อกรหัสแบบอินไลน์มีการทำเครื่องหมายด้วย backticks (`) บางทีนั่นอาจเป็นสิ่งที่คุณต้องการทำกับ" ไม่มี "
Izkata

3
ฉันกลัวว่านี่เป็นของปลอมใน Python คุณกำลังใช้การใช้เหตุผลสไตล์ C ++ / Java กับภาษาอื่น Python ใช้ข้อยกเว้นเพื่อระบุจุดสิ้นสุดของ for loop นั่นเป็นเรื่องที่ไม่มีข้อยกเว้น
Nir Friedman

2

ถ้าฉันเขียนฟังก์ชั่น

 def abe_is_alive():

ฉันจะเขียนถึงreturn TrueหรือFalseในกรณีที่ฉันแน่ใจอย่างใดอย่างหนึ่งหรืออื่น ๆ และraiseข้อผิดพลาดในกรณีอื่น ๆ (เช่นraise ValueError("Status neither 'dead' nor 'alive'")) นี่เป็นเพราะฟังก์ชั่นการเรียกของฉันกำลังคาดหวังว่าบูลีนและถ้าฉันไม่สามารถระบุได้ว่ามีการไหลของโปรแกรมปกติไม่ควรดำเนินการต่อไป

บางอย่างเช่นตัวอย่างการได้รับ "จำนวนครั้ง" ที่แตกต่างจากที่คาดไว้ฉันอาจเพิกเฉย ตราบใดที่หนึ่งในเพลงฮิตยังคงตรงกับรูปแบบของฉัน "Abe Vigoda is {dead | alive}" ก็ไม่เป็นไร สิ่งนี้ช่วยให้หน้าสามารถจัดเรียงใหม่ แต่ยังได้รับข้อมูลที่เหมาะสม

ค่อนข้างมากกว่า

try:
    hits[0] 
except IndexError:
    raise NotFoundError

ฉันจะตรวจสอบอย่างชัดเจน:

if not hits:
    raise NotFoundError

เช่นนี้มีแนวโน้มที่จะ "ถูกกว่า" tryแล้วการตั้งค่า

ฉันเห็นด้วยกับคุณในIOError; ฉันจะไม่พยายามที่จะจัดการกับข้อผิดพลาดในการเชื่อมต่อกับเว็บไซต์ - ถ้าเราทำไม่ได้ด้วยเหตุผลบางอย่างนี่ไม่ใช่สถานที่ที่เหมาะสมที่จะจัดการกับมัน (เพราะมันไม่ได้ช่วยให้เราตอบคำถามของเรา) และควรผ่าน ออกไปยังฟังก์ชั่นการโทร

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.