Сжатые генераторы Python, у которых 2-й короче: как извлечь элемент, который используется молча


50

Я хочу проанализировать 2 генератора (потенциально) разной длины с помощью zip:

for el1, el2 in zip(gen1, gen2):
    print(el1, el2)

Однако, если gen2есть меньше элементов, один дополнительный элемент gen1«потребляется».

Например,

def my_gen(n:int):
    for i in range(n):
        yield i

gen1 = my_gen(10)
gen2 = my_gen(8)

list(zip(gen1, gen2))  # Last tuple is (7, 7)
print(next(gen1))  # printed value is "9" => 8 is missing

gen1 = my_gen(8)
gen2 = my_gen(10)

list(zip(gen1, gen2))  # Last tuple is (7, 7)
print(next(gen2))  # printed value is "8" => OK

Очевидно, что значение отсутствует ( 8в моем предыдущем примере), потому что gen1читается (таким образом генерируя значение 8) до того, как оно осознает gen2, что в нем больше нет элементов. Но эта ценность исчезает во вселенной. Когда gen2«дольше», такой «проблемы» нет.

ВОПРОС : Есть ли способ получить это пропущенное значение (т.е. 8в моем предыдущем примере)? ... в идеале с переменным количеством аргументов (как это zipделает).

ПРИМЕЧАНИЕ : я в настоящее время реализовал по-другому с помощью, itertools.zip_longestно мне действительно интересно, как получить это пропущенное значение, используя zipили эквивалентный.

ПРИМЕЧАНИЕ 2 : Я создал несколько тестов различных реализаций в этом REPL на случай, если вы захотите отправить и попробовать новую реализацию :) https://repl.it/@jfthuong/MadPhysicistChester


19
Документы отмечают, что «zip () следует использовать только с входами неравной длины, если вам не нужны конечные, несопоставленные значения из более длинных итераций. Если эти значения важны, используйте вместо этого itertools.zip_longest ().».
Carcigenicate

2
@ Ch3steR. Но вопрос не имеет ничего общего с «почему». Он буквально гласит: «Есть ли способ восстановить это пропущенное значение ...?» Кажется, что все ответы, кроме моего, удобно забыли прочитать эту часть.
Безумный физик

@MadPhysicist Странно, действительно. Я перефразировал вопрос, чтобы прояснить этот аспект.
Жан-Франсуа Т.

1
Основная проблема заключается в том, что нет способа заглянуть или толкнуть обратно в генератор. Поэтому , как только zip()прочитал 8с gen1, это пошло.
Бармар

1
@ Бармар определенно, мы все согласились с этим. Вопрос заключался в том, как хранить его где-нибудь, чтобы иметь возможность использовать его.
Жан-Франсуа Т.

Ответы:


28

Один из способов - реализовать генератор, который позволит вам кэшировать последнее значение:

class cache_last(collections.abc.Iterator):
    """
    Wraps an iterable in an iterator that can retrieve the last value.

    .. attribute:: obj

       A reference to the wrapped iterable. Provided for convenience
       of one-line initializations.
    """
    def __init__(self, iterable):
        self.obj = iterable
        self._iter = iter(iterable)
        self._sentinel = object()

    @property
    def last(self):
        """
        The last object yielded by the wrapped iterator.

        Uninitialized iterators raise a `ValueError`. Exhausted
        iterators raise a `StopIteration`.
        """
        if self.exhausted:
            raise StopIteration
        return self._last

    @property
    def exhausted(self):
        """
        `True` if there are no more elements in the iterator.
        Violates EAFP, but convenient way to check if `last` is valid.
        Raise a `ValueError` if the iterator is not yet started.
        """
        if not hasattr(self, '_last'):
            raise ValueError('Not started!')
        return self._last is self._sentinel

    def __next__(self):
        """
        Retrieve, record, and return the next value of the iteration.
        """
        try:
            self._last = next(self._iter)
        except StopIteration:
            self._last = self._sentinel
            raise
        # An alternative that has fewer lines of code, but checks
        # for the return value one extra time, and loses the underlying
        # StopIteration:
        #self._last = next(self._iter, self._sentinel)
        #if self._last is self._sentinel:
        #    raise StopIteration
        return self._last

    def __iter__(self):
        """
        This object is already an iterator.
        """
        return self

Чтобы использовать это, оберните входы в zip:

gen1 = cache_last(range(10))
gen2 = iter(range(8))
list(zip(gen1, gen2))
print(gen1.last)
print(next(gen1)) 

Важно сделать gen2итератор, а не итеративный, чтобы вы могли знать, какой из них был исчерпан. Если gen2исчерпан, вам не нужно проверять gen1.last.

