Liệu 'cuối cùng' luôn luôn thực thi trong Python?


126

Đối với bất kỳ khối thử cuối cùng nào có thể có trong Python, có đảm bảo rằng finallykhối sẽ luôn được thực thi không?

Ví dụ: giả sử tôi trở lại trong khi ở trong một exceptkhối:

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

Hoặc có thể tôi nâng lại Exception:

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

Thử nghiệm cho thấy điều finallyđó được thực hiện cho các ví dụ trên, nhưng tôi tưởng tượng có những kịch bản khác mà tôi chưa từng nghĩ tới.

Có kịch bản nào trong đó một finallykhối có thể không thực thi trong Python không?


18
Trường hợp duy nhất tôi có thể tưởng tượng là finallykhông thực hiện hoặc "đánh bại mục đích của nó" là trong một vòng lặp vô hạn, sys.exithoặc một sự gián đoạn bắt buộc. Các tài liệu nói rằng finallyluôn luôn được thực thi, vì vậy tôi đi với điều đó.
Xay

1
Một chút suy nghĩ bên lề và chắc chắn không phải những gì bạn đã hỏi, nhưng tôi khá chắc chắn rằng nếu bạn mở Trình quản lý tác vụ và giết tiến trình, finallysẽ không chạy. Hoặc tương tự nếu máy tính gặp sự cố trước: D
Alejandro

144
finallysẽ không thực hiện nếu dây nguồn bị rách khỏi tường.
dùng253751

3
Bạn có thể quan tâm đến câu trả lời này cho cùng một câu hỏi về C #: stackoverflow.com/a/10260233/88656
Eric Lippert

1
Chặn nó trên một semaphore trống. Không bao giờ báo hiệu nó. Làm xong.
Martin James

Câu trả lời:


206

"Đảm bảo" là một từ mạnh mẽ hơn nhiều so với bất kỳ việc thực hiện finallyxứng đáng. Điều được đảm bảo là nếu thực thi chảy ra khỏi toàn bộ try- finallycấu trúc, nó sẽ chuyển qua finallyđể thực hiện. Điều không được đảm bảo là việc thực thi sẽ chảy ra khỏi try- finally.

  • Một finallytrong máy phát điện hoặc coroutine async có thể không bao giờ chạy , nếu đối tượng không bao giờ thực hiện để kết luận. Có rất nhiều cách có thể xảy ra; đây là một:

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

    Lưu ý rằng ví dụ này hơi phức tạp: khi trình tạo được thu gom rác, Python cố chạy finallykhối bằng cách ném GeneratorExitngoại lệ, nhưng ở đây chúng ta bắt gặp ngoại lệ đó và sau đó yield, một lần nữa Python in cảnh báo ("trình tạo bỏ qua GeneratorExit ") Và bỏ cuộc. Xem PEP 342 (Quân đoàn thông qua Máy phát điện nâng cao) để biết chi tiết.

    Các cách khác mà một trình tạo hoặc coroutine có thể không thực thi để kết luận bao gồm nếu đối tượng không bao giờ bị GC (vâng, điều đó là có thể, ngay cả trong CPython), hoặc nếu một async with awaits trong __aexit__, hoặc nếu đối tượng awaits hoặc yields trong một finallykhối. Danh sách này không có ý định đầy đủ.

  • Một finallyluồng trong daemon có thể không bao giờ thực thi nếu tất cả các luồng không phải daemon thoát ra trước.

  • os._exitsẽ dừng quá trình ngay lập tức mà không thực hiện finallycác khối.

  • os.forkcó thể khiến finallycác khối thực thi hai lần . Cũng như các vấn đề bình thường mà bạn mong đợi từ những điều xảy ra hai lần, điều này có thể gây ra xung đột truy cập đồng thời (sự cố, quầy hàng, ...) nếu quyền truy cập vào tài nguyên được chia sẻ không được đồng bộ hóa chính xác .

    multiprocessingsử dụng fork-without-exec để tạo các quy trình worker khi sử dụng phương thức fork start (mặc định trên Unix), sau đó gọi os._exitcông nhân một khi công việc của công nhân hoàn thành finallymultiprocessingtương tác có thể gặp vấn đề ( ví dụ ).

  • Một lỗi phân đoạn cấp độ C sẽ ngăn finallycác khối chạy.
  • kill -SIGKILLsẽ ngăn chặn finallycác khối chạy. SIGTERMSIGHUPcũng sẽ ngăn finallycác khối chạy trừ khi bạn cài đặt trình xử lý để tự kiểm soát việc tắt máy; theo mặc định, Python không xử lý SIGTERMhoặc SIGHUP.
  • Một ngoại lệ trong finallycó thể ngăn chặn dọn dẹp hoàn thành. Một trường hợp đặc biệt đáng chú ý là nếu người dùng nhấn control-C giống như chúng ta đang bắt đầu thực thi finallykhối. Python sẽ nâng cao KeyboardInterruptvà bỏ qua mọi dòng finallynội dung của khối. ( KeyboardInterruptMã an toàn rất khó viết).
  • Nếu máy tính bị mất điện hoặc nếu nó ngủ đông và không thức dậy, finallycác khối sẽ không chạy.

