Длина выходного сигнала генератора
Python предоставляет хороший метод для получения длины нетерпеливого, iterable, len(x)
. Но я не мог найти ничего подобного для ленивых итераций, представленных функциями и функциями генератора. Конечно, нетрудно написать что-то вроде:
def iterlen(x):
n = 0
try:
while True:
next(x)
n += 1
except StopIteration: pass
return n
Но я не могу избавиться от чувства, что я переоцениваю велосипед.
(Пока я печатал эту функцию, я подумал: может быть, нет такой функции, потому что она "уничтожает" ее аргумент. Однако не проблема для моего случая.)
P.S.: в отношении первых ответов - да, что-то вроде len(list(x))
тоже будет работать, но это значительно увеличивает использование памяти.
P.P.S.: повторная проверка... Не обращайте внимания на P.S., кажется, я ошибся, пытаясь это сделать, он отлично работает. Извините за неприятности.
Ответы
Ответ 1
Существует не один, потому что вы не можете сделать это в общем случае - что, если у вас есть ленивый бесконечный генератор? Например:
def fib():
a, b = 0, 1
while True:
a, b = b, a + b
yield a
Это никогда не заканчивается, но будет генерировать числа Фибоначчи. Вы можете получить столько чисел Фибоначчи, сколько хотите, набрав next()
.
Если вам действительно нужно знать количество элементов, которые есть, вы не можете проходить через них линейно один раз в любом случае, поэтому просто используйте другую структуру данных, такую как обычный список.
Ответ 2
Самый простой способ - это просто sum(1 for _ in gen)
, где gen - ваш генератор.
Ответ 3
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)
Ответ 4
Вы можете использовать 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
Решение Kamil Kisiel лучше:
def count_iterable(i):
return sum(1 for e in i)
Ответ 5
Используйте уменьшить (функция, iterable [, initializer]) для эффективного функционального решения с памятью:
>>> iter = "This string has 30 characters."
>>> reduce(lambda acc, e: acc + 1, iter, 0)
30
Ответ 6
По определению, только некоторое подмножество генераторов вернется после некоторого количества аргументов (имеет заданную длину), и даже тогда только подмножество этих конечных генераторов имеет предсказуемый конец (доступ к генератору может иметь сторону -эффекты, которые могли бы остановить генератор раньше).
Если вы хотите внедрить методы длины для своего генератора, вы должны сначала определить, что вы считаете "длиной" (это общее количество элементов? количество оставшихся элементов?), а затем обернуть ваш генератор в классе, Вот пример:
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]
Ответ 7
Это взломать, но если вы действительно хотите, чтобы 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 и (насколько мне известно) должны охватывать все мыслимые типы итераций.
Ответ 8
Попробуйте more_itertools
пакет для простого решения. Пример:
>>> import more_itertools
>>> it = iter("abcde") # sample generator
>>> it
<str_iterator at 0x4ab3630>
>>> more_itertools.ilen(it)
5
См. этот пост для другого применяемого примера.