Captura de uma exceção ao usar uma instrução Python 'with'

293

Para minha vergonha, não consigo descobrir como lidar com a exceção da instrução python 'with'. Se eu tiver um código:

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

Eu realmente quero lidar com 'arquivo não encontrado exceção' para fazer algo. Mas eu não sei escrever

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

e não pode escrever

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

incluir 'with' em uma instrução try / except não funciona mais: a exceção não é gerada. O que posso fazer para processar a falha dentro da declaração 'with' de uma maneira pitônica?

grigoryvp
fonte
O que você quer dizer com "anexar 'com' em uma instrução try / except não funciona mais: a exceção não é gerada" ? Uma withdeclaração não magicamente quebra uma try...exceptdeclaração circundante .
Aran-Fey
4
Curiosamente, try-with-recursos do Java faz suportar exatamente este caso de uso que quiser. docs.oracle.com/javase/tutorial/essential/exceptions/…
Nayuki 15/07/18

Respostas:

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'

Se você deseja um tratamento diferente para erros da chamada aberta versus o código de trabalho, você pode:

try:
    f = open('foo.txt')
except IOError:
    print('error')
else:
    with f:
        print f.readlines()
Douglas Leeder
fonte
3
Conforme observado em stackoverflow.com/questions/5205811/… , o bloco try aqui é realmente muito amplo. Nenhuma distinção é feita entre exceções ao criar o gerenciador de contexto e aquelas no corpo da instrução with, portanto, pode não ser uma solução válida para todos os casos de uso.
Ncoghlan 6/03
@ ncoghlan Mas você pode adicionar try...exceptblocos extras withpara ficar mais perto da fonte de uma exceção que não tem nada a ver open().
Rbaleksandar
1
@rbaleksandar Se bem me lembro, meu comentário foi estritamente referente ao primeiro exemplo da resposta, onde toda a declaração with está dentro do bloco try / except (portanto, mesmo se você tiver blocos try / expect internos, qualquer exceção que eles permitirem escapar será ainda atingiu o exterior). Douglas posteriormente adicionou o segundo exemplo para tratar de casos em que essa distinção importa.
precisa saber é o seguinte
3
O arquivo será fechado neste exemplo? Eu pergunto porque foi aberto fora do escopo "with".
Mike Collins
6
@MikeCollins Sair do 'com' fechará o arquivo aberto, mesmo quando o arquivo estiver aberto antes do 'com'.
user7938784
75

A melhor maneira "Pythonic" de fazer isso, explorando a withdeclaração, é listada como Exemplo # 6 no PEP 343 , que fornece os antecedentes da declaração.

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

Utilizado da seguinte forma:

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
fonte
38
Eu gosto, mas parece muita magia negra. A sua não é inteiramente explícito para o leitor
Paul Seeb
5
@PaulSeeb Por que você não o define e evita fazer isso toda vez que precisa? É definido no nível do seu aplicativo e é tão mágico quanto qualquer outro gerenciador de contexto. Eu acho que alguém usando a instrução with entenderia claramente (o nome da função também pode ser mais expressivo se você não gostar). A própria declaração "with" foi projetada para funcionar dessa maneira, para definir um bloco de código "seguro" e delegar funções de verificação aos gerenciadores de contexto (para tornar o código mais claro).
9
Todo esse problema apenas por não escrever o bloco finalmente no código do usuário. Estou começando a pensar que todos sofremos por um longo sintoma de hype na declaração with.
Jgomo3 2/17
1
A melhor maneira de lidar com exceções em python é escrever uma função que as captura e as retorna? Seriamente? A maneira pitônica de lidar com exceções é usar uma try...exceptdeclaração.
Aran-Fey
58

Captura de uma exceção ao usar uma instrução Python 'with'

A instrução with está disponível sem a __future__importação desde o Python 2.6 . Você pode obtê-lo já no Python 2.5 (mas, agora, é hora de atualizar!) Com:

from __future__ import with_statement

Aqui está a coisa mais próxima a ser corrigida. Você está quase lá, mas withnão tem uma exceptcláusula:

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

O __exit__método de um gerenciador de contexto , se ele retornar, Falseaumentará o erro ao concluir. Se retornar True, ele será suprimido. O openbuiltin __exit__não retorna True, então você só precisa aninhar em uma tentativa, exceto o bloco:

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

E clichê padrão: não use nada except:que capte BaseExceptione todas as outras exceções e avisos possíveis. Seja pelo menos tão específico quanto Exception, e, para esse erro, talvez pegue IOError. Capture apenas os erros que você está preparado para lidar.

Portanto, neste caso, você faria:

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

Diferenciando entre as possíveis origens de exceções levantadas de uma withdeclaração composta

A diferenciação entre exceções que ocorrem em uma withinstrução é complicada porque elas podem se originar em lugares diferentes. Exceções podem ser geradas a partir de um dos seguintes locais (ou funções aqui denominadas):

  • ContextManager.__init__
  • ContextManager.__enter__
  • o corpo do with
  • ContextManager.__exit__

Para mais detalhes, consulte a documentação sobre tipos de gerenciador de contexto .

Se quisermos distinguir entre esses casos diferentes, apenas agrupar o withem a try .. exceptnão é suficiente. Considere o seguinte exemplo (usando ValueErrorcomo exemplo, mas é claro que poderia ser substituído por qualquer outro tipo de exceção):

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

Aqui, exceptserão capturadas exceções originadas em todos os quatro locais diferentes e, portanto, não permitem distinguir entre eles. Se movermos a instanciação do objeto do gerenciador de contexto para fora do with, poderemos distinguir entre __init__e 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

Efetivamente, isso apenas ajudou na __init__parte, mas podemos adicionar uma variável sentinela extra para verificar se o corpo do withcomeçou a executar (ou seja, diferenciar entre __enter__os outros):

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

A parte complicada é diferenciar as exceções originadas BLOCKe __exit__porque uma exceção que escapa ao corpo do withserá transmitida para __exit__que possa decidir como lidar com isso (consulte os documentos ). Se, no entanto __exit__, se elevar, a exceção original será substituída pela nova. Para lidar com esses casos, podemos adicionar uma exceptcláusula geral no corpo do witharquivo para armazenar qualquer exceção em potencial que, de outra forma, passaria despercebida e compará-la com a capturada mais excepttarde no exterior - se forem iguais, significa que a origem foi BLOCKou de outra forma era __exit__(no caso, __exit__suprime a exceção retornando um valor verdadeiro o mais externoexcept simplesmente não será executado).

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)

Abordagem alternativa usando o formulário equivalente mencionado no PEP 343

PEP 343 - A declaração "with" especifica uma versão equivalente "non-with" da withdeclaração. Aqui, podemos prontamente agrupar as várias partes try ... excepte, assim, diferenciar as diferentes fontes potenciais de erro:

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)

Geralmente, uma abordagem mais simples funciona muito bem

A necessidade desse tratamento de exceção especial deve ser bastante rara e normalmente envolver o todo withem um try ... exceptbloco será suficiente. Especialmente se as várias fontes de erro são indicadas por diferentes tipos de exceção (personalizados) (os gerenciadores de contexto precisam ser projetados de acordo), podemos distinguir facilmente entre elas. Por exemplo:

try:
    with ContextManager():
        BLOCK
except InitError:  # raised from __init__
    ...
except AcquireResourceError:  # raised from __enter__
    ...
except ValueError:  # raised from BLOCK
    ...
except ReleaseResourceError:  # raised from __exit__
    ...
um convidado
fonte