Devo passar nomes de arquivos para serem abertos ou abrir arquivos?


53

Suponha que eu tenha uma função que faça coisas com um arquivo de texto - por exemplo, leia e remova a palavra 'a'. Eu poderia passar um nome de arquivo para ele e lidar com a abertura / fechamento da função, ou poderia passar o arquivo aberto e esperar que quem telefonasse tratasse de fechá-lo.

A primeira maneira parece ser a melhor maneira de garantir que nenhum arquivo seja deixado aberto, mas me impede de usar coisas como objetos StringIO

A segunda maneira pode ser um pouco perigosa - não há como saber se o arquivo será fechado ou não, mas eu seria capaz de usar objetos semelhantes a arquivos

def ver_1(filename):
    with open(filename, 'r') as f:
        return do_stuff(f)

def ver_2(open_file):
    return do_stuff(open_file)

print ver_1('my_file.txt')

with open('my_file.txt', 'r') as f:
    print ver_2(f)

Um destes é geralmente preferido? É geralmente esperado que uma função se comporte de uma dessas duas maneiras? Ou deveria apenas ser bem documentado, para que o programador possa usar a função conforme apropriado?

Respostas:


39

Interfaces convenientes são legais e, às vezes, o caminho a percorrer. No entanto, na maioria das vezes, a boa composição é mais importante do que a conveniência , pois uma abstração compostável nos permite implementar outras funcionalidades (inclusive wrappers de conveniência) sobre ela.

A maneira mais geral de sua função usar arquivos é usar um identificador de arquivo aberto como parâmetro, pois isso também permite usar identificadores de arquivo que não fazem parte do sistema de arquivos (por exemplo, tubos, soquetes, etc.):

def your_function(open_file):
    return do_stuff(open_file)

Se a ortografia with open(filename, 'r') as f: result = your_function(f)é pedir demais aos seus usuários, você pode escolher uma das seguintes soluções:

  • your_functionusa um arquivo aberto ou um nome de arquivo como parâmetro. Se for um nome de arquivo, o arquivo é aberto e fechado e as exceções propagadas. Há um pouco de problema com ambiguidade aqui, que poderia ser contornado usando argumentos nomeados.
  • Ofereça um invólucro simples que cuide da abertura do arquivo, por exemplo

    def your_function_filename(file):
        with open(file, 'r') as f:
            return your_function(f)
    

    Geralmente percebo funções como o inchaço da API, mas se elas fornecem funcionalidade comumente usada, a conveniência adquirida é um argumento suficientemente forte.

  • Embrulhe a with openfuncionalidade em outra função de composição:

    def with_file(filename, callback):
        with open(filename, 'r') as f:
            return callback(f)
    

    usado como with_file(name, your_function)ou em casos mais complicadoswith_file(name, lambda f: some_function(1, 2, f, named=4))


6
A única desvantagem dessa abordagem é que, às vezes, é necessário o nome do objeto semelhante ao arquivo, por exemplo, para o relatório de erros: os usuários finais preferem ver "Erro no foo.cfg (12)" em vez de "Erro no <stream @ 0x03fd2bb6> (12) " Um argumento opcional "stream_name" your_functionpode ser usado nesse sentido.

22

A verdadeira questão é de completude. Sua função de processamento de arquivos é o processamento completo do arquivo ou é apenas uma parte de uma cadeia de etapas de processamento? Se estiver completo por si só, fique à vontade para encapsular todo o acesso a arquivos em uma função.

def ver(filepath):
    with open(filepath, "r") as f:
        # do processing steps on f
        return result

Isso tem a propriedade muito agradável de finalizar o recurso (fechando o arquivo) no final da withinstrução.

Se, no entanto, houver a necessidade de processar um arquivo já aberto, a distinção entre você ver_1e ver_2faz mais sentido. Por exemplo:

def _ver_file(f):
    # do processing steps on f
    return result

def ver(fileobj):
    if isinstance(fileobj, str):
        with open(fileobj, 'r') as f:
            return _ver_file(f)
    else:
        return _ver_file(fileobj)

