web-dev-qa-db-fra.com

Dois-je transmettre des noms de fichiers à ouvrir ou ouvrir des fichiers?

Supposons que j'ai une fonction qui fait des choses avec un fichier texte - par exemple, le lit et supprime le mot "a". Je pourrais lui passer un nom de fichier et gérer l'ouverture/fermeture dans la fonction, ou je pourrais lui passer le fichier ouvert et m'attendre à ce que celui qui l'appelle se charge de le fermer.

La première façon semble être une meilleure façon de garantir qu'aucun fichier n'est laissé ouvert, mais m'empêche d'utiliser des choses comme les 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 serais en mesure d'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 d'entre eux 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 doit-il simplement être bien documenté pour que le programmeur puisse utiliser la fonction comme il convient?

55
Dannnno

Les interfaces pratiques sont agréables, et parfois la voie à suivre. Cependant, la plupart du temps bon la composabilité est plus importante que la commodité, car une abstraction composable nous permet d'implémenter d'autres fonctionnalités (y compris des emballages de commodité) par-dessus.

La manière la plus générale pour votre fonction d'utiliser des fichiers est de prendre un descripteur de fichier ouvert comme paramètre, car cela lui permet également d'utiliser des descripteurs de fichier qui ne font pas partie du système de fichiers (par exemple, les tuyaux, les sockets,…):

def your_function(open_file):
    return do_stuff(open_file)

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

  • your_function Prend en paramètre un fichier ouvert ou un nom de fichier. S'il s'agit d'un nom de fichier, le fichier est ouvert et fermé et les exceptions sont propagées. Il y a un peu d'ambiguïté ici qui pourrait être contourné en utilisant des arguments nommés.
  • Offrez un emballage simple qui prend soin d'ouvrir le fichier, par exemple.

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

    Je perçois généralement ces fonctions comme un gonflement de l'API, mais si elles fournissent des fonctionnalités couramment utilisées, la commodité acquise est un argument suffisamment fort.

  • Enveloppez la fonctionnalité with open 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és with_file(name, lambda f: some_function(1, 2, f, named=4))

41
amon

La vraie question est celle de l'exhaustivité. Votre fonction de traitement de fichiers est-elle le traitement complet du fichier, ou s'agit-il d'une seule pièce dans une chaîne d'étapes de traitement? S'il est complet en lui-même, n'hésitez pas à encapsuler tous les accès aux fichiers dans 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 l'instruction with.

Si toutefois il est possible de traiter un fichier déjà ouvert, la distinction de votre ver_1 et ver_2 A plus 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 le test de type explicite est souvent mal v , en particulier dans les langages comme Java, Julia et Go où la répartition basée sur le type ou l'interface est directement prise en charge. En Python, cependant, il n'y a pas de prise en charge linguistique pour la répartition basée sur le type. Vous pouvez parfois voir des critiques de tests de type directs en Python, mais dans la pratique, c'est à la fois extrêmement courant et assez efficace. Il permet à une fonction d'avoir un degré élevé de généralité, en gérant tous les types de données susceptibles de se présenter, alias "typage du canard". Notez le trait de soulignement principal sur _ver_file; c'est une manière conventionnelle de désigner une fonction (ou 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.


Mise à jour 2019: compte tenu des mises à jour récentes dans Python 3, par exemple, les chemins sont désormais potentiellement stockés sous la forme pathlib.Path objets pas seulement str ou bytes (3.4+), et ce type d'indication est passé de l'ésotérique au courant dominant (vers 3.6+, bien qu'évoluant encore activement), voici un code mis à jour qui prend ces les avances en compte:

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)
22
Jonathan Eunice

Si vous passez le nom de fichier au lieu du descripteur de fichier, rien ne garantit que le deuxième fichier est le même que le premier lorsqu'il est ouvert; cela peut entraîner des bogues de correction et des failles de sécurité.

5
user541686

Il s'agit de la propriété et de la responsabilité de fermer le fichier. Vous pouvez transmettre un descripteur de flux ou de fichier ou tout ce qui devrait être fermé/supprimé à un moment donné à une autre méthode, tant que vous vous assurez qu'il est clair à qui il appartient et certain qu'il sera être fermé par le propriétaire lorsque vous avez terminé. Cela implique généralement une construction d'essai ou le modèle jetable.

1
Martin Maat

