Menangkap pengecualian saat menggunakan pernyataan 'with' Python

293

Sayangnya, saya tidak tahu cara menangani pengecualian untuk pernyataan python 'with'. Jika saya punya kode:

with open("a.txt") as f:
    print f.readlines()

Saya benar-benar ingin menangani 'file not found exception' untuk melakukan sesuatu. Tetapi saya tidak bisa menulis

with open("a.txt") as f:
    print f.readlines()
except:
    print 'oops'

dan tidak bisa menulis

with open("a.txt") as f:
    print f.readlines()
else:
    print 'oops'

melampirkan 'dengan' dalam percobaan / kecuali pernyataan tidak berfungsi: pengecualian tidak dimunculkan. Apa yang bisa saya lakukan untuk memproses kegagalan di dalam pernyataan 'with' dengan cara Pythonic?

grigoryvp
sumber
Apa maksud Anda "melampirkan 'dengan' dalam percobaan / kecuali pernyataan tidak berfungsi: pengecualian tidak dimunculkan" ? Sebuah withpernyataan tidak ajaib istirahat sekitarnya try...exceptpernyataan.
Aran-Fey
4
Menariknya, Jawa try-dengan-sumber pernyataan tidak mendukung persis kasus penggunaan ini yang Anda inginkan. docs.oracle.com/javase/tutorial/essential/exceptions/…
Nayuki

Jawaban:

256
from __future__ import with_statement

try:
    with open( "a.txt" ) as f :
        print f.readlines()
except EnvironmentError: # parent of IOError, OSError *and* WindowsError where available
    print 'oops'

Jika Anda ingin penanganan berbeda untuk kesalahan dari panggilan terbuka vs kode kerja yang dapat Anda lakukan:

try:
    f = open('foo.txt')
except IOError:
    print('error')
else:
    with f:
        print f.readlines()
Douglas Leeder
sumber
3
Seperti disebutkan dalam stackoverflow.com/questions/5205811/... , blok coba di sini benar-benar terlalu luas. Tidak ada perbedaan yang dibuat antara pengecualian saat membuat manajer konteks dan orang-orang di tubuh pernyataan with, jadi itu mungkin bukan solusi yang valid untuk semua kasus penggunaan.
ncoghlan
@ncoghlan Tapi Anda bisa menambahkan try...exceptblok tambahan di dalam withagar lebih dekat ke sumber pengecualian yang tidak ada hubungannya dengan open().
rbaleksandar
1
@rbaleksandar Jika saya ingat dengan benar, komentar saya mengacu pada contoh pertama dalam jawaban, di mana keseluruhan dengan pernyataan berada di dalam blok coba / kecuali (jadi bahkan jika Anda memiliki blok percobaan dalam / harapan, setiap pengecualian yang dibiarkan lolos akan masih memukul bagian luar). Douglas kemudian menambahkan contoh kedua untuk menangani kasus-kasus di mana perbedaan itu penting.
ncoghlan
3
Apakah file akan ditutup dalam contoh ini? Saya bertanya karena dibuka di luar ruang lingkup "dengan".
Mike Collins
6
@MikeCollins Keluar dari 'with' akan menutup file yang terbuka bahkan ketika file tersebut dibuka sebelum 'with'.
user7938784
75

Cara "Pythonic" terbaik untuk melakukan ini, mengeksploitasi withpernyataan, terdaftar sebagai Contoh # 6 dalam PEP 343 , yang memberikan latar belakang pernyataan tersebut.

@contextmanager
def opened_w_error(filename, mode="r"):
    try:
        f = open(filename, mode)
    except IOError, err:
        yield None, err
    else:
        try:
            yield f, None
        finally:
            f.close()

Digunakan sebagai berikut:

with opened_w_error("/etc/passwd", "a") as (f, err):
    if err:
        print "IOError:", err
    else:
        f.write("guido::0:0::/:/bin/sh\n")
