Utilisation de 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'environ 400KB ) dans un dossier. Je l'ai fait lire à l'aide de readlines,

   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 l'échantillon de mes entrées (50 100 fichiers) . Quand j'ai lancé sur l'ensemble de l'entrée plus de 5K fichiers, le temps-pris n'était pas proche de l'accroissement linéaire.J'avais prévu de faire une analyse de performance et une analyse de Cprofile. Le temps pris pour le plus de fichiers en croissance exponentielle avec atteindre les taux sont plus mauvais lorsque les entrées atteignent les fichiers 7K.

voici le temps cumulé pour les lignes de lecture , premier - > 354 fichiers (échantillon d'entrée) et deuxième -> 7473 fichiers (ensemble d'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}

pour cette raison, le temps-pris par mon code n'est pas linéaire à mesure que l'entrée augmente. J'ai lu un peu de doc notes sur readlines(), où les gens ont affirmé que cela readlines() lit tout le contenu du fichier 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 collecteur de déchets devrait automatiquement effacer ce contenu chargé de la mémoire à la fin de ma boucle, donc à tout moment ma mémoire devrait avoir seulement le contenu de mon fichier en cours de traitement droit ? Mais, il y a certaines prises 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 éboueur. Content de savoir.

Aussi, suggérez d'autres façons de faire la même chose en mémoire et de manière efficace dans le temps. TIA.

31
demandé sur Maximilian Peters 2013-06-22 04:48:23

2 réponses

La version courte est: la façon efficace d'utiliser readlines() est de ne pas l'utiliser. Jamais.


j'ai lu un peu de doc notes sur readlines(), où les gens ont affirmé que cela readlines() lit tout le contenu du fichier en mémoire et consomme donc généralement plus de mémoire que readline () ou read ().

la documentation pour readlines()garantit explicitement qu'il lit tout le fichier dans la mémoire, et l'analyse en lignes, et construit un liststrings de ces lignes.

mais la documentation pour read() garantit aussi qu'il lit tout le fichier en mémoire, et construit un string, donc ça n'aide pas.


en plus d'utiliser plus de mémoire, cela signifie aussi que vous ne pouvez pas faire de travail tant que tout n'est pas lu. Si vous alternez la lecture et le traitement, même de la manière la plus naïve, vous va bénéficier d'au moins quelques pipelinages (grâce à la cache de disque OS, DMA, pipeline CPU, etc.), ainsi vous travaillerez sur un lot pendant que le prochain lot est en cours de lecture. Mais si vous forcez l'ordinateur à lire le fichier en entier, puis analyser le fichier en entier, puis exécuter votre code, vous obtenez seulement une région de chevauchement de travail pour l'ensemble du fichier, au lieu d'une région de chevauchement de travail par lecture.


Vous pouvez contourner cela en trois façons:

  1. Ecrire une boucle autour de readlines(sizehint),read(size), ou readline().
  2. il suffit d'utiliser le fichier comme un itérateur paresseux sans appeler l'un de ceux-ci.
  3. mmap le fichier, qui vous permet de le traiter comme une chaîne géante sans l'avoir lu en premier.

Par exemple, ce doit lire tous foo à la fois:

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

mais cela ne se lit qu'environ 8K à la fois:

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

Et ce ne lit que d'une ligne à la fois-bien que Python soit autorisé à (et choisira) une taille de tampon agréable pour rendre les choses plus rapides.

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

Et cela permettra de faire exactement la même chose que la précédente:

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

en Attendant:

mais si le collecteur de déchets automatiquement effacer ce contenu chargé de la mémoire à la fin de ma boucle, donc à tout moment ma mémoire devrait avoir seulement le contenu de mon fichier en cours de traitement droit ?

Python ne fait pas de telles garanties sur la collecte des ordures.

la mise en œuvre de CPython utilise par hasard refcounting pour GC, ce qui signifie que dans votre code, dès que file_content rebondit ou disparaît, la liste géante des cordes, et toutes les cordes qu'elle contient, seront libérées au freelist, ce qui signifie que le même souvenir peut être réutilisé à nouveau pour votre prochaine passe.

cependant, toutes ces attributions, copies, et désallocations ne sont pas gratuit-il est plus rapide de ne pas le faire que de le faire.

en plus de cela, le fait d'avoir vos cordes dispersées à travers une grande partie de la mémoire au lieu de réutiliser le même petit morceau de mémoire au-dessus et au-dessus nuit à votre comportement de cache.

de plus, alors que l'utilisation de la mémoire peut être constante (ou, plutôt, linéaire dans la taille de votre plus grand fichier, plutôt que dans la somme de vos tailles de fichier), cette ruée de malloc S pour l'étendre la première fois sera l'une des choses les plus lentes que vous faites (ce qui rend également les comparaisons de rendement beaucoup plus difficiles).


en regroupant tout cela, 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 ...
63
répondu abarnert 2013-06-24 20:05:27

Lire ligne par ligne, pas la totalité du fichier:

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

Encore mieux utiliser 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 Lira l'objet file en utilisant un itérateur, une ligne à la fois.

11
répondu Óscar López 2013-06-22 00:49:34