Un aspect que les autres réponses n'ont pas souligné est une approche de "capacités". Ceci est généralement appliqué dans le contexte de sécurité , mais peut également aider à la conception et au développement (par exemple, codage défensif, débogage, testabilité, etc.).

En gros: une "capacité" est l'information requise pour effectuer une action. Si nous avons une capacité, nous pouvons effectuer ses actions correspondantes; nous ne pouvons pas effectuer d'actions sans une capacité correspondante. Exemples de fonctionnalités: URL, clés API, paires nom d'utilisateur/mot de passe, etc. Les capacités sont utiles car elles se concentrent sur ce qui pourrait se produire plutôt que sur ce devrait se produire.

Dans votre cas, il semble que les noms de fichiers et les descripteurs sont à peu près équivalents: ils nous permettent de lire les fichiers. Pourtant, les choses ne sont pas si simples et les capacités nous permettent de réfléchir aux différences.

Notre code devrait lire les données d'un fichier, il a donc besoin de la capacité (informations AKA requises) pour le faire. Un nom de fichier nous donne-t-il cette capacité? Pas assez. En particulier:

  • Il se peut qu'il n'y ait aucun fichier avec le nom de fichier donné. Dans ce cas, nous ne pouvons pas lire un fichier, donc (selon ma définition approximative ci-dessus), ces noms de fichiers ne sont pas les capacités dont nous avons besoin.
  • S'il y a un fichier avec le nom de fichier donné, le compte utilisateur de notre programme pourrait ne pas avoir la permission de le lire. Encore une fois, d'après ma définition approximative, ces noms de fichiers ne sont pas les capacités dont nous avons besoin: nous aurions besoin d'informations supplémentaires pour y accéder, comme les informations de connexion d'un utilisateur autorisé.

Les fichiers ouverts (poignées AKA ou "objets de type fichier") ne présentent pas ces problèmes: des erreurs d'autorisation ou de fichier introuvable seront lancées avant l'appel de notre fonction; et vraisemblablement (via le principe de responsabilité unique) le code qui essaie d'ouvrir ces fichiers est mieux placé pour gérer ces erreurs ou les transmettre.

Les noms de fichiers ne sont donc pas toujours les capacités dont nous avons besoin. Ils pourraient également nous donner des capacités supplémentaires dont nous ne voulons pas non plus! Par exemple, étant donné un nom de fichier, nous pouvons (essayer de) le supprimer, le renommer, le déplacer, etc .; alors que nous ne pouvons pas le faire en utilisant une poignée (au moins, pas aussi facilement). Si nous écrivons notre code en utilisant un ensemble de capacités plus restreint, il est moins probable que nous déclenchions une action indésirable par erreur; c'est donc une autre raison d'accepter les descripteurs au lieu des noms de fichiers. Je dirais également que cela facilite la compréhension et le débogage, car nous pouvons deviner à partir de sa signature si une fonction peut être la source d'un problème (comme, par exemple, des fichiers supprimés accidentellement). Cette façon de penser peut également influencer notre conception et notre architecture, car un aspect important est de s'assurer que chaque composant a tout ce dont il a besoin pour faire son travail (c'est-à-dire que les bonnes capacités sont disponibles), tout en encapsulant/modularisant/protégeant d'autres parties du système contre brouillage (c'est-à-dire restreindre ces capacités).

Mise en garde: Si nous nous appuyons sur un modèle de capacité pour la sécurité , il est important que les capacités soient gardées secrètes; sont impossibles à deviner (par exemple, nous ne devrions pas pouvoir incrémenter un ID valide pour en obtenir un autre); et non découvrable (par exemple, la possibilité de lister les noms de fichiers devrait être restreinte). Nous devons supposer que si quelque chose est plausible , alors les acteurs malveillants l'exploiteront (par exemple en creusant profondément dans les attributs d'un descripteur de fichier pour comprendre le nom du fichier) . Si nous ne sommes pas préoccupés par les acteurs malveillants, j'ai tendance à supposer que les développeurs (y compris moi) sont principalement paresseux: s'il y a une solution paresseuse et une solution délicate (par exemple, lire à partir d'un handle donné ou trouver son nom de fichier et l'ouvrir) , alors nous pouvons généralement ignorer l'approche délicate lors de la conception, car il est peu probable qu'elle soit (ab) utilisée.

1
Warbo