ข้อยกเว้นสำหรับการควบคุมการไหลที่ดีที่สุดใน Python หรือไม่?


15

ฉันกำลังอ่าน "Learning Python" และเจอสิ่งต่อไปนี้:

ข้อยกเว้นที่ผู้ใช้กำหนดยังสามารถส่งสัญญาณเงื่อนไขที่ไม่ใช่ผู้ใช้ ตัวอย่างเช่นรูทีนการค้นหาสามารถเขียนโค้ดเพื่อเพิ่มข้อยกเว้นเมื่อพบการจับคู่แทนที่จะส่งกลับค่าสถานะสถานะเพื่อให้ผู้โทรตีความ ในต่อไปนี้ตัวจัดการข้อยกเว้นลอง / ยกเว้น / อื่นทำงานของตัวทดสอบ if / else return-value:

class Found(Exception): pass

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

เนื่องจาก Python นั้นถูกพิมพ์แบบไดนามิกและ polymorphic ไปที่แกนกลาง, ข้อยกเว้น, แทนที่จะเป็น Sentinel return values ​​นั้นเป็นวิธีที่นิยมใช้ในการส่งสัญญาณเงื่อนไขดังกล่าว

ฉันได้เห็นสิ่งต่าง ๆ ที่กล่าวถึงหลายครั้งในฟอรัมต่าง ๆ และการอ้างอิงถึง Python โดยใช้ StopIteration เพื่อวางลูป แต่ฉันไม่พบอะไรมากในไกด์นำเที่ยวอย่างเป็นทางการ (PEP 8 มีการอ้างอิงทันทีสำหรับการควบคุมโฟลว์) หรือข้อความจากนักพัฒนา มีอะไรเป็นทางการหรือไม่ที่ระบุว่านี่เป็นแนวทางปฏิบัติที่ดีที่สุดสำหรับ Python

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

TIA

คำตอบ:


25

ฉันทามติทั่วไป“ อย่าใช้ข้อยกเว้น!” ส่วนใหญ่มาจากภาษาอื่นและบางครั้งก็ล้าสมัย

  • ใน C ++ การขว้างข้อยกเว้นนั้นมีค่าใช้จ่ายสูงมากเนื่องจาก“ stack unwinding” การประกาศตัวแปรโลคัลทุกครั้งเป็นเหมือนwithคำสั่งใน Python และวัตถุในตัวแปรนั้นอาจเรียกใช้ destructors destructors เหล่านี้จะถูกดำเนินการเมื่อมีข้อยกเว้นจะถูกโยน แต่เมื่อกลับจากฟังก์ชั่น “ RAII idiom” นี้เป็นคุณสมบัติทางภาษาที่สำคัญและสำคัญอย่างยิ่งในการเขียนรหัสที่ถูกต้องและแม่นยำดังนั้นดังนั้น RAII กับข้อยกเว้นราคาถูกจึงเป็นข้อแลกเปลี่ยนที่ C ++ ตัดสินใจต่อ RAII

  • ในช่วงต้น C ++ โค้ดจำนวนมากไม่ได้ถูกเขียนขึ้นในลักษณะที่ปลอดภัยยกเว้น: ยกเว้นว่าคุณใช้จริง RAII มันเป็นเรื่องง่ายที่จะรั่วไหลของหน่วยความจำและทรัพยากรอื่น ๆ ดังนั้นการโยนข้อยกเว้นจะทำให้รหัสนั้นไม่ถูกต้อง สิ่งนี้ไม่สมเหตุสมผลอีกต่อไปเนื่องจากแม้แต่ไลบรารีมาตรฐาน C ++ ก็ใช้ข้อยกเว้น: คุณไม่สามารถทำท่าว่ามีข้อยกเว้นได้ อย่างไรก็ตามข้อยกเว้นยังคงมีปัญหาเมื่อรวมรหัส C กับ C ++

  • ใน Java ทุกข้อยกเว้นมีการติดตามสแต็กที่เกี่ยวข้อง การติดตามสแต็กมีค่ามากเมื่อทำการดีบักข้อผิดพลาด แต่ต้องใช้ความพยายามอย่างมากเมื่อไม่มีการพิมพ์ข้อยกเว้นเช่นเนื่องจากใช้สำหรับโฟลว์การควบคุมเท่านั้น