Esse tipo de teste explícito de tipo geralmente é desaprovado , especialmente em linguagens como Java, Julia e Go, nas quais o envio por tipo ou por interface é diretamente suportado. No Python, no entanto, não há suporte à linguagem para envio baseado em tipo. Ocasionalmente, você pode ver críticas ao teste de tipo direto no Python, mas na prática é extremamente comum e bastante eficaz. Ele permite que uma função tenha um alto grau de generalidade, manipulando quaisquer tipos de dados que possam aparecer no seu caminho, também conhecido como "digitação de pato". Observe o sublinhado principal em _ver_file; essa é uma maneira convencional de designar uma função "privada" (ou método). Embora tecnicamente possa ser chamado diretamente, ele sugere que a função não se destina ao consumo externo direto.


2019 atualização: Dado atualizações recentes em Python 3, por exemplo, que os caminhos estão agora potencialmente armazenados como pathlib.Pathobjetos e não apenas strou bytes(3.4+), e que a indicação de tipo passou de esotérico para integrar (cerca 3.6+, embora ainda em evolução ativamente), aqui está código atualizado que leva esses avanços em consideração:

from pathlib import Path
from typing import IO, Any, AnyStr, Union

Pathish = Union[AnyStr, Path]  # in lieu of yet-unimplemented PEP 519
FileSpec = Union[IO, Pathish]

def _ver_file(f: IO) -> Any:
    "Process file f"
    ...
    return result

def ver(fileobj: FileSpec) -> Any:
    "Process file (or file path) f"
    if isinstance(fileobj, (str, bytes, Path)):
        with open(fileobj, 'r') as f:
            return _ver_file(f)
    else:
        return _ver_file(fileobj)

11
A digitação de pato testaria com base no que você pode fazer com o objeto, e não no seu tipo. Por exemplo, tentar chamar readalgo que possa ser semelhante a um arquivo ou chamar open(fileobj, 'r')e capturar o TypeErrorif fileobjnão é uma string.
user2357112

Você está argumentando pela digitação do pato em uso . O exemplo fornece a digitação do pato com efeito - ou seja, os usuários obtêm a veroperação independente do tipo. Também pode ser possível implementar veratravés da digitação do pato, como você diz. Mas gerar e capturar exceções é mais lento que a simples inspeção de tipo, e a IMO não produz nenhum benefício específico (clareza, generalidade etc.) . "
Jonathan Eunice

3
Não, o que você está fazendo ainda não é digitação de pato. Um hasattr(fileobj, 'read')teste seria digitar pato; um isinstance(fileobj, str)teste não é. Aqui está um exemplo da diferença: o isinstanceteste falha com os nomes de arquivos unicode, pois u'adsf.txt'não é um str. Você testou para um tipo muito específico. Um teste de digitação de pato, baseado em chamadas openou em alguma does_this_object_represent_a_filenamefunção hipotética , não teria esse problema.
user2357112

11
Se o código fosse um código de produção e não um exemplo explicativo, eu também não teria esse problema, porque não usaria, is_instance(x, str)mas sim algo como is_instance(x, string_types), com string_typesdefinido corretamente para a operação adequada entre PY2 e PY3. Dado algo que grasna como uma corda, verreagiria corretamente; dado algo que grasna como um arquivo, o mesmo. Para um usuário de ver, não haveria diferença - exceto que a implementação da inspeção de tipo seria executada mais rapidamente. Puristas de pato: fique à vontade para discordar.
Jonathan Eunice

5

Se você passar o nome do arquivo em vez do identificador, não há garantia de que o segundo arquivo seja o mesmo que o primeiro quando ele for aberto; isso pode levar a erros de correção e falhas de segurança.


11
Verdadeiro. Mas isso deve ser contrabalançado com outra desvantagem: se você repassar um identificador de arquivo, todos os leitores deverão coordenar seus acessos ao arquivo, porque é provável que cada um mova a "posição atual do arquivo".
Jonathan Eunice

@ JonathanEunice: coordene em que sentido? Tudo o que eles precisam fazer é definir a posição do arquivo para onde quer que estejam.
Mehrdad