Другой подход заключается в переопределении zip для принятия изменяемой последовательности итераций вместо отдельных итераций. Это позволит вам заменить итерации на цепочечную версию, включающую ваш «заглядывающий» элемент:

def myzip(iterables):
    iterators = [iter(it) for it in iterables]
    while True:
        items = []
        for it in iterators:
            try:
                items.append(next(it))
            except StopIteration:
                for i, peeked in enumerate(items):
                    iterables[i] = itertools.chain([peeked], iterators[i])
                return
            else:
                yield tuple(items)

gens = [range(10), range(8)]
list(myzip(gens))
print(next(gens[0]))

Такой подход проблематичен по многим причинам. Он не только потеряет исходную итерацию, но и потеряет все полезные свойства, которые мог иметь исходный объект, заменив его chainобъектом.


@MadPhysicist. Мне нравится ваш ответ cache_last, и тот факт, что он не меняет nextповедение ... так плохо, что это не симметрично (переключение gen1и gen2в zip приведет к разным результатам). Приветствия
Жан-Франсуа Т.

1
@ Жан-Франсуа. Я обновил итератор для правильного ответа на lastвызовы после его исчерпания. Это должно помочь выяснить, нужно ли вам последнее значение или нет. Также делает его более производительным.
Безумный физик

@MadPhysicist Я побежал код и выход print(gen1.last) print(next(gen1)) ISNone and 9
Ch3steR

@MadPhysicist с некоторыми документами и все. Приятно;) Я проверю позже, когда у меня будет время. Спасибо за потраченное время
Жан-Франсуа Т.

@ Ch3steR. Спасибо за улов. Я был слишком взволнован и удалил заявление о возврате last.
Безумный физик

17

Это zipэквивалент реализации, указанный в документации

def zip(*iterables):
    # zip('ABCD', 'xy') --> Ax By
    sentinel = object()
    iterators = [iter(it) for it in iterables]
    while iterators:
        result = []
        for it in iterators:
            elem = next(it, sentinel)
            if elem is sentinel:
                return
            result.append(elem)
        yield tuple(result)

В вашем первом примере gen1 = my_gen(10)и gen2 = my_gen(8). После того как оба генератора расходуются до 7-й итерации. Теперь в 8-ой итерации gen1вызовы, elem = next(it, sentinel)которые возвращают 8, но когда gen2вызовы elem = next(it, sentinel)возвращаются sentinel(потому что при этом gen2исчерпаны) и if elem is sentinelвыполняются, а функция выполняет возврат и останавливается. Сейчас next(gen1)возвращается 9.

В вашем втором примере gen1 = gen(8)и gen2 = gen(10). После того как оба генератора расходуются до 7-й итерации. Теперь в 8-й итерации gen1вызовы, elem = next(it, sentinel)которые возвращаются sentinel(потому что в этот момент gen1исчерпаны) и if elem is sentinelвыполняются, а функция выполняет возврат и останавливается. Теперь next(gen2)возвращает 8.

Вдохновленный ответом Безумного Физика , вы можете использовать эту Genобертку, чтобы противостоять ей:

Изменить : для обработки случаев, указанных Жан-Франсуа Т.

Как только значение получено из итератора, оно навсегда исчезает из итератора, и для итераторов не существует метода мутирования на месте, чтобы добавить его обратно в итератор. Один из способов - сохранить последнее использованное значение.

class Gen:
    def __init__(self,iterable):
        self.d = iter(iterable)
        self.sentinal = object()
        self.prev = self.sentinal
    def __iter__(self):
        return self
    @property
    def last_val_consumed(self):
        if self.prev is None:
            raise StopIteration
        if self.prev == self.sentinal:
            raise ValueError('Nothing has been consumed')
        return self.prev
    def __next__(self):
        self.prev = next(self.d,None)
        if self.prev is None:
            raise StopIteration
        return self.prev

Примеры:

# When `gen1` is larger than `gen2`
gen1 = Gen(range(10))
gen2 = Gen(range(8))
list(zip(gen1,gen2))
# [(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6), (7, 7)]
gen1.last_val_consumed
# 8 #as it was the last values consumed
next(gen1)
# 9
gen1.last_val_consumed
# 9

# 2. When `gen1` or `gen2` is empty
gen1 = Gen(range(0))
gen2 = Gen(range(5))
list(zip(gen1,gen2))
gen1.last_val_consumed
# StopIteration error is raised
gen2.last_val_consumed
# ValueError is raised saying `ValueError: Nothing has been consumed`

Спасибо @ Ch3steR за время, потраченное на эту проблему. Ваша модификация решения MadPhysicist имеет несколько ограничений: # 1. Если gen1 = cache_last(range(0))и gen2 = cache_last(range(2))после этого list(zip(gen1, gen2), вызов next(gen2)вызовет AttributeError: 'cache_last' object has no attribute 'prev'. # 2. Если gen1 длиннее, чем gen2, после использования всех элементов next(gen2)будет продолжать возвращать последнее значение вместо StopIteration. Я отмечу MadPhysicist ответ и ответ. Спасибо!
Жан-Франсуа Т.

