Как проверить, что итеративный допускает более одного прохода? - PullRequest
8 голосов
/ 25 января 2012

В Python 3, как я могу проверить, является ли объект контейнером (а не итератором, который может разрешить только один проход)?

Вот пример:

def renormalize(cont):
    '''
    each value from the original container is scaled by the same factor
    such that their total becomes 1.0
    '''
    total = sum(cont)
    for v in cont:
        yield v/total

list(renormalize(range(5))) # [0.0, 0.1, 0.2, 0.3, 0.4]
list(renormalize(k for k in range(5))) # [] - a bug!

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

В идеале я хотел бы сделать это:

def renormalize(cont):
    if not is_container(cont):
      raise ContainerExpectedException
    # ...

Как я могу реализоватьis_container?

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

Я, конечно, могу переписать функцию renormalize для работыправильно с однопроходным итератором.Но это требует копирования входных данных в контейнер.Влияние на производительность копирования миллионов больших списков «на всякий случай, если они не списки» смешно.

РЕДАКТИРОВАТЬ: Мой оригинальный пример использовал функцию weighted_average:

def weighted_average(c):
    '''
    returns weighted average of a container c
    c contains values and weights in tuples
    weights don't need to sum up 1 (automatically renormalized)
    '''
    return sum((v * w for v, w in c)) / sum((w for v, w in c))

weighted_average([(0,1), (1,1)]) #0.5 
weighted_average([(k, 1) for k in range(2)]) #0.5
weighted_average((k, 1) for k in range(2)) #mistake

Но этобыл не лучшим примером, так как версия weighted_average, переписанная для использования одного прохода, в любом случае, возможно, лучше:

def weighted_average(it):
    '''
    returns weighted average of an iterator it
    it yields values and weights in tuples
    weights don't need to sum up 1 (automatically renormalized)
    '''
    total_value = 0
    total_weight = 0
    for v, w in it:
        total_value += v
        total_weight += w
    return total_value / total_weight

Ответы [ 3 ]

4 голосов
/ 25 января 2012

Хотя все итерируемые объекты должны быть подклассами. И, к сожалению, не все из них делают.Вот ответ, основанный на том, какой интерфейс реализуют объекты, а не на том, что они «объявляют».

Краткий ответ:

"контейнер", как вы его называете, то есть список / кортеж, который можно повторять более одного раза, в отличие от генератора, которыйбудет исчерпан, как правило, реализует как __iter__, так и __getitem__.Следовательно, вы можете сделать это:

>>> def is_container_iterable(o):
...     return hasattr(o, '__iter__') and hasattr(o, '__getitem__')
... 
>>> is_container_iterable([])
True
>>> is_container_iterable(())
True
>>> is_container_iterable({})
True
>>> is_container_iterable(range(5))
True
>>> is_container_iterable(iter([]))
False

Длинный ответ:

Однако вы можете сделать итерацию, которая не будет исчерпана и не будет поддерживать getitem.Например, функция, которая генерирует простые числа.Вы можете повторить генерацию много раз, если хотите, но наличие функции для получения 1065-го простого числа потребует значительных вычислений, поэтому вы можете не захотеть поддерживать это.: -)

Так есть ли более "надежный" способ?

Ну, все итерируемые будут реализовывать функцию __iter__, которая будет возвращать итератор.Итераторы будут иметь функцию __next__.Это то, что используется при итерации по нему.Повторный вызов __next__ в конечном итоге приведет к исчерпанию итератора.

Так что, если он имеет функцию __next__, он является итератором и будет исчерпан.

>>> def foo():
...    for x in range(5):
...        yield x
... 
>>> f = foo()
>>> f.__next__
<method-wrapper '__next__' of generator object at 0xb73c02d4>

Итерации, которые являютсяеще итераторы не будут иметь функцию __next__, но будут реализовывать функцию __iter__, которая будет возвращать итерацию:

>>> r = range(5)
>>> r.__next__
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'range' object has no attribute '__next__'
>>> ri = iter(r)
>>> ri.__next__
<method-wrapper '__next__' of range_iterator object at 0xb73bef80>

Таким образом, вы можете проверить, что объект имеет __iter__, но онне имеет __next__.

>>> def is_container_iterable(o):
...     return hasattr(o, '__iter__') and not hasattr(o, '__next__')
... 
>>> is_container_iterable(())
True
>>> is_container_iterable([])
True
>>> is_container_iterable({})
True
>>> is_container_iterable(range(5))
True
>>> is_container_iterable(iter(range(5)))
False

Итераторы также имеют функцию __iter__, которая будет возвращать себя.

>>> iter(f) is f
True
>>> iter(r) is r
False
>>> iter(ri) is ri
True

Следовательно, вы можете выполнить следующие варианты проверки:

>>> def is_container_iterable(o):
...     return iter(o) is not o
... 
>>> is_container_iterable([])
True
>>> is_container_iterable(())
True
>>> is_container_iterable({})
True
>>> is_container_iterable(range(5))
True
>>> is_container_iterable(iter([]))
False

Это не получится, если вы реализуете объект, который возвращает сломанный итератор, тот, который не возвращает self, когда вы снова вызываете iter ().Но тогда ваш код (или модули сторонних производителей) на самом деле делает что-то не так.

Это зависит от создания итератора и, следовательно, от вызова объектов __iter__, что в теории может иметь побочные эффектыв то время как вышеупомянутые вызовы hasattr не должны иметь побочных эффектов.Итак, он вызывает getattribute , который мог бы иметь.Но вы можете исправить это следующим образом:

>>> def is_container_iterable(o):
...     try:
...         object.__getattribute__(o, '__iter__')
...     except AttributeError:
...         return False
...     try:
...         object.__getattribute__(o, '__next__')
...     except AttributeError:
...         return True
...     return False
... 
>>> is_container_iterable([])
True
>>> is_container_iterable(())
True
>>> is_container_iterable({})
True
>>> is_container_iterable(range(5))
True
>>> is_container_iterable(iter(range(5)))
False

Это достаточно безопасно и должно работать во всех случаях, кроме случаев, когда объект генерирует __next__ или __iter__ динамически при __getattribute__ вызовах, но если высделай это, ты безумен: -)

Инстинктивно моя предпочтительная версия была бы iter(o) is o, но мне никогда не нужно было это делать, так что это не основано на опыте.

3 голосов
/ 25 января 2012

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

if isinstance(it, collections.Iterator):
    # handle the iterator case

Лично я нахожу вашу версию средневзвешенного значения, удобную для итераторов, гораздо легче читать, чем версию с несколькими списками / суммами. : -)

1 голос
/ 25 января 2012

Лучше всего использовать абстрактную инфраструктуру базового класса:

def weighted_average(c):
    if not isinstance(c, collections.Sequence):
      raise ContainerExpectedException
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...