Длина выхода генератора
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 и (насколько я знаю) должно охватывать все мыслимые виды итераций.