Làm thế nào để dọn dẹp thử lồng / ngoại trừ / khác?


8

Khi viết mã, tôi thường muốn làm một cái gì đó như thế này:

try:
    foo()
except FooError:
    handle_foo()
else:
    try:
        bar()
    except BarError:
        handle_bar()
    else:
        try:
            baz()
        except BazError:
            handle_baz()
        else:
            qux()
finally:
    cleanup()

Rõ ràng, điều này là hoàn toàn không thể đọc được. Nhưng nó thể hiện một ý tưởng tương đối đơn giản: thực thi một loạt các hàm (hoặc đoạn mã ngắn), với một trình xử lý ngoại lệ cho mỗi hàm và dừng ngay khi một hàm bị lỗi. Tôi tưởng tượng Python có thể cung cấp đường cú pháp cho mã này, có lẽ giống như thế này:

# NB: This is *not* valid Python
try:
    foo()
except FooError:
    handle_foo()
    # GOTO finally block
else try:
    bar()
except BarError:
    handle_bar()
    # ditto
else try:
    baz()
except BazError:
    handle_baz()
    # ditto
else:
    qux()
finally:
    cleanup()

Nếu không có ngoại lệ được nêu ra, điều này tương đương với foo();bar();baz();qux();cleanup(). Nếu ngoại lệ được nêu ra, chúng được xử lý bởi trình xử lý ngoại lệ thích hợp (nếu có) và chúng tôi bỏ qua cleanup(). Cụ thể, nếu bar()tăng một FooErrorhoặc BazError, ngoại lệ sẽ không bị bắt và sẽ lan truyền đến người gọi. Điều này là mong muốn vì vậy chúng tôi chỉ nắm bắt các ngoại lệ mà chúng tôi thực sự mong đợi để xử lý.

Bất kể sự xấu xí cú pháp, loại mã này chỉ là một ý tưởng tồi nói chung? Nếu vậy, làm thế nào bạn sẽ cấu trúc lại nó? Tôi tưởng tượng các nhà quản lý bối cảnh có thể được sử dụng để hấp thụ một số sự phức tạp, nhưng tôi không thực sự hiểu nó sẽ hoạt động như thế nào trong trường hợp chung.


Những loại điều bạn đang làm trong các handle_*chức năng?
Winston Ewert

Câu trả lời:


8
try:
    foo()
except FooError:
    handle_foo()
else:
    ...
finally:
    cleanup()

Không gì handle_foolàm gì? Có một vài điều chúng ta thường làm trong các khối xử lý ngoại lệ.

  1. Dọn dẹp sau lỗi: Nhưng trong trường hợp này, foo () nên tự dọn dẹp, không để chúng tôi làm như vậy. Ngoài ra, hầu hết các công việc dọn dẹp được xử lý tốt nhất vớiwith
  2. Khôi phục lại con đường hạnh phúc: Nhưng bạn không làm điều này khi bạn không tiếp tục với các chức năng còn lại.
  3. Dịch loại ngoại lệ: Nhưng bạn không ném ngoại lệ khác
  4. Ghi lại lỗi: Nhưng không cần phải có các khối ngoại lệ đặc biệt cho từng loại.

Dường như với tôi rằng bạn đang làm điều gì đó kỳ quặc trong việc xử lý ngoại lệ của bạn. Câu hỏi của bạn ở đây đơn giản là một triệu chứng về việc sử dụng các ngoại lệ theo một cách khác thường. Bạn không rơi vào khuôn mẫu điển hình, và đó là lý do tại sao điều này trở nên khó xử.

Không có ý tưởng tốt hơn về những gì bạn đang làm trong các handle_chức năng đó là tất cả những gì tôi có thể nói.


Đây chỉ là mã mẫu. Các handle_hàm có thể dễ dàng là các đoạn mã ngắn (ghi) ghi lại lỗi và trả về các giá trị dự phòng, hoặc đưa ra các ngoại lệ mới hoặc thực hiện bất kỳ số lượng nào khác. Trong khi đó, các hàm foo(), bar()v.v. cũng có thể là các đoạn mã ngắn. Chẳng hạn, chúng ta có thể có spam[eggs]và cần phải nắm bắt KeyError.
Kevin

1
@Kevin, đúng, nhưng điều đó không thay đổi câu trả lời của tôi. Nếu xử lý của bạn returned hoặc raised bất cứ điều gì, vấn đề bạn đề cập sẽ không phát sinh. Phần còn lại của chức năng sẽ được bỏ qua tự động. Trong thực tế, một cách đơn giản để giải quyết vấn đề của bạn sẽ là trả về trong tất cả các khối ngoại lệ. Mã này rất khó xử vì bạn không thực hiện bảo lãnh thông thường hoặc tăng để cho biết rằng bạn đang từ bỏ.
Winston Ewert

