'ในที่สุด' จะดำเนินการใน Python เสมอหรือไม่?


126

สำหรับการบล็อกการลองสุดท้ายที่เป็นไปได้ใน Python จะรับประกันfinallyได้หรือไม่ว่าบล็อกจะถูกเรียกใช้งานเสมอ

ตัวอย่างเช่นสมมติว่าฉันกลับมาขณะอยู่ในexceptบล็อก:

try:
    1/0
except ZeroDivisionError:
    return
finally:
    print("Does this code run?")

หรือบางทีฉันอาจจะเพิ่มException:

try:
    1/0
except ZeroDivisionError:
    raise
finally:
    print("What about this code?")

การทดสอบแสดงให้เห็นว่ามีfinallyการดำเนินการสำหรับตัวอย่างข้างต้น แต่ฉันคิดว่ามีสถานการณ์อื่น ๆ ที่ฉันไม่ได้นึกถึง

มีสถานการณ์ใดบ้างที่finallyบล็อกไม่สามารถดำเนินการใน Python ได้


18
กรณีเดียวที่ฉันนึกได้ว่าfinallyล้มเหลวในการดำเนินการหรือ "พ่ายแพ้จุดประสงค์" คือระหว่างการวนซ้ำที่ไม่มีที่สิ้นสุดsys.exitหรือการขัดจังหวะแบบบังคับ เอกสารระบุว่าfinallyจะดำเนินการเสมอดังนั้นฉันไปกับที่
Xay

1
ความคิดด้านข้างเล็กน้อยและแน่ใจว่าไม่ใช่สิ่งที่คุณถาม แต่ฉันค่อนข้างแน่ใจว่าถ้าคุณเปิดตัวจัดการงานและฆ่ากระบวนการfinallyจะไม่ทำงาน หรือเหมือนกันถ้าคอมพิวเตอร์ขัดข้องก่อน: D
Alejandro

145
finallyจะไม่ทำงานหากสายไฟขาดจากผนัง
user253751

3
คุณอาจสนใจคำตอบสำหรับคำถามเดียวกันเกี่ยวกับ C #: stackoverflow.com/a/10260233/88656
Eric Lippert

1
ปิดกั้นบนเซมาฟอร์ที่ว่างเปล่า ห้ามส่งสัญญาณเด็ดขาด เสร็จสิ้น
Martin James

คำตอบ:


207