ดังนั้นในข้อยกเว้นภาษาเหล่านั้น "แพงเกินไป" ที่จะใช้เป็นโฟลว์ควบคุม ใน Python ปัญหานี้มีน้อยและข้อยกเว้นจะถูกกว่ามาก นอกจากนี้ภาษา Python แล้วทนทุกข์ทรมานจากค่าใช้จ่ายบางอย่างที่ทำให้ค่าใช้จ่ายของข้อยกเว้นสังเกตเมื่อเทียบกับโครงสร้างการควบคุมการไหลอื่น ๆ เช่นการตรวจสอบถ้ารายการ Dict ที่มีอยู่กับการทดสอบการเป็นสมาชิกอย่างชัดเจนif key in the_dict: ...โดยทั่วไปว่าเป็นอย่างรวดเร็วเป็นเพียงแค่การเข้าถึงรายการthe_dict[key]; ...และการตรวจสอบถ้าคุณ รับ KeyError คุณสมบัติทางภาษาที่สำคัญบางอย่าง (เช่นเครื่องกำเนิดไฟฟ้า) ได้รับการออกแบบในแง่ของข้อยกเว้น

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

  • พวกเขาไม่ชัดเจนเลย คุณไม่สามารถดูฟังก์ชั่นได้อย่างง่ายดายและดูว่ามีข้อยกเว้นใดที่ทำให้คุณไม่เข้าใจ ค่าส่งคืนมีแนวโน้มที่จะมีความชัดเจนมากขึ้น

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

โดยทั่วไปแล้ววัฒนธรรมของหลามนั้นมีลักษณะเป็นข้อยกเว้น แต่ก็ง่ายที่จะลงน้ำ ลองนึกภาพlist_contains(the_list, item)ฟังก์ชั่นที่ตรวจสอบว่ารายการนั้นมีรายการเท่ากับรายการนั้นหรือไม่ หากผลการสื่อสารผ่านข้อยกเว้นที่น่ารำคาญอย่างยิ่งเพราะเราต้องเรียกมันว่า:

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

การส่งคืนบูลจะชัดเจนมากขึ้น:

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

หากฟังก์ชั่นที่ควรจะคืนค่าแล้วส่งกลับค่าพิเศษสำหรับเงื่อนไขพิเศษค่อนข้างผิดพลาดได้ง่ายเพราะคนจะลืมที่จะตรวจสอบค่านี้ (นั่นอาจเป็นสาเหตุของปัญหา 1/3 ใน C) ข้อยกเว้นมักจะถูกต้องมากขึ้น

