web-dev-qa-db-fra.com

Python readlines () et pratique efficace pour la lecture

J'ai un problème pour analyser des milliers de fichiers texte (environ 3000 lignes dans chaque fichier d'une taille d'environ 400 Ko) dans un dossier. Je les ai lus en utilisant des lignes de lecture,

   for filename in os.listdir (input_dir) :
       if filename.endswith(".gz"):
          f = gzip.open(file, 'rb')
       else:
          f = open(file, 'rb')

       file_content = f.readlines()
       f.close()
   len_file = len(file_content)
   while i < len_file:
       line = file_content[i].split(delimiter) 
       ... my logic ...  
       i += 1  

Cela fonctionne parfaitement pour les échantillons de mes entrées (50 100 fichiers). Lorsque j'ai utilisé plus de 5 000 fichiers sur l'ensemble de l'entrée, le temps pris était loin d'être égal à l'incrément linéaire. J'avais prévu d'effectuer une analyse des performances et une analyse Cprofile. Le temps pris par le plus grand nombre de fichiers augmente de manière exponentielle et atteint des taux plus bas lorsque les entrées atteignent 7 000 fichiers.

Voici le temps cumulé pour les lignes de lecture, le premier -> 354 fichiers (exemple d'entrée) et le second -> 7473 fichiers (l'ensemble de l'entrée).

 ncalls  tottime  percall  cumtime  percall filename:lineno(function)
 354    0.192    0.001    **0.192**    0.001 {method 'readlines' of 'file' objects}
 7473 1329.380    0.178  **1329.380**    0.178 {method 'readlines' of 'file' objects}

De ce fait, le temps pris par mon code n'est pas proportionnel à la taille de l'entrée. J'ai lu des notes de doc sur readlines(), où des gens ont prétendu que cette readlines() lit le contenu du fichier entier en mémoire et consomme donc généralement plus de mémoire que readline() ou read().

Je suis d'accord avec ce point, mais le ramasse-miettes doit-il automatiquement effacer le contenu chargé de la mémoire à la fin de la boucle, de sorte qu'à tout moment ma mémoire ne devrait contenir que le contenu de mon fichier en cours de traitement, n'est-ce pas? Mais, il y a quelques pièges ici. Quelqu'un peut-il donner un aperçu de cette question?.

Est-ce un comportement inhérent de readlines() ou ma mauvaise interprétation de python garbage collector. Heureux de savoir.

Également, suggérez des moyens alternatifs de faire la même chose en mémoire et en temps. TIA.

37
Learner

La version courte est: Le moyen efficace d'utiliser readlines() est de ne pas l'utiliser. Jamais.


J'ai lu des notes de documentation sur readlines(), où des gens ont affirmé que cette readlines() lit le contenu du fichier entier en mémoire et consomme donc généralement plus de mémoire que readline () ou read ().

La documentation de readlines() garantit explicitement qu’il lit le fichier entier en mémoire et l’analyse. et construit un list complet de strings à partir de ces lignes.

Mais la documentation de read() garantit également qu'il lit l'intégralité du fichier en mémoire et crée un string, ce qui n'aide en rien.


En plus d'utiliser plus de mémoire, cela signifie également que vous ne pouvez effectuer aucun travail tant que tout n'est pas lu. Si vous alternez lecture et traitement même de la manière la plus naïve, vous bénéficierez d'au moins un peu de traitement en pipeline (grâce au cache disque du système d'exploitation, au DMA, au pipeline de processeur, etc.). Vous travaillerez donc sur un lot tandis que le prochain est en cours de lecture. Mais si vous forcez l'ordinateur à lire l'intégralité du fichier, puis analysez l'intégralité du fichier, puis exécutez votre code, vous obtenez uniquement une région de travail qui se chevauche pour le fichier entier, au lieu d'une région de travail qui se chevauchent par lecture.


Vous pouvez contourner ce problème de trois manières:

  1. Ecrivez une boucle autour de readlines(sizehint), read(size) ou readline().
  2. Utilisez simplement le fichier comme un itérateur paresseux sans appeler aucun de ceux-ci.
  3. mmap le fichier, ce qui vous permet de le traiter comme une chaîne géante sans le lire au préalable.

Par exemple, ceci doit lire tout foo à la fois:

with open('foo') as f:
    lines = f.readlines()
    for line in lines:
        pass

Mais cela ne lit que 8K à la fois:

with open('foo') as f:
    while True:
        lines = f.readlines(8192)
        if not lines:
            break
        for line in lines:
            pass

Et cela ne lit qu'une ligne à la fois, bien que Python soit autorisé (et choisira) une taille de tampon Nice pour rendre les choses plus rapides.

with open('foo') as f:
    while True:
        line = f.readline()
        if not line:
            break
        pass

Et cela fera exactement la même chose que le précédent:

with open('foo') as f:
    for line in f:
        pass

Pendant ce temps:

mais le ramasse-miettes doit-il effacer automatiquement le contenu chargé de la mémoire à la fin de ma boucle, donc à tout moment, ma mémoire ne doit contenir que le contenu de mon fichier en cours de traitement, non?

Python ne fait aucune telle garantie sur la récupération de place.

L'implémentation CPython utilise souvent refcounting for GC, ce qui signifie que dans votre code, dès que file_content _ rebondit ou disparaît, la liste géante de chaînes et toutes les chaînes qu’elle contient sont libérées dans la liste libre, ce qui signifie que la même mémoire peut être réutilisée pour votre prochaine passe.

Cependant, toutes ces allocations, copies et désallocations ne sont pas gratuites - il est beaucoup plus rapide de ne pas les faire que de les faire.

De plus, le fait d'avoir vos chaînes dispersées sur une large bande de mémoire au lieu de réutiliser le même petit bloc de mémoire nuit à votre comportement en cache.

De plus, bien que l'utilisation de la mémoire puisse être constante (ou plutôt linéaire dans la taille de votre fichier le plus volumineux, plutôt que dans la somme de vos tailles de fichiers), Rush of mallocs le développera pour la première fois être l'une des choses les plus lentes que vous fassiez (ce qui rend également beaucoup plus difficile la comparaison des performances).


En résumé, voici comment j'écrirais votre programme:

for filename in os.listdir(input_dir):
    with open(filename, 'rb') as f:
        if filename.endswith(".gz"):
            f = gzip.open(fileobj=f)
        words = (line.split(delimiter) for line in f)
        ... my logic ...  

Ou peut-être:

for filename in os.listdir(input_dir):
    if filename.endswith(".gz"):
        f = gzip.open(filename, 'rb')
    else:
        f = open(filename, 'rb')
    with contextlib.closing(f):
        words = (line.split(delimiter) for line in f)
        ... my logic ...
74
abarnert

Lire ligne par ligne, pas le fichier entier:

for line in open(file_name, 'rb'):
    # process line here

Encore mieux, utilisez with pour fermer automatiquement le fichier:

with open(file_name, 'rb') as f:
    for line in f:
        # process line here

Ce qui précède lit l'objet fichier à l'aide d'un itérateur, ligne par ligne.

16
Óscar López