@ Жан-Francoist. Да согласился. Вы должны пометить его ответ как ответ. Это имеет ограничения. Я постараюсь улучшить этот ответ, чтобы противостоять всем случаям. ;)
Ch3steR

@ Ch3steR Я могу помочь вам встряхнуть, если хотите. Я профессионал в области проверки программного обеспечения :)
Жан-Франсуа Т.

@ Жан-Francoist. Мне бы хотелось. Это будет много значить. Я студентка 3 курса.
Ch3steR

2
Хорошая работа, он проходит все тесты, которые я написал здесь: repl.it/@jfthuong/MadPhysicistChester Вы можете запустить их онлайн, довольно удобно :)
Жан-Франсуа Т.

6

Я вижу, вы уже нашли этот ответ, и он упоминался в комментариях, но я решил, что из этого получу ответ. Вы хотите использовать itertools.zip_longest(), который заменит пустые значения более короткого генератора на None:

import itertools

def my_gen(n:int):
    for i in range(n):
        yield i

gen1 = my_gen(10)
gen2 = my_gen(8)

for i, j in itertools.zip_longest(gen1, gen2):
    print(i, j)

Печать:

0 0
1 1
2 2
3 3
4 4
5 5
6 6
7 7
8 None
9 None

Вы также можете указать fillvalueаргумент при вызове zip_longestдля замены на Noneзначение по умолчанию, но в основном для вашего решения, когда вы нажмете None(или, iили j) в цикле for, другая переменная будет иметь вашу 8.


Спасибо. Я действительно уже придумал, zip_longestи это было в моем вопросе на самом деле. :)
Жан-Франсуа Т.

6

Вдохновленные разъяснениями @ GrandPhuba о zip, давайте создадим «безопасный» вариант ( здесь протестировано на модуле ):

def safe_zip(*args):
    """
    Safe zip that restores last consumed element in eachgenerator
    if not able to consume an element in all of them

    Returns:
        * generators in tuple
        * generator for zipped generators
    """
  continue_ = True
  n = len(args)
  result = (_ for _ in [])
  while continue_:
    addend = []
    for i, gen in enumerate(args):
      try:
        value = next(gen)
        addend.append(value)
      except StopIteration:
        genlist = list(args)
        args = tuple([chain([v], g) for v, g in zip(addend, genlist[:i])]+genlist[i:])
        continue_ = False
        break
    if len(addend)==n: result = chain(result, [tuple(addend)])
  return args, result

Вот базовый тест:

    g1, g2 = (i for i in range(10)), (i for i in range(4))
    # Create (g1, g2), g3 first, then loop over g3 as one would with zip
    (g1, g2), g3 = safe_zip(g1, g2)
    for a, b in g3:
        print(a, b)#(0, 0) to (3, 3)
    for x in g1:
        print(x)#4 to 9

4

Вы можете использовать itertools.tee и itertools.islice :

from itertools import islice, tee

def zipped(gen1, gen2, pred=list):
    g11, g12 = tee(gen1)
    z = pred(zip(g11, gen2))

    return (islice(g12, len(z), None), gen2), z

gen1 = iter(range(10))
gen2 = iter(range(5))

(gen1, gen2), output = zipped(gen1, gen2)

print(output)
print(next(gen1))
# [(0, 0), (1, 1), (2, 2), (3, 3), (4, 4)]
# 5

3

Если вы хотите повторно использовать код, самое простое решение:

from more_itertools import peekable

a = peekable(a)
b = peekable(b)

while True:
    try:
        a.peek()
        b.peek()
    except StopIteration:
        break
    x = next(a)
    y = next(b)
    print(x, y)


print(list(a), list(b))  # Misses nothing.

Вы можете проверить этот код, используя ваши настройки:

def my_gen(n: int):
    yield from range(n)

a = my_gen(10)
b = my_gen(8)

Он напечатает:

0 0
1 1
2 2
3 3
4 4
5 5
6 6
7 7
[8, 9] []

2

Я не думаю, что вы можете получить отброшенное значение с помощью базового цикла for, потому что исчерпан итератор, взятый из zip(..., ...).__iter__отброшенного после исчерпания, и вы не можете получить к нему доступ.

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

z = zip(range(10), range(8))
for _ in iter(z.__next__, None):
    ...
_, (one, other) = z.__reduce__()
_, (i_one,), p_one = one.__reduce__() # p_one == current pos, 1 based
import itertools
val = next(itertools.islice(iter(i_one), p_one - 1, p_one))
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.