11
Se houver várias entidades lendo o arquivo, pode haver dependências. Pode ser necessário começar de onde o outro parou (ou em um local definido pelos dados lidos por uma leitura anterior). Além disso, os leitores podem estar executando em diferentes threads, abrindo outras latas de coordenação de worms. Objetos de arquivo distribuídos tornam-se expostos ao estado global, com todos os problemas (e benefícios) que isso implica.
Jonathan Eunice

11
Não é a chave que está passando pelo caminho do arquivo. É ter uma função (ou classe, método ou outro local de controle) assumir a responsabilidade pelo "processamento completo do arquivo". Se os acessos a arquivos estiverem encapsulados em algum lugar , não será necessário passar um estado global mutável, como identificadores de arquivos abertos.
Jonathan Eunice

11
Bem, podemos concordar em discordar então. Estou dizendo que há uma desvantagem decidida em projetos que passam de maneira insignificante por um estado global mutável. Existem algumas vantagens também. Assim, uma "troca". Projetos que passam por caminhos de arquivo geralmente fazem E / S de uma só vez, de maneira encapsulada. Eu vejo isso como um acoplamento vantajoso. YMMV.
Jonathan Eunice

1

Trata-se de propriedade e a responsabilidade de fechar o arquivo. Você pode transmitir um identificador de fluxo ou arquivo ou qualquer outra coisa que deva ser fechada / descartada em algum momento para outro método, desde que você esteja claro quem é o proprietário e que ele será fechado pelo proprietário quando você terminar . Isso geralmente envolve uma construção try-finalmente ou o padrão descartável.


-1

Se você optar por passar os arquivos abertos, poderá fazer algo como o seguinte, MAS você não terá acesso ao nome do arquivo na função que grava no arquivo.

Eu faria isso se quisesse ter uma classe que fosse 100% responsável pelas operações de arquivo / fluxo e outras classes ou funções que seriam ingênuas e não se espera que abram ou fechem esses arquivos / fluxos.

Lembre-se de que os gerentes de contexto funcionam como uma cláusula Finalmente. Portanto, se uma exceção for lançada na função de gravação, o arquivo será fechado, não importa o quê.

import contextlib

class FileOpener:

    def __init__(self, path_to_file):
        self.path_to_file = path_to_file

    @contextlib.contextmanager
    def open_write(self):
        # ...
        # Here you can add code to create the directory that will accept the file.
        # ...
        # And you can add code that will check that the file does not exist 
        # already and maybe raise FileExistsError
        # ...
        try:            
            with open(self.path_to_file, "w") as file:
                print(f"open_write: has opened the file with id:{id(file)}")            
                yield file                
        except IOError:
            raise
        finally:
            # The try/catch/finally is not mandatory (except if you want to manage Exceptions in an other way, as file objects have predefined cleanup actions 
            # and when used with a 'with' ie. a context manager (not the decorator in this example) 
            # are closed even if an error occurs. Finally here is just used to demonstrate that the 
            # file was really closed.
            print(f"open_write: has closed the file with id:{id(file)} - {file.closed}")        


def writer(file_open, data, raise_exc):
    with file_open() as file:
        print("writer: started writing data.")
        file.write(data)
        if raise_exc:
            raise IOError("I am a broken data cable in your server!")
        print("writer: wrote data.")
    print("writer: finished.")

if __name__ == "__main__":
    fo = FileOpener('./my_test_file.txt')    
    data = "Hello!"  
    raise_exc = False  # change me to True and see that the file is closed even if an Exception is raised.
    writer(fo.open_write, data, raise_exc)

Como isso é melhor / diferente do que apenas usar with open? Como isso aborda a questão do uso de nomes de arquivos e objetos semelhantes a arquivos?
Dannnno 15/10

Isso mostra uma maneira de ocultar o comportamento de abrir / fechar arquivo / fluxo. Como você pode ver claramente nos comentários, é possível adicionar lógica antes de abrir o fluxo / arquivo que é transparente para o "escritor". O "escritor" pode ser um método de uma classe de outro pacote. Em essência, é um invólucro de aberto. Além disso, obrigado por responder e votar.
Vls 15/10

Esse comportamento já é tratado por with open, certo? E o que você está defendendo efetivamente é uma função que usa apenas objetos do tipo arquivo e não se importa de onde veio?
Dannnno 16/10
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.