Các finallykhối không phải là một hệ thống giao dịch; nó không cung cấp bảo đảm nguyên tử hoặc bất cứ thứ gì thuộc loại này. Một số ví dụ này có vẻ rõ ràng, nhưng thật dễ để quên những điều như vậy có thể xảy ra và dựa vào finallyquá nhiều.


14
Tôi tin rằng chỉ điểm đầu tiên trong danh sách của bạn là thực sự có liên quan, và có một cách dễ dàng để tránh nó: 1) không bao giờ sử dụng trần exceptvà không bao giờ bắt GeneratorExittrong máy phát điện. Những điểm về chủ đề / giết quá trình / segfaulting / tắt nguồn được mong đợi, trăn không thể làm phép thuật. Ngoài ra: ngoại lệ trong finallyrõ ràng là một vấn đề nhưng điều này không thay đổi thực tế là luồng điều khiển đã được chuyển sang finallykhối. Về Ctrl+C, bạn có thể thêm một trình xử lý tín hiệu mà bỏ qua nó, hoặc đơn giản là "lập lịch" tắt máy sau khi hoàn thành thao tác hiện tại.
Giacomo Alzetta

8
Việc đề cập đến kill -9 là đúng về mặt kỹ thuật, nhưng hơi bất công. Không có chương trình nào được viết bằng bất kỳ ngôn ngữ nào chạy bất kỳ mã nào khi nhận lệnh kill -9. Trong thực tế, không có chương trình bao giờ nhận được một kill -9 ở tất cả, vì vậy ngay cả nếu nó muốn, nó không thể thực hiện bất cứ điều gì. Đó là toàn bộ điểm giết -9.
Tom

10
@Tom: Quan điểm về việc kill -9không chỉ định ngôn ngữ. Và thẳng thắn, nó cần lặp lại, bởi vì nó nằm trong một điểm mù. Quá nhiều người quên hoặc không nhận ra rằng chương trình của họ có thể bị ngừng hoạt động mà không được phép dọn dẹp.
cHao

5
@GiacomoAlzetta: Có những người ngoài kia dựa vào finallycác khối như thể họ cung cấp đảm bảo giao dịch. Có vẻ như rõ ràng là họ không, nhưng đó không phải là điều ai cũng nhận ra. Đối với trường hợp máy phát điện, có rất nhiều cách mà một máy phát điện hoàn toàn không thể được tạo ra, và rất nhiều cách mà một máy phát điện hoặc coroutine có thể vô tình mang lại sau khi GeneratorExitnó không bắt được GeneratorExit, ví dụ như nếu bị async withđình chỉ một coroutine trong __exit__.
user2357112 hỗ trợ Monica

2
@ user2357112 yeah - Tôi đã cố gắng trong nhiều thập kỷ để có được các nhà phát triển để dọn sạch các tệp tạm thời, v.v. khi khởi động ứng dụng, không thoát ra. Dựa vào cái gọi là 'dọn dẹp và chấm dứt duyên dáng', đang yêu cầu sự thất vọng và nước mắt :)
Martin James

69

Đúng. Cuối cùng luôn luôn chiến thắng.

Cách duy nhất để đánh bại nó là tạm dừng thực thi trước khi finally:có cơ hội thực thi (ví dụ: làm hỏng trình thông dịch, tắt máy tính của bạn, tạm dừng máy phát điện mãi mãi).

Tôi tưởng tượng có những kịch bản khác mà tôi chưa từng nghĩ đến.

Dưới đây là một vài điều bạn có thể không nghĩ tới:

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'

Tùy thuộc vào cách bạn thoát trình thông dịch, đôi khi bạn có thể "hủy" cuối cùng, nhưng không như thế này:

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

Sử dụng sự bấp bênh os._exit(theo ý kiến ​​của tôi, điều này nằm trong "sự cố trình thông dịch"):

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

Tôi hiện đang chạy mã này, để kiểm tra xem cuối cùng có còn thực thi sau cái chết nóng của vũ trụ hay không:

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

Tuy nhiên, tôi vẫn đang chờ kết quả, vì vậy hãy kiểm tra lại ở đây sau.


5
hoặc có một vòng lặp hữu hạn i trong thử bắt
sacco

8
finallytrong một máy phát điện hoặc coroutine có thể dễ dàng không thực thi được , mà không đi đến bất cứ nơi nào gần điều kiện "sự cố trình thông dịch".
user2357112 hỗ trợ Monica

