Длина выхода генератора

Python предоставляет хороший метод для получения длины итеративного итератора, len(x) то есть. Но я не смог найти ничего похожего для ленивых итераций, представленных генератором и функциями. Конечно, нетрудно написать что-то вроде:

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

Но я не могу избавиться от ощущения, что я перевоплощаю велосипед.

(Пока я печатал функцию, мне в голову пришла мысль: может быть, такой функции действительно нет, потому что она "уничтожает" ее аргумент. Хотя для моего случая это не проблема).

PS: по поводу первых ответов - да, что-то вроде len(list(x)) будет работать, но это резко увеличивает использование памяти.

PPS: перепроверил... Не обращая внимания на PS, кажется, я сделал ошибку при попытке этого, он работает нормально. Извините за беспокойство.

9 ответов

Решение

Нет такого, потому что вы не можете сделать это в общем случае - что если у вас есть ленивый бесконечный генератор? Например:

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

Это никогда не заканчивается, но будет генерировать числа Фибоначчи. Вы можете получить столько чисел Фибоначчи, сколько захотите, позвонив next(),

Если вам действительно нужно знать количество элементов, которые есть, то вы все равно не можете выполнять линейную итерацию по ним один раз, так что просто используйте другую структуру данных, такую ​​как обычный список.

Самый простой способ, вероятно, просто sum(1 for _ in gen) где ген ваш генератор.

Итак, для тех, кто хотел бы узнать краткое содержание этого обсуждения. Итоговые максимальные оценки для подсчета выражения генератора длиной 50 миллионов с использованием:

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

отсортированные по производительности выполнения (включая потребление памяти), удивят вас:

`` `

1: test_list.py:8: 0,492 КиБ

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

('list, sec', 1.9684218849870376)

2: test_list_compr.py:8: 0,867 КиБ

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 КиБ

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

("сумма, сек", 3.441088170016883)

4: more_itertools / more.py: 413: 1,266 КиБ

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 КиБ

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

("уменьшить, сек", 13.436614598002052) ```

Так, len(list(gen)) это самый частый и менее потребляемый объем памяти

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

Или еще лучше:

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

Если это не повторяется, он бросит TypeError,

Или, если вы хотите посчитать что-то конкретное в генераторе:

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)

Вы можете использовать enumerate(), чтобы пройти через сгенерированный поток данных, а затем вернуть последнее число - количество элементов.

Я пытался использовать itertools.count() с itertools.izip(), но безуспешно. Это лучший / самый короткий ответ, который я придумал:

#!/usr/bin/python

import itertools

def func():
    for i in 'yummy beer':
        yield i

def icount(ifunc):
    size = -1 # for the case of an empty iterator
    for size, _ in enumerate(ifunc()):
        pass
    return size + 1

print list(func())
print 'icount', icount(func)

# ['y', 'u', 'm', 'm', 'y', ' ', 'b', 'e', 'e', 'r']
# icount 10

Решение Камиля Кисиела намного лучше:

def count_iterable(i):
    return sum(1 for e in i)

По определению, только подмножество генераторов вернется после определенного количества аргументов (имеют заранее заданную длину), и даже тогда, только подмножество этих конечных генераторов имеет предсказуемый конец (доступ к генератору может иметь побочные эффекты, которые мог остановить генератор раньше).

Если вы хотите реализовать методы длины для вашего генератора, вы должны сначала определить, что вы считаете "длиной" (это общее количество элементов? Количество оставшихся элементов?), А затем обернуть ваш генератор в класс. Вот пример:

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

Вот как это использовать:

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]

Используйте Reduce(function, iterable[, initializer]) для чисто функционального решения с эффективным использованием памяти:

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

Попробуйте more_itertools пакет для простого решения. Пример:

>>> import more_itertools

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

>>> more_itertools.ilen(it)
5

Смотрите этот пост для другого примера применения.

Это взломать, но если вы действительно хотите иметь len работать над общей итерацией (потребляя это в пути), вы можете создать свою собственную версию len,

len Функция по существу эквивалентна следующему (хотя реализации обычно предоставляют некоторые оптимизации, чтобы избежать дополнительного поиска):

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

Поэтому мы можем определить наши new_len чтобы попробовать это, и если __len__ не существует, посчитайте количество элементов сами, используя итерируемое:

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

Вышеприведенное работает в Python 2/3 и (насколько я знаю) должно охватывать все мыслимые виды итераций.

Другие вопросы по тегам