jscs
sumber
38
Saya suka tapi rasanya agak banyak ilmu hitam. Ini tidak sepenuhnya eksplisit untuk pembaca
Paul Seeb
5
@ PaulSeeb Mengapa Anda tidak mendefinisikannya dan menyelamatkan diri dari melakukannya setiap kali Anda perlu? Ini didefinisikan pada tingkat aplikasi Anda, dan itu sama ajaibnya dengan manajer konteks lainnya. Saya pikir seseorang yang menggunakan pernyataan with akan memahaminya dengan jelas (nama fungsi mungkin juga lebih ekspresif jika Anda tidak menyukainya). Pernyataan "with" itu sendiri telah direkayasa untuk berfungsi dengan cara ini, untuk menentukan blok kode "aman" dan mendelegasikan fungsi pemeriksaan ke manajer konteks (untuk membuat kode lebih jelas).
9
Semua masalah ini hanya karena tidak menulis blok terakhir pada kode pengguna. Saya mulai berpikir kita semua menderita untuk sensasi panjang pada pernyataan dengan.
jgomo3
1
Cara terbaik untuk menangani pengecualian dalam python adalah dengan menulis fungsi yang menangkap dan mengembalikannya? Serius? Cara pythonic untuk menangani pengecualian adalah dengan menggunakan try...exceptpernyataan.
Aran-Fey
58

Menangkap pengecualian saat menggunakan pernyataan 'with' Python

Pernyataan with telah tersedia tanpa __future__impor sejak Python 2.6 . Anda bisa mendapatkannya sedini Python 2.5 (tetapi pada titik ini saatnya untuk meningkatkan!) Dengan:

from __future__ import with_statement

Inilah hal terdekat untuk memperbaiki yang Anda miliki. Anda hampir sampai, tetapi withtidak memiliki exceptklausa:

with open("a.txt") as f: 
    print(f.readlines())
except:                    # <- with doesn't have an except clause.
    print('oops')

Metode manajer konteks __exit__, jika dikembalikan Falseakan membangkitkan kembali kesalahan ketika selesai. Jika kembali True, itu akan menekannya. The openbuiltin ini __exit__tidak kembali True, sehingga Anda hanya perlu sarang dalam mencoba, kecuali blok:

try:
    with open("a.txt") as f:
        print(f.readlines())
except Exception as error: 
    print('oops')

Dan standar boilerplate: jangan gunakan yang telanjang except:yang menangkap BaseExceptiondan setiap kemungkinan pengecualian dan peringatan lainnya. Paling tidak sespesifik Exception, dan untuk kesalahan ini, mungkin menangkap IOError. Hanya menangkap kesalahan yang siap Anda tangani.

Jadi dalam hal ini, Anda harus:

>>> try:
...     with open("a.txt") as f:
...         print(f.readlines())
... except IOError as error: 
...     print('oops')
... 
oops
Aaron Hall
sumber
2

Membedakan antara kemungkinan asal-usul pengecualian yang muncul dari withpernyataan majemuk

Membedakan antara pengecualian yang terjadi dalam withpernyataan itu rumit karena mereka bisa berasal dari tempat yang berbeda. Pengecualian dapat diajukan dari salah satu tempat berikut (atau fungsi yang disebut di dalamnya):

  • ContextManager.__init__
  • ContextManager.__enter__
  • tubuh with
  • ContextManager.__exit__

Untuk lebih jelasnya lihat dokumentasi tentang Jenis-jenis Manajer Konteks .

Jika kita ingin membedakan antara kasus-kasus yang berbeda, hanya membungkus withmenjadi try .. excepttidak cukup. Pertimbangkan contoh berikut (menggunakan ValueErrorsebagai contoh tetapi tentu saja itu bisa diganti dengan jenis pengecualian lainnya):

try:
    with ContextManager():
        BLOCK
except ValueError as err:
    print(err)

Di sini surat exceptwasiat akan menangkap pengecualian yang berasal dari keempat tempat yang berbeda dan karenanya tidak memungkinkan untuk membedakan di antara mereka. Jika kita memindahkan instantiasi objek manajer konteks di luar with, kita dapat membedakan antara __init__dan BLOCK / __enter__ / __exit__:

try:
    mgr = ContextManager()
except ValueError as err:
    print('__init__ raised:', err)
else:
    try:
        with mgr:
            try:
                BLOCK
            except TypeError:  # catching another type (which we want to handle here)
                pass
    except ValueError as err:
        # At this point we still cannot distinguish between exceptions raised from
        # __enter__, BLOCK, __exit__ (also BLOCK since we didn't catch ValueError in the body)
        pass