27
Sau cái chết nhiệt của thời gian vũ trụ không còn tồn tại, do đó sleep(1)chắc chắn sẽ dẫn đến hành vi không xác định. :-D
David Foerster

Bạn có thể muốn đề cập trực tiếp đến _os.exit sau khi cách duy nhất để đánh bại nó là đánh sập trình biên dịch. Ngay bây giờ, nó là một ví dụ hỗn hợp trong đó cuối cùng là chiến thắng.
Stevoisiak

2
@StevenVascellaro Tôi không nghĩ điều đó là cần thiết - os._exitlà, cho tất cả các mục đích thực tế, giống như gây ra một vụ tai nạn (lối thoát ô uế). Cách chính xác để thoát là sys.exit.
wim

9

Theo tài liệu Python :

Bất kể điều gì đã xảy ra trước đó, khối cuối cùng được thực thi khi khối mã hoàn tất và mọi trường hợp ngoại lệ được xử lý. Ngay cả khi có lỗi trong trình xử lý ngoại lệ hoặc khối khác và một ngoại lệ mới được đưa ra, mã trong khối cuối cùng vẫn chạy.

Cũng cần lưu ý rằng nếu có nhiều câu lệnh return, bao gồm một câu lệnh trong khối cuối cùng, thì câu trả về khối cuối cùng là câu lệnh duy nhất sẽ thực thi.


8

Vâng, có và không.

Điều được đảm bảo là Python sẽ luôn cố gắng thực thi khối cuối cùng. Trong trường hợp bạn trở về từ khối hoặc đưa ra một ngoại lệ chưa được lưu, khối cuối cùng được thực thi ngay trước khi thực sự quay lại hoặc tăng ngoại lệ.

(những gì bạn có thể tự kiểm soát bằng cách chạy mã trong câu hỏi của bạn)

Trường hợp duy nhất tôi có thể tưởng tượng nơi khối cuối cùng sẽ không được thực thi là khi chính trình thông dịch Python gặp sự cố, ví dụ bên trong mã C hoặc do mất điện.


ha ha .. hoặc có một vòng lặp vô hạn trong thử bắt
sacco

Tôi nghĩ "Vâng, có và không" là chính xác nhất. Cuối cùng: luôn thắng trong đó "luôn luôn" có nghĩa là trình thông dịch có thể chạy và mã cho "cuối cùng:" vẫn có sẵn và "thắng" được định nghĩa là trình thông dịch sẽ cố gắng chạy khối cuối cùng: nó sẽ thành công. Đó là "Có" và nó rất có điều kiện. "Không" là tất cả các cách mà trình thông dịch có thể dừng trước khi "cuối cùng:" - mất điện, lỗi phần cứng, giết -9 nhằm vào trình thông dịch, lỗi trong trình thông dịch hoặc mã mà nó phụ thuộc vào, các cách khác để treo trình thông dịch. Và cách để treo bên trong "cuối cùng:".
Bill IV

1

Tôi tìm thấy cái này mà không sử dụng chức năng tạo:

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)

Giấc ngủ có thể là bất kỳ mã nào có thể chạy trong khoảng thời gian không nhất quán.

Điều dường như đang xảy ra ở đây là quá trình song song đầu tiên để kết thúc thành công khối thử, nhưng sau đó cố gắng trả về từ hàm một giá trị (foo) chưa được xác định ở bất kỳ đâu, điều này gây ra ngoại lệ. Ngoại lệ đó giết chết bản đồ mà không cho phép các quá trình khác tiếp cận các khối cuối cùng của chúng.

Ngoài ra, nếu bạn thêm dòng bar = bazzngay sau lệnh gọi ngủ () trong khối thử. Sau đó, quá trình đầu tiên để đạt được dòng đó sẽ tạo ra một ngoại lệ (vì bazz không được xác định), điều này khiến khối cuối cùng của chính nó được chạy, nhưng sau đó giết chết bản đồ, khiến các khối thử khác biến mất mà không đến được khối cuối cùng của chúng và quá trình đầu tiên không đạt được tuyên bố trở lại của nó, một trong hai.

Điều này có nghĩa gì đối với đa xử lý Python là bạn không thể tin vào cơ chế xử lý ngoại lệ để dọn sạch tài nguyên trong tất cả các quy trình nếu ngay cả một trong các quy trình có thể có ngoại lệ. Xử lý tín hiệu bổ sung hoặc quản lý tài nguyên bên ngoài cuộc gọi bản đồ đa xử lý sẽ là cần thiết.


-2

Phụ lục cho câu trả lời được chấp nhận, chỉ để giúp xem cách thức hoạt động của nó, với một vài ví dụ:

  • Điều này:

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

    sẽ xuất

    cuối cùng

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

    sẽ xuất

    ngoại trừ
    cuối cùng


Điều này không trả lời câu hỏi nào cả. Nó chỉ có ví dụ khi nó hoạt động .
zixuan
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.