web-dev-qa-db-fra.com

Longueur de sortie du générateur

Python fournit une méthode agréable pour obtenir la longueur d'un itérable désirable, len(x) c'est-à-dire. Mais je n'ai rien trouvé de semblable pour les itérables paresseux représentés par des fonctions et des compréhensions de générateur. Bien sûr, il n'est pas difficile d'écrire quelque chose comme:

def iterlen(x):
  n = 0
  try:
    while True:
      next(x)
      n += 1
  except StopIteration: pass
  return n

Mais je ne peux pas me débarrasser du sentiment que je réimplémente un vélo.

(Pendant que je tapais la fonction, une pensée m'a frappé: peut-être qu'il n'y a vraiment pas une telle fonction, car elle "détruit" son argument. Pas un problème pour mon cas, cependant).

P.S .: concernant les premières réponses - oui, quelque chose comme len(list(x)) fonctionnerait aussi, mais cela augmente considérablement l'utilisation de la mémoire.

P.P.S .: revérifié ... Ne tenez pas compte du P.S., il semble que j'ai fait une erreur en essayant cela, cela fonctionne très bien. Désolé pour le dérangement.

116
Maxim

Il n'y en a pas parce que vous ne pouvez pas le faire dans le cas général - et si vous avez un générateur infini paresseux? Par exemple:

def fib():
    a, b = 0, 1
    while True:
        a, b = b, a + b
        yield a

Cela ne se termine jamais mais générera les numéros de Fibonacci. Vous pouvez obtenir autant de numéros de Fibonacci que vous le souhaitez en appelant next().

Si vous avez vraiment besoin de connaître le nombre d'éléments qu'il y a, vous ne pouvez pas les parcourir de manière linéaire une fois de toute façon, alors utilisez simplement une structure de données différente telle qu'une liste régulière.

32
Adam Rosenfield

Le moyen le plus simple est probablement juste sum(1 for _ in gen) où gen est votre générateur.

223
Matt Dunham
def count(iter):
    return sum(1 for _ in iter)

Ou mieux encore:

def count(iter):
    try:
        return len(iter)
    except TypeError:
        return sum(1 for _ in iter)

S'il n'est pas itérable, il lancera un TypeError.

Ou, si vous voulez compter quelque chose de spécifique dans le générateur:

def count(iter, key=None):
    if key:
        if callable(key):
            return sum(bool(key(x)) for x in iter)
        return sum(x == key for x in iter)
    try:
        return len(iter)
    except TypeError:
        return sum(1 for _ in iter)
17
mpen

Donc, pour ceux qui voudraient connaître le résumé de cette discussion. Les meilleurs scores finaux pour compter une expression de générateur de 50 millions de long en utilisant:

  • len(list(gen)),
  • len([_ for _ in gen]),
  • sum(1 for _ in gen),
  • ilen(gen) (de more_itertool ),
  • reduce(lambda c, i: c + 1, gen, 0),

triés par performances d'exécution (y compris la consommation mémoire), vous surprendront:

`` ''

1: test_list.py:8: 0,492 Ko

gen = (i for i in data*1000); t0 = monotonic(); len(list(gen))

('list, sec', 1.9684218849870376)

2: test_list_compr.py:8: 0,867 Ko

gen = (i for i in data*1000); t0 = monotonic(); len([i for i in gen])

('list_compr, sec', 2.5885991149989422)

3: test_sum.py:8: 0,859 Ko

gen = (i for i in data*1000); t0 = monotonic(); sum(1 for i in gen); t1 = monotonic()

('somme, sec', 3.441088170016883)

4: more_itertools/more.py: 413: 1,266 Kio

d = deque(enumerate(iterable, 1), maxlen=1)

test_ilen.py:10: 0.875 KiB
gen = (i for i in data*1000); t0 = monotonic(); ilen(gen)

("ilen, sec", 9.812256851990242)

5: test_reduce.py:8: 0,859 Ko

gen = (i for i in data*1000); t0 = monotonic(); reduce(lambda counter, i: counter + 1, gen, 0)

('réduire, sec', 13.436614598002052) `` '

Ainsi, len(list(gen)) est le consommable de mémoire le plus fréquent et le moins

9
Alex-Bogdanov

Utilisez réduire (fonction, itérable [ initialiseur]) pour une solution purement efficace et efficace en mémoire:

>>> iter = "This string has 30 characters."
>>> reduce(lambda acc, e: acc + 1, iter, 0)
30
5
OlivierBlanvillain

Essaie le more_itertools package pour une solution simple. Exemple:

>>> import more_itertools

>>> it = iter("abcde")                                         # sample generator
>>> it
<str_iterator at 0x4ab3630>

>>> more_itertools.ilen(it)
5

Voir ce post pour un autre exemple appliqué.

3
pylang

Par définition, seul un sous-ensemble de générateurs retournera après un certain nombre d'arguments (ont une longueur prédéfinie), et même alors, seul un sous-ensemble de ces générateurs finis a une fin prévisible (l'accès au générateur peut avoir des effets secondaires qui pourrait arrêter le générateur plus tôt).

Si vous souhaitez implémenter des méthodes de longueur pour votre générateur, vous devez d'abord définir ce que vous considérez comme la "longueur" (est-ce le nombre total d'éléments? Le nombre d'éléments restants?), Puis envelopper votre générateur dans une classe. Voici un exemple:

class MyFib(object):
    """
    A class iterator that iterates through values of the
    Fibonacci sequence, until, optionally, a maximum length is reached.
    """

    def __init__(self, length):
        self._length = length
        self._i = 0

     def __iter__(self):
        a, b = 0, 1
        while not self._length or self._i < self._length:
            a, b = b, a + b
            self._i += 1
            yield a

    def __len__(self):
        "This method returns the total number of elements"
        if self._length:
            return self._length
        else:
            raise NotImplementedError("Infinite sequence has no length")
            # or simply return None / 0 depending
            # on implementation

Voici comment l'utiliser:

In [151]: mf = MyFib(20)

In [152]: len(mf)
Out[152]: 20

In [153]: l = [n for n in mf]

In [154]: len(l)
Out[154]: 20

In [155]: l
Out[155]: 
[1,
 1,
 2,
...
6765]


In [156]: mf0 = MyFib(0)

In [157]: len(mf0)
---------------------------------------------------------------------------
NotImplementedError                       Traceback (most recent call last)
<ipython-input-157-2e89b32ad3e4> in <module>()
----> 1 len(mf0)

/tmp/ipython_edit_TWcV1I.py in __len__(self)
     22             return self._length
     23         else:
---> 24             raise NotImplementedError
     25             # or simply return None / 0 depending
     26             # on implementation

NotImplementedError: 

In [158]: g = iter(mf0)

In [159]: l0 = [g.next(), g.next(), g.next()]

In [160]: l0
Out[160]: [1, 1, 2]
3
sleblanc

C'est un hack, mais si vous voulez vraiment que len travaille sur un itérable général (en le consommant en chemin), vous pouvez créer votre propre version de len.

La fonction len est essentiellement équivalente à la suivante (bien que les implémentations fournissent généralement des optimisations pour éviter la recherche supplémentaire):

def len(iterable):
    return iterable.__len__()

Nous pouvons donc définir notre new_len pour essayer, et si __len__ n'existe pas, comptez nous-mêmes le nombre d'éléments en consommant l'itérable:

def new_len(iterable):
    try:
      return iterable.__len__()
    except AttributeError:
      return sum(1 for _ in iterable)

Ce qui précède fonctionne en Python 2/3, et (pour autant que je sache) devrait couvrir tous les types d'itérables imaginables.

1
yoniLavi