No Python, se eu retornar dentro de um bloco "with", o arquivo ainda será fechado?

256

Considere o seguinte:

with open(path, mode) as f:
    return [line for line in f if condition]

O arquivo será fechado corretamente ou o uso de returnalguma forma ignora o gerenciador de contexto ?

Brisa leve
fonte

Respostas:

238

Sim, ele age como o finallybloco após um trybloco, ou seja, sempre é executado (a menos que o processo python termine de uma maneira incomum, é claro).

Também é mencionado em um dos exemplos de PEP-343, que é a especificação para a withdeclaração:

with locked(myLock):
    # Code here executes with myLock held.  The lock is
    # guaranteed to be released when the block is left (even
    # if via return or by an uncaught exception).

Algo que vale a pena mencionar é que você não pode capturar facilmente exceções geradas pela open()chamada sem colocar todo o withbloco dentro de um try..exceptbloco que geralmente não é o que se deseja.

ThiefMaster
fonte
8
elsepode ser adicionado withpara resolver esse try with exceptproblema. edit: adicionado ao idioma
rplnt 27/03
7
Não sei se é relevante, mas, pelo que sei, Process.terminate()é um dos poucos (o único?) Cenário que não garante a chamada de uma finallydeclaração: "Observe que os manipuladores de saída e, finalmente, as cláusulas etc., não serão executado."
Rik Poggi 27/03
Às os._exitvezes, o @RikPoggi é usado - ele sai do processo Python sem chamar manipuladores de limpeza.
Acumenus
2
Talvez provocando um pouco a cobra, mas e se eu retornar uma expressão de gerador de dentro do withbloco, a garantia se mantém enquanto o gerador continuar produzindo valores? enquanto alguma coisa referenciar isso? delOu seja, eu preciso usar ou atribuir um valor diferente para a variável que contém o objeto gerador?
Ack
1
@davidA Depois que o arquivo é fechado, as referências ainda estão acessíveis; no entanto, qualquer tentativa de usar as referências a dados pull / push para / a partir do arquivo vai dar: ValueError: I/O operation on closed file..
RWDJ 5/09/19
36

Sim.

def example(path, mode):
    with open(path, mode) as f:
        return [line for line in f if condition]

..é praticamente equivalente a:

def example(path, mode):
    f = open(path, mode)

    try:
        return [line for line in f if condition]
    finally:
        f.close()

Mais precisamente, o __exit__método em um gerenciador de contexto é sempre chamado ao sair do bloco (independentemente de exceções, retornos etc.). O __exit__método do objeto de arquivo apenas chama f.close()(por exemplo, aqui no CPython )

dbr
fonte
30
Uma experiência interessante para mostrar a garantia de que você começa a partir do finallykeywrod é: def test(): try: return True; finally: return False.
Ehsan Kia
20

Sim. De maneira mais geral, o __exit__método de um Gerenciador de Contexto With Statement será realmente chamado no caso de um returnde dentro do contexto. Isso pode ser testado com o seguinte:

class MyResource:
    def __enter__(self):
        print('Entering context.')
        return self

    def __exit__(self, *exc):
        print('EXITING context.')

def fun():
    with MyResource():
        print('Returning inside with-statement.')
        return
    print('Returning outside with-statement.')

fun()

A saída é:

Entering context.
Returning inside with-statement.
EXITING context.

A saída acima confirma que __exit__foi chamado apesar do início return. Como tal, o gerenciador de contexto não é ignorado.

Acumenus
fonte
4

Sim, mas pode haver algum efeito colateral em outros casos, porque deve fazer algo (como buffer de liberação) no __exit__bloco

import gzip
import io

def test(data):
    out = io.BytesIO()
    with gzip.GzipFile(fileobj=out, mode="wb") as f:
        f.write(data)
        return out.getvalue()

def test1(data):
    out = io.BytesIO()
    with gzip.GzipFile(fileobj=out, mode="wb") as f:
        f.write(data)
    return out.getvalue()

print(test(b"test"), test1(b"test"))

# b'\x1f\x8b\x08\x00\x95\x1b\xb3[\x02\xff' b'\x1f\x8b\x08\x00\x95\x1b\xb3[\x02\xff+I-.\x01\x00\x0c~\x7f\xd8\x04\x00\x00\x00'
virusdefender
fonte