"รับประกัน" เป็นคำที่แข็งแกร่งกว่าการใช้งานที่finallyสมควรได้รับ อะไรคือสิ่งที่รับประกันว่าหากการดำเนินการไหลออกของทั้งtry- finallyสร้างก็จะผ่านfinallyการทำเช่นนั้น สิ่งที่ไม่ได้รับประกันการดำเนินการที่จะไหลออกของ-tryfinally

  • finallyในกำเนิดหรือ async coroutine อาจไม่เคยทำงานถ้าวัตถุไม่เคยดำเนินการไปสู่ข้อสรุป มีหลายวิธีที่อาจเกิดขึ้นได้ นี่คือหนึ่ง:

    def gen(text):
        try:
            for line in text:
                try:
                    yield int(line)
                except:
                    # Ignore blank lines - but catch too much!
                    pass
        finally:
            print('Doing important cleanup')
    
    text = ['1', '', '2', '', '3']
    
    if any(n > 1 for n in gen(text)):
        print('Found a number')
    
    print('Oops, no cleanup.')
    

    โปรดทราบว่าตัวอย่างนี้ค่อนข้างยุ่งยาก: เมื่อเครื่องกำเนิดไฟฟ้าถูกรวบรวมขยะ Python จะพยายามเรียกใช้finallyบล็อกโดยทิ้งGeneratorExitข้อยกเว้น แต่ที่นี่เราตรวจจับข้อยกเว้นนั้นและจากนั้นyieldอีกครั้งเมื่อถึงจุดนั้น Python จะพิมพ์คำเตือน ("generator เพิกเฉย GeneratorExit ") และยอมแพ้ ดูPEP 342 (Coroutines ผ่าน Enhanced Generators)สำหรับรายละเอียด

    วิธีการอื่น ๆ กำเนิดหรือ coroutine อาจจะไม่ดำเนินการสรุปรวมว่าวัตถุเป็นเพียงการไม่เคย GC'ed (ใช่ว่าเป็นไปได้แม้ใน CPython) หรือถ้าasync with awaitใน__aexit__หรือถ้าวัตถุawaitหรือyields ในfinallyบล็อก รายการนี้ไม่ได้มีวัตถุประสงค์เพื่อให้ข้อมูลครบถ้วน

  • A finallyใน daemon thread อาจไม่รันหากเธรดที่ไม่ใช่ daemon ทั้งหมดออกก่อน

  • os._exitจะหยุดกระบวนการทันทีโดยไม่ต้องดำเนินการfinallyบล็อก

  • os.forkอาจทำให้เกิดการfinallyบล็อกที่จะดำเนินการเป็นครั้งที่สอง เช่นเดียวกับการเพียง แต่ปัญหาที่ตามปกติคุณคาดหวังจากสิ่งที่เกิดขึ้นสองครั้งนี้อาจทำให้เกิดความขัดแย้งเข้าถึงพร้อมกัน (ล่ม, แผงลอย, ... ) ถ้าการเข้าถึงทรัพยากรที่ใช้ร่วมกันจะไม่ตรงกันได้อย่างถูกต้อง

    เนื่องจากmultiprocessingใช้ fork-without-exec เพื่อสร้างกระบวนการของผู้ปฏิบัติงานเมื่อใช้วิธีfork start (ค่าเริ่มต้นใน Unix) จากนั้นจึงเรียกos._exitผู้ปฏิบัติงานเมื่องานของผู้ปฏิบัติงานเสร็จสิ้นfinallyและmultiprocessingการโต้ตอบอาจเป็นปัญหาได้ ( ตัวอย่าง )

  • ข้อผิดพลาดในการแบ่งส่วนระดับ C จะป้องกันไม่ให้finallyบล็อกทำงาน
  • kill -SIGKILLจะป้องกันไม่ให้finallyบล็อกทำงาน SIGTERMและSIGHUPยังป้องกันไม่ให้finallyบล็อกทำงานเว้นแต่คุณจะติดตั้งตัวจัดการเพื่อควบคุมการปิดเครื่องด้วยตัวเอง โดยค่าเริ่มต้น Python ไม่จัดการSIGTERMหรือSIGHUP.
  • ข้อยกเว้นในfinallyสามารถป้องกันไม่ให้การล้างข้อมูลเสร็จสิ้น กรณีหนึ่งที่น่าสังเกตโดยเฉพาะอย่างยิ่งหากผู้ใช้ฮิตควบคุม-C เพียงในขณะที่เรากำลังเริ่มดำเนินการfinallyบล็อก Python จะเพิ่มKeyboardInterruptและข้ามfinallyเนื้อหาของบล็อกทุกบรรทัด (- KeyboardInterruptโค้ดปลอดภัยเขียนยากมาก)
  • หากคอมพิวเตอร์สูญเสียพลังงานหรือหากอยู่ในโหมดไฮเบอร์เนตและไม่ตื่นขึ้นมาfinallyบล็อกจะไม่ทำงาน

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


14
ฉันเชื่อว่าจุดแรกของรายการของคุณเท่านั้นที่เกี่ยวข้องจริงๆและมีวิธีง่ายๆในการหลีกเลี่ยง: 1) อย่าใช้เครื่องเปล่าexceptและอย่าจับGeneratorExitเข้าไปในเครื่องกำเนิดไฟฟ้า ประเด็นเกี่ยวกับเธรด / การฆ่ากระบวนการ / segfaulting / ปิดเครื่องคาดว่า python ไม่สามารถทำเวทมนตร์ได้ นอกจากนี้: ข้อยกเว้นในfinallyเป็นปัญหาอย่างเห็นได้ชัด แต่สิ่งนี้ไม่ได้เปลี่ยนความจริงที่ว่าโฟลว์การควบคุมถูกย้ายไปที่finallyบล็อก เกี่ยวกับCtrl+Cคุณสามารถเพิ่มเครื่องจัดการสัญญาณที่เพิกเฉยหรือเพียงแค่ "กำหนดเวลา" ให้ปิดเครื่องใหม่ทั้งหมดหลังจากการดำเนินการปัจจุบันเสร็จสิ้น
Giacomo Alzetta