Secara efektif ini hanya membantu __init__bagian tetapi kita dapat menambahkan variabel sentinel tambahan untuk memeriksa apakah tubuh yang withmulai dieksekusi (yaitu membedakan antara __enter__dan yang lain):

try:
    mgr = ContextManager()  # __init__ could raise
except ValueError as err:
    print('__init__ raised:', err)
else:
    try:
        entered_body = False
        with mgr:
            entered_body = True  # __enter__ did not raise at this point
            try:
                BLOCK
            except TypeError:  # catching another type (which we want to handle here)
                pass
    except ValueError as err:
        if not entered_body:
            print('__enter__ raised:', err)
        else:
            # At this point we know the exception came either from BLOCK or from __exit__
            pass

Bagian yang sulit adalah untuk membedakan antara pengecualian yang berasal dari BLOCKdan __exit__karena pengecualian yang lolos dari tubuh withakan diteruskan ke __exit__yang dapat memutuskan bagaimana menanganinya (lihat dokumen ). Namun jika __exit__memunculkan sendiri, pengecualian asli akan digantikan oleh yang baru. Untuk menangani kasus-kasus ini kita dapat menambahkan exceptklausa umum dalam tubuh withuntuk menyimpan setiap pengecualian potensial yang seharusnya lolos tanpa diketahui dan membandingkannya dengan yang tertangkap di terluar exceptnanti - jika mereka sama, ini berarti asal usulnya. BLOCKatau sebaliknya __exit__(dalam kasus __exit__menekan pengecualian dengan mengembalikan nilai sebenarnya yang paling luarexcept tidak akan dieksekusi).

try:
    mgr = ContextManager()  # __init__ could raise
except ValueError as err:
    print('__init__ raised:', err)
else:
    entered_body = exc_escaped_from_body = False
    try:
        with mgr:
            entered_body = True  # __enter__ did not raise at this point
            try:
                BLOCK
            except TypeError:  # catching another type (which we want to handle here)
                pass
            except Exception as err:  # this exception would normally escape without notice
                # we store this exception to check in the outer `except` clause
                # whether it is the same (otherwise it comes from __exit__)
                exc_escaped_from_body = err
                raise  # re-raise since we didn't intend to handle it, just needed to store it
    except ValueError as err:
        if not entered_body:
            print('__enter__ raised:', err)
        elif err is exc_escaped_from_body:
            print('BLOCK raised:', err)
        else:
            print('__exit__ raised:', err)

Pendekatan alternatif menggunakan formulir setara yang disebutkan dalam PEP 343

PEP 343 - Pernyataan "with" menentukan versi withpernyataan "non-with" yang setara . Di sini kita dapat dengan mudah membungkus berbagai bagian dengan try ... exceptdan dengan demikian membedakan antara sumber kesalahan potensial yang berbeda:

import sys

try:
    mgr = ContextManager()
except ValueError as err:
    print('__init__ raised:', err)
else:
    try:
        value = type(mgr).__enter__(mgr)
    except ValueError as err:
        print('__enter__ raised:', err)
    else:
        exit = type(mgr).__exit__
        exc = True
        try:
            try:
                BLOCK
            except TypeError:
                pass
            except:
                exc = False
                try:
                    exit_val = exit(mgr, *sys.exc_info())
                except ValueError as err:
                    print('__exit__ raised:', err)
                else:
                    if not exit_val:
                        raise
        except ValueError as err:
            print('BLOCK raised:', err)
        finally:
            if exc:
                try:
                    exit(mgr, None, None, None)
                except ValueError as err:
                    print('__exit__ raised:', err)

Biasanya pendekatan yang lebih sederhana akan baik-baik saja

Kebutuhan untuk penanganan pengecualian khusus semacam itu harus sangat jarang dan biasanya membungkus keseluruhan withdalam satu try ... exceptblok sudah cukup. Terutama jika berbagai sumber kesalahan ditunjukkan oleh tipe pengecualian (khusus) yang berbeda (manajer konteks perlu dirancang sesuai) kita dapat dengan mudah membedakannya. Sebagai contoh:

try:
    with ContextManager():
        BLOCK
except InitError:  # raised from __init__
    ...
except AcquireResourceError:  # raised from __enter__
    ...
except ValueError:  # raised from BLOCK
    ...
except ReleaseResourceError:  # raised from __exit__
    ...
seorang tamu
sumber