Dois-je transmettre les noms de fichiers à ouvrir ou les fichiers ouverts?

53

Supposons que ma fonction utilise un fichier texte - par exemple, la lit et supprime le mot "a". Je pouvais soit lui passer un nom de fichier et gérer l'ouverture / la fermeture de la fonction, soit le transmettre le fichier ouvert et m'attendre à ce que celui qui l'appelle s'occupe de le fermer.

Le premier moyen semble être un meilleur moyen de garantir qu'aucun fichier n'est laissé ouvert, mais m'empêche d'utiliser des objets tels que des objets StringIO.

La deuxième façon pourrait être un peu dangereuse - aucun moyen de savoir si le fichier sera fermé ou non, mais je pourrais utiliser des objets de type fichier

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)

Est-ce que l'un de ceux-ci est généralement préféré? Est-il généralement prévu qu'une fonction se comporte de l'une de ces deux manières? Ou devrait-il simplement être bien documenté pour que le programmeur puisse utiliser la fonction de manière appropriée?

Dannnno
la source

Réponses:

39

Des interfaces pratiques sont sympas, et parfois la voie à suivre. Cependant, la plupart du temps, une bonne composabilité est plus importante que la commodité , car une abstraction pouvant être composée nous permet de mettre en œuvre d'autres fonctionnalités (y compris des wrappers de commodité).

Le moyen le plus général pour que votre fonction utilise des fichiers est de prendre un descripteur de fichier ouvert en tant que paramètre, car cela lui permet également d'utiliser des descripteurs de fichier ne faisant pas partie du système de fichiers (par exemple, les tuyaux, les sockets,…):

def your_function(open_file):
    return do_stuff(open_file)

Si l'orthographe with open(filename, 'r') as f: result = your_function(f)est trop demander à vos utilisateurs, vous pouvez choisir l'une des solutions suivantes:

  • your_functionprend un fichier ouvert ou un nom de fichier en paramètre. S'il s'agit d'un nom de fichier, le fichier est ouvert et fermé, et les exceptions propagées. Il y a un problème d'ambiguïté qui pourrait être résolu en utilisant des arguments nommés.
  • Offrez un simple emballage qui s’occupe de l’ouverture du fichier, par exemple

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

    En général, je perçois des fonctions telles que des API gonflées, mais si elles fournissent des fonctionnalités couramment utilisées, la commodité acquise est un argument suffisamment puissant.

  • Enveloppez la with openfonctionnalité dans une autre fonction composable:

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

    utilisé comme with_file(name, your_function)ou dans des cas plus compliquéswith_file(name, lambda f: some_function(1, 2, f, named=4))

Amon
la source
6
Le seul inconvénient de cette approche est que parfois le nom de l’objet ressemblant à un fichier est nécessaire, par exemple pour signaler les erreurs: les utilisateurs finaux préfèrent voir "Erreur dans foo.cfg (12)" plutôt que "Erreur dans <stream @ 0x03fd2bb6>> (12) ". Un argument optionnel "stream_name" your_functionpeut être utilisé à cet égard.
22

La vraie question est celle de la complétude. Votre fonction de traitement de fichier est-elle le traitement complet du fichier ou s'agit-il simplement d'une partie d'une chaîne d'étapes de traitement? S'il est complet et autonome, n'hésitez pas à encapsuler tous les accès aux fichiers au sein d'une fonction.

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

Cela a la très belle propriété de finaliser la ressource (fermeture du fichier) à la fin de la withdéclaration.

Si toutefois il est éventuellement nécessaire de traiter un fichier déjà ouvert, la distinction entre vous ver_1et a ver_2plus de sens. Par exemple:

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)

Ce type de test de type explicite est souvent mal vu , en particulier dans des langages tels que Java, Julia et Go, où la distribution par type ou par interface est directement prise en charge. En Python, toutefois, il n'y a pas de prise en charge linguistique pour la répartition basée sur un type. Vous pouvez parfois rencontrer des critiques sur les tests de type directs en Python, mais dans la pratique, elles sont extrêmement courantes et très efficaces. Il permet à une fonction d’avoir un degré de généralité élevé et de manipuler tous les types de données susceptibles de lui arriver, c'est-à-dire "frappe de canard". Notez le trait de soulignement principal sur _ver_file; c’est une manière conventionnelle de désigner une fonction (ou une méthode) "privée". Bien qu’elle puisse techniquement être appelée directement, elle suggère que la fonction n’est pas destinée à une consommation externe directe.