8
การกล่าวถึงkill -9นั้นถูกต้องในทางเทคนิค แต่ค่อนข้างไม่ยุติธรรม ไม่มีโปรแกรมที่เขียนด้วยภาษาใด ๆ รันโค้ดใด ๆ เมื่อได้รับ kill -9 ในความเป็นจริงไม่มีโปรแกรมใดที่ได้รับ kill -9 เลยแม้ว่ามันจะต้องการ แต่ก็ไม่สามารถดำเนินการอะไรได้ นั่นคือจุดรวมของการฆ่า -9
ทอม

10
@ ทอม: ประเด็นเกี่ยวกับkill -9ไม่ได้ระบุภาษา และตรงไปตรงมามันต้องการการทำซ้ำเพราะมันอยู่ในจุดบอด มีคนจำนวนมากเกินไปที่ลืมหรือไม่ทราบว่าโปรแกรมของพวกเขาอาจหยุดตายในเส้นทางโดยไม่ได้รับอนุญาตให้ทำความสะอาด
cHao

5
@GiacomoAlzetta: มีผู้คนมากมายที่อาศัยfinallyบล็อกราวกับว่าพวกเขาให้การค้ำประกันการทำธุรกรรม อาจดูเหมือนชัดเจนว่าไม่ทำ แต่ไม่ใช่สิ่งที่ทุกคนตระหนัก สำหรับกรณีของเครื่องกำเนิดไฟฟ้ามีหลายวิธีที่เครื่องกำเนิดไฟฟ้าอาจไม่ได้รับการ GC เลยและหลายวิธีที่เครื่องกำเนิดไฟฟ้าหรือโครูทีนอาจให้ผลโดยบังเอิญGeneratorExitแม้ว่าจะไม่จับGeneratorExitก็ตามตัวอย่างเช่นหากเครื่องasync withหยุดทำงาน coroutine __exit__ใน
user2357112 รองรับ Monica

2
@ user2357112 ใช่ - ฉันพยายามมาหลายทศวรรษแล้วที่จะรับ devs เพื่อล้างไฟล์ temp และอื่น ๆ ในการเริ่มต้นแอปไม่ใช่ออก อาศัยสิ่งที่เรียกว่า 'สะอาดขึ้นและการยุติอย่างสง่างาม' กำลังขอความผิดหวังและน้ำตา :)
Martin James

68

ใช่. สุดท้ายชนะเสมอ

วิธีเดียวที่จะเอาชนะมันได้คือหยุดการดำเนินการก่อนที่finally:จะมีโอกาสดำเนินการ (เช่นขัดจังหวะล่ามปิดคอมพิวเตอร์ของคุณหยุดเครื่องกำเนิดไฟฟ้าตลอดไป)

ฉันคิดว่ามีสถานการณ์อื่น ๆ ที่ฉันไม่เคยนึกถึง

ต่อไปนี้เป็นอีกสองสามข้อที่คุณอาจไม่เคยนึกถึง:

def foo():
    # finally always wins
    try:
        return 1
    finally:
        return 2

def bar():
    # even if he has to eat an unhandled exception, finally wins
    try:
        raise Exception('boom')
    finally:
        return 'no boom'

ขึ้นอยู่กับว่าคุณเลิกล่ามในที่สุดบางครั้งคุณสามารถ "ยกเลิก" ได้ในที่สุด แต่ไม่ใช่เช่นนี้:

>>> import sys
>>> try:
...     sys.exit()
... finally:
...     print('finally wins!')
... 
finally wins!
$

การใช้คำที่ล่อแหลมos._exit(ซึ่งอยู่ภายใต้ "ความผิดพลาดของล่าม" ในความคิดของฉัน):

>>> import os
>>> try:
...     os._exit(1)
... finally:
...     print('finally!')
... 
$

ฉันกำลังใช้รหัสนี้เพื่อทดสอบว่าในที่สุดจะยังคงดำเนินการอยู่หลังจากการตายของจักรวาล:

try:
    while True:
       sleep(1)
finally:
    print('done')

อย่างไรก็ตามฉันยังคงรอผลดังนั้นโปรดกลับมาตรวจสอบที่นี่ในภายหลัง


5
หรือมีวง จำกัด ในการลองจับ
sapy