ตัวอย่างที่ดีคือpos = find_string(haystack, needle)ฟังก์ชันที่ค้นหาการเกิดขึ้นครั้งแรกของneedleสตริงในสตริง `haystack และส่งคืนตำแหน่งเริ่มต้น แต่ถ้าพวกเขาเป็นกองฟางไม่มีเข็มสตริง?

วิธีการแก้ปัญหาโดย C และเลียนแบบโดย Python คือการคืนค่าพิเศษ ใน C -1นี้เป็นตัวชี้โมฆะในหลามนี้อยู่ สิ่งนี้จะนำไปสู่ผลลัพธ์ที่น่าประหลาดใจเมื่อมีการใช้ตำแหน่งเป็นดัชนีสตริงโดยไม่ตรวจสอบโดยเฉพาะอย่างยิ่ง-1ดัชนีที่ถูกต้องใน Python ใน C ตัวชี้ NULL ของคุณอย่างน้อยจะให้ segfault

ใน PHP จะส่งคืนค่าพิเศษของประเภทที่แตกต่างนั่นคือบูลีนFALSEแทนที่จะเป็นจำนวนเต็ม ตามที่ปรากฎว่าสิ่งนี้ไม่ได้ดีไปกว่านี้อีกแล้วเนื่องจากกฎการแปลงโดยนัยของภาษา (แต่โปรดทราบว่าใน Python รวมถึง booleans สามารถใช้เป็น ints!) ฟังก์ชั่นที่ไม่ส่งคืนชนิดที่สอดคล้องกันนั้นโดยทั่วไปถือว่ามีความสับสนมาก

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

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

อีกวิธีหนึ่งคือส่งคืนชนิดที่ไม่สามารถใช้โดยตรงได้ แต่ต้องสามารถนำกลับมาใช้ใหม่ได้ก่อนเช่น e-bool tuple ที่บูลีนระบุว่ามีข้อยกเว้นเกิดขึ้นหรือผลที่ได้นั้นสามารถใช้ได้ แล้ว:

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

นี่เป็นการบังคับให้คุณจัดการกับปัญหาในทันที แต่มันก็น่ารำคาญอย่างรวดเร็ว นอกจากนี้ยังป้องกันคุณจากการผูกมัดฟังก์ชั่นได้อย่างง่ายดาย การเรียกใช้ฟังก์ชันทุกอันต้องใช้รหัสสามบรรทัด Golang เป็นภาษาที่คิดว่าสิ่งนี้เป็นสิ่งที่คุ้มค่ากับความปลอดภัย

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


1
ขอบคุณสำหรับคำตอบเชิงลึก ฉันมาจากพื้นหลังของภาษาอื่น ๆ เช่น Java ที่ "ข้อยกเว้นสำหรับเงื่อนไขพิเศษเท่านั้น" ดังนั้นนี่จึงเป็นเรื่องใหม่สำหรับฉัน มี Python core ใด ๆ ที่เคยระบุไว้ในแบบที่พวกเขามีกับแนวทาง Python อื่น ๆ เช่น EAFP, EIBTI และอื่น ๆ (ในรายชื่อผู้รับจดหมาย, โพสต์บล็อก ฯลฯ ) ฉันต้องการอ้างถึงสิ่งที่เป็นทางการหาก dev / boss คนอื่นถาม ขอบคุณ!
JB

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

กระสุนแรกของคุณในบทนำของคุณสับสนในตอนแรก บางทีคุณสามารถเปลี่ยนเป็นบางอย่างเช่น "การจัดการรหัสข้อยกเว้นใน C ++ สมัยใหม่นั้นไม่มีค่าใช้จ่าย แต่การโยนข้อยกเว้นมีราคาแพง"? (สำหรับ lurkers: stackoverflow.com/questions/13835817/ … ) [แก้ไข: แก้ไขข้อเสนอ]
phresnel

โปรดทราบว่าสำหรับการดำเนินการค้นหาพจนานุกรมโดยใช้collections.defaultdictหรือmy_dict.get(key, default)ทำให้โค้ดมีความชัดเจนมากขึ้นtry: my_dict[key] except: return default
Steve Barnes

11

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

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

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

อีกทางเลือกหนึ่ง:

# 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()

แต่สิ่งนี้มีตั้งแต่PEP 343ถูกนำมาใช้ & กลับพอร์ตทั้งหมดได้รับการห่ออย่างเรียบร้อยในwithคำสั่ง ดังกล่าวข้างต้นกลายเป็น 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

ใน python3 สิ่งนี้กลายเป็น:

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

ฉันขอแนะนำให้คุณอ่านPEP 343ซึ่งให้พื้นหลังเหตุผลตัวอย่างและอื่น ๆ

นอกจากนี้ยังเป็นเรื่องปกติที่จะใช้ข้อยกเว้นเพื่อส่งสัญญาณการสิ้นสุดของการประมวลผลเมื่อใช้ฟังก์ชั่นเครื่องกำเนิดเพื่อส่งสัญญาณให้เสร็จ

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


คุณพูดว่า: "ไม่! - ไม่ใช่โดยทั่วไป - ข้อยกเว้นจะไม่ถือว่าเป็นการฝึกการควบคุมการไหลที่ดียกเว้นรหัสชั้นเดียว" เหตุผลสำหรับคำสั่งที่เน้นนี้อยู่ที่ไหน คำตอบนี้ดูเหมือนจะเน้นไปที่กรณีการทำซ้ำอย่างหวุดหวิด มีอีกหลายกรณีที่ผู้คนอาจคิดว่าจะใช้ข้อยกเว้น ปัญหาในการทำเช่นนั้นในกรณีอื่น ๆ เหล่านั้นคืออะไร?
Daniel Waltrip

@DanielWaltrip ฉันเห็นรหัสมากเกินไปในภาษาการเขียนโปรแกรมต่าง ๆ ที่มีการใช้ข้อยกเว้นบ่อยครั้งที่กว้างเกินไปเมื่อความเรียบง่ายifดีกว่า เมื่อพูดถึงการดีบัก & ทดสอบหรือให้เหตุผลเกี่ยวกับพฤติกรรมรหัสของคุณจะเป็นการดีกว่าที่จะไม่ใช้ข้อยกเว้น - ควรเป็นข้อยกเว้น ฉันได้เห็นแล้วในรหัสการผลิตการคำนวณที่ส่งคืนผ่านการโยน / การจับแทนที่จะส่งคืนแบบง่ายนั่นหมายความว่าเมื่อการคำนวณการหารหารด้วยข้อผิดพลาดเป็นศูนย์จะส่งคืนค่าการสุ่มแทนที่จะหยุดทำงานเมื่อเกิดข้อผิดพลาด แก้จุดบกพร่อง
Steve Barnes

เฮ้ @SteveBarnes ตัวอย่างการจับข้อผิดพลาดหารด้วยศูนย์ดูเหมือนการเขียนโปรแกรมที่ไม่ดี (ที่มีการจับทั่วไปมากเกินไปที่ไม่ยกข้อยกเว้นขึ้นอีกครั้ง)
wTyeRogers

คำตอบของคุณดูเหมือนจะเดือดลงไปที่: A) "ไม่!" B) มีตัวอย่างที่ถูกต้องของที่การควบคุมการไหลผ่านข้อยกเว้นทำงานได้ดีกว่าทางเลือก C) การแก้ไขรหัสของคำถามที่จะใช้เครื่องกำเนิดไฟฟ้า (ซึ่งใช้ข้อยกเว้นเป็นการควบคุมการไหลและทำได้ดี) ในขณะที่คำตอบของคุณเป็นข้อมูลทั่วไปไม่มีข้อโต้แย้งปรากฏอยู่ในนั้นเพื่อสนับสนุนรายการ A ซึ่งเป็นตัวหนาและดูเหมือนคำสั่งหลักฉันคาดหวังการสนับสนุนบางอย่าง หากได้รับการสนับสนุนฉันจะไม่โหวตคำตอบของคุณ อนิจจา ...
wTyeRogers
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.