2.26. การ raise ข้อผิดพลาด#

ฟังก์ชันสามารถส่งสัญญาณปัญหาให้ผู้เรียกโดยการ raise exception คีย์เวิร์ดคือ raise:

def square_root(x):
    if x < 0:
        raise ValueError("square_root expects a non-negative number")
    return x ** 0.5

การเรียก square_root(-1) จะหยุดที่บรรทัด raise กระโดดออกจากฟังก์ชัน และค้นหา except ที่ตรงกันในผู้เรียก หากไม่มีผู้เรียกดักจับ สคริปต์จะสิ้นสุดพร้อม traceback

2.26.1. ทำไมต้อง raise แทนการคืนค่า sentinel#

สองวิธีในการรายงาน "input ไม่ถูกต้อง":

# signal with a sentinel
def square_root_or_none(x):
    if x < 0:
        return None
    return x ** 0.5

# raise an exception
def square_root(x):
    if x < 0:
        raise ValueError("...")
    return x ** 0.5

รูปแบบ exception มักดีกว่า:

  • ผู้เรียกต้อง จงใจ จัดการกรณีข้อผิดพลาด ไม่ว่าจะด้วย try หรือปล่อยให้ exception ส่งต่อ Sentinels ลืมได้ง่ายและเข้าใจผิดว่าเป็นผลลัพธ์ปกติได้ง่าย

  • ข้อความข้อผิดพลาดเดินทางไปกับ exception แต่วิธี sentinel ต้องแนบการวินิจฉัยไว้ที่อื่น

  • พฤติกรรมเริ่มต้นเมื่อ exception ไม่ถูกจัดการคือการ crash ดังๆ พร้อม traceback ที่ชี้ไปที่การเรียกที่ผิดพลาด การคืน None แบบเงียบกลายเป็น bug ที่ซ่อนอยู่ในภายหลัง

ใช้ sentinels เฉพาะเมื่อ "ไม่พบ" เป็นผลลัพธ์ที่เกิดขึ้นปกติและไม่ใช่กรณีพิเศษ dict.get() คืน None เมื่อ key ไม่มี เพราะการค้นหาคาดว่าจะไม่พบได้บางครั้ง

2.26.2. Custom exception classes#

หากต้องการ raise ปัญหาที่ผู้เรียกอาจต้องการแยกแยะจาก built-in errors ให้กำหนด subclass ของ Exception:

class ConfigError(Exception):
    pass

def load_config(path):
    try:
        f = open(path)
    except OSError as e:
        raise ConfigError("missing config file: " + path)

try:
    load_config("settings.json")
except ConfigError as e:
    print("startup failed:", e)

body class ที่ว่างเปล่าใช้ได้ ชื่อเองคือสิ่งที่สำคัญ เพราะผู้เรียกดักจับตาม class จัดกลุ่ม errors ที่เกี่ยวข้องภายใต้ base เดียวกันหากผู้เรียกต้องการดักจับทั้งกลุ่มในบล็อกเดียว

2.26.2.1. การ re-raise#

raise แบบเปล่าภายในบล็อก except จะ re-raise exception ปัจจุบันให้ส่งต่อไปยัง handler ถัดไป:

try:
    do_work()
except Exception as e:
    log(e)
    raise        # let it keep going

นี่คือรูปแบบที่ถูกต้องเมื่อฟังก์ชันต้องการ สังเกต ข้อผิดพลาด (log ไว้ นับ หรือยกเลิกการเปลี่ยนแปลงบางส่วน) โดยไม่ได้จัดการจริงๆ

2.26.3. เมื่อใดควรดักจับและเมื่อใดควรส่งต่อ#

กฎเกณฑ์ที่มีประโยชน์:

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

  • ปล่อยให้ส่งต่อเมื่อไม่มีอะไรมีประโยชน์ให้ทำนอกจาก crash หรือเมื่อชั้นด้านบนเป็นอันที่รู้วิธีกู้คืน

ฟังก์ชันกลาง call stack ที่กลืน errors ทุกอย่างและคืนค่าอย่างเงียบทำให้ความล้มเหลวตามรอยไม่ได้ เลือกให้ exceptions เดินทางจนถึงโค้ดที่มีแผนจริงๆ