27
หลังจากการตายอย่างร้อนแรงของเวลาจักรวาลสิ้นสุดลงดังนั้นsleep(1)แน่นอนว่าจะส่งผลให้เกิดพฤติกรรมที่ไม่ได้กำหนด :-D
David Foerster

คุณอาจต้องการพูดถึง _os.exit โดยตรงหลัง "วิธีเดียวที่จะเอาชนะมันคือการทำให้คอมไพเลอร์หยุดทำงาน" ตอนนี้มีการผสมผสานกันระหว่างตัวอย่างที่ชนะในที่สุด
Stevoisiak

2
@StevenVascellaro ฉันไม่คิดว่ามันจำเป็น - os._exitคือเพื่อวัตถุประสงค์ในทางปฏิบัติทั้งหมดเช่นเดียวกับการกระตุ้นให้เกิดความผิดพลาด (ทางออกที่ไม่สะอาด) sys.exitวิธีที่ถูกต้องออกไปเป็น
Wim

9

ตามเอกสาร Python :

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

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


8

ใช่และไม่ใช่

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

(สิ่งที่คุณสามารถควบคุมตัวเองได้เพียงแค่เรียกใช้รหัสในคำถามของคุณ)

กรณีเดียวที่ฉันสามารถจินตนาการได้ว่าจะไม่ดำเนินการบล็อกในที่สุดเมื่อใดก็คือเมื่อตัวแปลภาษา Python ขัดข้องเช่นภายในรหัส C หรือเนื่องจากไฟฟ้าดับ


ฮ่า.. หรือมีการลองจับที่ไม่สิ้นสุด
sapy

ฉันคิดว่า "ดีใช่และไม่ใช่" ถูกต้องที่สุด สุดท้าย: ชนะเสมอโดยที่ "เสมอ" หมายความว่าล่ามสามารถทำงานได้และโค้ดสำหรับ "ในที่สุด:" ยังคงใช้งานได้และ "ชนะ" ถูกกำหนดไว้เนื่องจากล่ามจะพยายามเรียกใช้ในที่สุด: บล็อกและจะสำเร็จ นั่นคือ "ใช่" และเป็นเงื่อนไขมาก "ไม่" เป็นวิธีการทั้งหมดที่ล่ามอาจหยุดก่อน "ในที่สุด:" - ไฟฟ้าดับ, ฮาร์ดแวร์ล้มเหลว, ฆ่า -9 โดยมุ่งเป้าไปที่ล่าม, ข้อผิดพลาดในล่ามหรือรหัสขึ้นอยู่กับวิธีอื่น ๆ ในการแขวนล่าม และวิธีแขวนภายใน "ในที่สุด:"
Bill IV

1

ฉันพบสิ่งนี้โดยไม่ใช้ฟังก์ชันเครื่องกำเนิดไฟฟ้า:

import multiprocessing
import time

def fun(arg):
  try:
    print("tried " + str(arg))
    time.sleep(arg)
  finally:
    print("finally cleaned up " + str(arg))
  return foo

list = [1, 2, 3]
multiprocessing.Pool().map(fun, list)

การนอนหลับอาจเป็นรหัสใด ๆ ที่อาจทำงานในช่วงเวลาที่ไม่สอดคล้องกัน

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

นอกจากนี้หากคุณเพิ่มบรรทัดbar = bazzหลังจากการโทร sleep () ในบล็อก try จากนั้นกระบวนการแรกที่ไปถึงบรรทัดนั้นจะแสดงข้อยกเว้น (เนื่องจากไม่ได้กำหนด bazz ไว้) ซึ่งทำให้บล็อกของตัวเองถูกเรียกใช้ในที่สุด แต่จากนั้นก็ฆ่าแผนที่ทำให้บล็อกการลองอื่นหายไปโดยไม่ถึงบล็อกในที่สุดและ กระบวนการแรกที่จะไม่ไปถึงคำสั่งส่งคืนเช่นกัน

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


-2

ภาคผนวกของคำตอบที่ยอมรับเพียงเพื่อช่วยในการดูว่ามันทำงานอย่างไรโดยมีตัวอย่างบางส่วน:

  • นี้:

     try:
         1
     except:
         print 'except'
     finally:
         print 'finally'

    จะส่งออก

    ในที่สุด

  •    try:
           1/0
       except:
           print 'except'
       finally:
           print 'finally'

    จะส่งออก

    ยกเว้น
    ในที่สุด


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