Một câu trả lời tốt về tổng thể, nhưng tôi không đồng ý với quan điểm 1 của bạn - bằng chứng nào có foonên làm việc dọn dẹp của chính nó? foođang báo hiệu rằng đã xảy ra sự cố và không biết quy trình dọn dẹp chính xác là gì.
Ethan Furman

@EthanFurman, tôi nghĩ rằng chúng ta có thể nghĩ về các mặt hàng khác nhau dưới tiêu đề dọn dẹp. Tôi hình dung nếu foo mở tệp, kết nối cơ sở dữ liệu, cấp phát bộ nhớ, v.v. thì trách nhiệm của foo là đảm bảo những tệp đó được đóng / giải phóng trước khi quay trở lại. Đó là những gì tôi có nghĩa là dọn dẹp.
Winston Ewert

Ah, trong trường hợp đó tôi hoàn toàn đồng ý với bạn. :)
Ethan Furman

3

Có vẻ như bạn có một chuỗi các lệnh có thể đưa ra một ngoại lệ cần được xử lý trước khi quay trở lại. Hãy thử nhóm mã của bạn và xử lý ngoại lệ ở các vị trí riêng biệt. Tôi tin rằng đây là những gì bạn có ý định.

try:
    foo()
    bar()
    baz()
    qux()

except FooError:
    handle_foo()
except BarError:
    handle_bar()
except BazError:
    handle_baz()

finally:
    cleanup()

2
Tôi đã được dạy để cố gắng tránh đặt nhiều mã hơn mức cần thiết trong try:khối. Trong trường hợp này, tôi sẽ lo lắng về bar()việc tăng một FooError, sẽ được xử lý không chính xác với mã này.
Kevin

2
Điều này có thể hợp lý nếu tất cả các phương pháp đưa ra các ngoại lệ rất cụ thể, nhưng điều đó thường không đúng. Tôi rất muốn giới thiệu chống lại phương pháp này.
Winston Ewert

@Kevin Việc gói từng dòng mã trong khối Thử Bắt lại nhanh chóng trở nên không thể đọc được. Điều này được thực hiện nhiều hơn bởi các quy tắc thụt lề của Python. Hãy nghĩ xem điều này sẽ kết thúc thụt lề bao xa nếu mã dài 10 dòng. Nó sẽ có thể đọc hoặc duy trì. Miễn là mã tuân theo trách nhiệm duy nhất, tôi sẽ đứng theo ví dụ trên. Nếu không, thì mã nên được viết lại.
BillThor

@WinstonEwert Trong một ví dụ thực tế, tôi sẽ mong đợi một số trùng lặp trong các Lỗi (ngoại lệ). Tuy nhiên, tôi cũng mong muốn việc xử lý là như nhau. BazError có thể là một ngắt bàn phím. Tôi hy vọng nó sẽ được xử lý thích hợp cho cả bốn dòng mã.
BillThor

1

Có một vài cách khác nhau, tùy thuộc vào những gì bạn cần.

Đây là một cách với các vòng lặp:

try:
    for func, error, err_handler in (
            (foo, FooError, handle_foo),
            (bar, BarError, handle_bar),
            (baz, BazError, handle_baz),
        ):
        try:
            func()
        except error:
            err_handler()
            break
finally:
    cleanup()

Đây là một cách với một lối ra sau error_handler:

def some_func():
    try:
        try:
            foo()
        except FooError:
            handle_foo()
            return
        try:
            bar()
        except BarError:
            handle_bar()
            return
        try:
            baz()
        except BazError:
            handle_baz()
            return
        else:
            qux()
    finally:
        cleanup()

Cá nhân tôi nghĩ rằng phiên bản vòng lặp dễ đọc hơn.


IMHO đây là một giải pháp tốt hơn giải pháp được chấp nhận vì nó 1) cố gắng trả lời và 2) làm cho trình tự rất rõ ràng.
bob

0

Trước hết, việc sử dụng phù hợp withthường có thể làm giảm hoặc thậm chí loại bỏ rất nhiều mã xử lý ngoại lệ, cải thiện cả khả năng bảo trì và khả năng đọc.

Bây giờ, bạn có thể giảm việc làm tổ theo nhiều cách; các áp phích khác đã cung cấp một vài, vì vậy đây là biến thể của riêng tôi:

for _ in range(1):
    try:
        foo()
    except FooError:
        handle_foo()
        break
    try:
        bar()
    except BarError:
        handle_bar()
        break
    try:
        baz()
    except BazError:
        handle_baz()
        break
    qux()
cleanup()
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.