2019 Mise à jour: Compte tenu des mises à jour récentes en Python 3, par exemple , que les chemins sont maintenant potentiellement stockées sous forme d' pathlib.Pathobjets non seulement strou bytes(3.4+), et que le type hinting est passé de ésotérique à intégrer (vers 3.6+, mais toujours en évolution active), voici code mis à jour qui prend en compte ces avancées:

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)
Jonathan Eunice
la source
1
La saisie de canard serait testée en fonction de ce que vous pouvez faire avec l'objet plutôt que de son type. Par exemple, essayer d'appeler readquelque chose qui pourrait ressembler à un fichier, ou appeler open(fileobj, 'r')et intercepter le TypeErrorsi fileobjn'est pas une chaîne.
user2357112
Vous plaidez pour l' utilisation de la frappe de canard . L'exemple fournit la saisie de type de canard en vigueur - c'est-à-dire que les utilisateurs obtiennent l' veropération indépendamment du type. verComme vous le dites, il est peut-être également possible d’implémenter la technique de frappe de canard. Cependant, générer des exceptions est plus lent que la simple inspection de type et l’OMI ne procure aucun avantage particulier (clarté, généralité, etc.). D’après mon expérience, la frappe de canards est géniale "dans le grand", mais neutre à contre-productive "dans le petit . "
Jonathan Eunice
3
Non, ce que vous faites n'est toujours pas la frappe de canard. Un hasattr(fileobj, 'read')test serait la frappe de canard; un isinstance(fileobj, str)test n'est pas. Voici un exemple de la différence: le isinstancetest échoue avec les noms de fichiers unicode, car u'adsf.txt'n’est pas un str. Vous avez testé un type trop spécifique. Un test de frappe de canard, qu'il soit basé sur un appel openou sur une does_this_object_represent_a_filenamefonction hypothétique , n'aurait pas ce problème.
user2357112
1
Si le code était un code de production plutôt qu'un exemple explicatif, je n'aurais pas non plus ce problème, car je ne l'utiliserais pas, is_instance(x, str)mais plutôt quelque chose comme is_instance(x, string_types), avec string_typesun réglage approprié pour un fonctionnement correct sur les années PY2 et PY3. Étant donné que quelque chose qui plaisante comme une ficelle, verréagirait correctement; étant donné quelque chose qui plaisante comme un fichier, le même. Pour un utilisateur de ver, il n'y aurait aucune différence, à l'exception du fait que l'implémentation de l'inspection de type serait plus rapide. Puristes de canard: n'hésitez pas à être en désaccord.
Jonathan Eunice
5

Si vous passez le nom du fichier à la place du descripteur de fichier, rien ne garantit que le deuxième fichier est le même fichier que le premier à l'ouverture du fichier; Cela peut entraîner des erreurs de correction et des failles de sécurité.

Mehrdad
la source
1
Vrai. Mais cela doit être contrebalancé par un autre compromis: si vous transmettez un descripteur de fichier, tous les lecteurs doivent coordonner leurs accès au fichier, car chacun risque de déplacer la "position actuelle du fichier".
Jonathan Eunice
@JonathanEunice: Coordonner dans quel sens? Tout ce qu'ils ont à faire, c'est de définir la position du fichier à l'endroit souhaité.
Mehrdad
1
Si plusieurs entités lisent le fichier, il peut y avoir des dépendances. Il peut être nécessaire de commencer là où un autre s’est arrêté (ou à un endroit défini par les données lues par une lecture précédente). En outre, les lecteurs peuvent s'exécuter dans différents threads, ouvrant d'autres boîtes de coordination de vers. Les objets de fichier distribués deviennent des états globaux exposés, avec tous les problèmes (ainsi que les avantages) que cela implique.
Jonathan Eunice
1
Ce n'est pas en contournant le chemin du fichier qui est la clé. C'est le fait qu'une fonction (ou classe, méthode ou autre lieu de contrôle) assume la responsabilité du "traitement complet du fichier". Si les accès aux fichiers sont encapsulés quelque part , vous n'avez pas besoin de transmettre un état global mutable comme les descripteurs de fichiers ouverts.
Jonathan Eunice
1
Eh bien, nous pouvons accepter d'être en désaccord alors. Je dis qu'il y a un inconvénient résolu pour les conceptions qui transmettent avec gloire un état mondial mutable. Il y a aussi des avantages. Ainsi, un "compromis". Les conceptions qui passent les chemins de fichiers font souvent des E / S d'un seul coup, de manière encapsulée. Je vois cela comme un couplage avantageux. YMMV.
Jonathan Eunice
1

Il s'agit de la propriété et de la responsabilité de fermer le fichier. Vous pouvez transmettre un flux ou un descripteur de fichier ou tout autre élément qui devrait être fermé / éliminé à un moment donné à une autre méthode, à condition que vous sachiez qui est le propriétaire et certain qu'il sera fermé par le propriétaire lorsque vous aurez terminé. . Cela implique généralement une construction try-finally ou le motif jetable.

Martin Maat
la source
-1

Si vous choisissez de transmettre des fichiers ouverts, vous pouvez procéder de la manière suivante, MAIS vous n’avez pas accès au nom de fichier dans la fonction qui écrit dans le fichier.

Je le ferais si je voulais avoir une classe qui était à 100% responsable des opérations de fichier / flux et d'autres classes ou fonctions qui seraient naïves et ne devraient pas ouvrir ou fermer lesdits fichiers / flux.

Rappelez-vous que les gestionnaires de contexte fonctionnent comme si vous aviez une clause finally. Donc, si une exception est levée dans la fonction writer, le fichier va être fermé quoi qu'il arrive.

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)
Vls
la source
Comment est-ce meilleur / différent que de simplement utiliser with open? Comment cela résout-il la question de l'utilisation des noms de fichiers par rapport aux objets de type fichier?
Dannnno
Cela vous montre un moyen de masquer le comportement d'ouverture / fermeture de fichier / flux. Comme vous pouvez le voir clairement dans les commentaires, cela vous permet d'ajouter une logique transparente avant "flux / fichier" au "rédacteur". "Writer" pourrait être une méthode d'une classe d'un autre paquet. En substance, c'est un emballage d'ouvert. Merci également d'avoir répondu et voté.
Vls
Ce comportement est déjà traité par with openbien, non? Et ce que vous préconisez efficacement, c’est une fonction qui n’utilise que des objets de type fichier, sans se soucier de sa provenance?
Dannnno