Forum Programmation.python écrire la sortie d'un générateur

Posté par . Licence CC by-sa
3
30
juin
2013

Salut !

j'ai un générateur "gen" qui me génère des caractères (type str), je souhaite écrire ce qui est généré dans un fichier.
Je vois deux solutions évidentes pour ça :

# solution 1
for c in gen():
    f.write(c)

# solution 2
out = ''.join(gen())
f.write(out)

(merci de ne pas tenir compte du nom des variables :p)

avantage de la solution 1 : si le générateur sort 1Go de données, on n'a pas besoin de tout charger en mémoire (c'est un peu le but du truc)
avantage de la solution 2 : c'est environ 5 fois plus rapide… (sur un test où le générateur sort 100ko de caractères)

Voyez-vous une alternative pas trop moche qui allie la vitesse de solution 2 avec une utilisation mémoire limitée ? (en python3)

Merci !

  • # Mémoire tampon

    Posté par . Évalué à 6.

    Si votre générateur retourne un seul caractère à la fois, alors chaque caractère sera écrit, ce qui est lent. À l'inverse, la seconde méthode lit tous les caractères à la fois, comme si le générateur avait retourné tous les caractères à la fois, et écrit en une seule passe.

    Vous pouvez avoir les avantages des deux méthodes en faisant une solution intermédiaire (entre "écrire 1 caractère" et "écrire tous les caractères") des 2 méthodes.
    Vous pouvez modifier le générateur pour qu'il retourne plusieurs caractères à la fois, par exemple 2 caractères, ou 10, ou encore 4096 (n'hésitez pas à utiliser un bloc de plusieurs kilos, les systèmes de fichiers fonctionnent de toute manière par blocs, à plus forte raison sur les SSD).
    Si vous ne pouvez pas modifier le générateur, vous pouvez quand même utiliser une variable "out" qui va accumuler 4096 caractères du générateur, et les écrire par paquets de 4096. Aussi, cette fonction pourra vous être utile.

  • # Bufferiser soi-même, ou laisser l'OS faire

    Posté par . Évalué à 3.

    Je vois deux solutions :

    1) Utiliser un StringIO, que tu remplies de X caractères, et que tu écris régulièrement dans ton fichier, genre

    import io
    s = io.BytesIO()
    for c in gen():
        for i in range(4096):
            s.write(c)
        f.write(s)
        s.truncate(0)
    

    cf http://docs.python.org/3.0/library/io.html#io.BytesIO (je suppose que ce sera plutôt des bytes que des caractères que tu écriras)
    C'est un peu équivalent à la solotion proposée par BFG.

    2) Mapper le fichier en mémoire, et écrire directement les caractères dans ce tableau :

    import mmap
    mm = mmap.mmap(f.fileno, 1000000)
    for i, c in enumerate(gen()):
        mm[i] = c
    f.flush()
    

    cf http://docs.python.org/3/library/mmap.html

    • [^] # Re: Bufferiser soi-même, ou laisser l'OS faire

      Posté par . Évalué à 4.

      Attention, dans votre premier collage de code, vous écrivez 4096 fois le même caractère.
      Voici à quoi ça aurait ressemblé avec islice :

      import itertools
      
      iter_gen = gen()
      while True:
          block = ''.join(itertools.islice(iter_gen, 4096))
          if not block:
              break
          f.write(block)
      • [^] # Re: Bufferiser soi-même, ou laisser l'OS faire

        Posté par . Évalué à 1.

        iter_gen = gen()
        for block in iter(lambda: ''.join(itertools.islice(iter_gen, 4096)), ''):
            sys.stdout.write(block)

        Les résultats sont grosso modo similaires à la solution "join unique".
        (j'aime pas trop le lambda ici mais bon, avec partial dans ce cas ça aurait été moins clair)

        merci

      • [^] # Re: Bufferiser soi-même, ou laisser l'OS faire

        Posté par . Évalué à 2.

        Attention, dans votre premier collage de code, vous écrivez 4096 fois le même caractère.

        Oups, faute d'inattention, merci. (et tu peux me tutoyer ;-))

        Bon, comme je préfère sans join(), je propose un autre truc :

        import io
        s = io.BytesIO()
        for i, c in enumerate(gen()):
            s.write(c)
            if (i+1) % 4096 == 0:
                f.write(s)
                s.truncate(0)
        f.write(s)
        

        (bon, du coup une « simple » boucle sur i pourrait être plus lisible peut-être)

        • [^] # Re: Bufferiser soi-même, ou laisser l'OS faire

          Posté par . Évalué à 1.

          j'ai testé avec

          with io.StringIO() as s:
                  for i, c in enumerate(gen()):
                          s.write(c)
                          if (i+1) % 4096 == 0:
                                  sys.stdout.write(s.getvalue())
                                  s.truncate(0)
                  sys.stdout.write(s.getvalue())

          et j'obtiens comme résultats (en secondes):
          write direct 2.623428489023354
          join du total 0.563591810001526
          join par block 0.5687419429887086
          StringIO 2.5268013479653746

          le soucis de ta solution c'est qu'on fait un paquet d'appels à write (même si derrière y'a pas d'appel système à chaque fois)

          • [^] # Re: Bufferiser soi-même, ou laisser l'OS faire

            Posté par . Évalué à 2.

            le soucis de ta solution c'est qu'on fait un paquet d'appels à write (même si derrière y'a pas d'appel système à chaque fois)

            Moui, effectivement ; je ne pensais pas que ça donnerait de si mauvaise perfs…

            Merci pour les résultats.

Suivre le flux des commentaires

Note : les commentaires appartiennent à ceux qui les ont postés. Nous n'en sommes pas responsables.