Генераторы на молнии Python, второй из которых короче: как извлечь элемент, который используется незаметно - PullRequest
50 голосов
/ 09 апреля 2020

Я хочу проанализировать 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 делает).

NOTE : В настоящее время я реализовал по-другому, используя itertools.zip_longest но мне действительно интересно, как получить это пропущенное значение, используя zip или его эквивалент.

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

Ответы [ 7 ]

28 голосов
/ 09 апреля 2020

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

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)) 

It важно сделать 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]))

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

17 голосов
/ 09 апреля 2020

Это 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`
6 голосов
/ 11 апреля 2020

Вдохновленный разъяснением @ 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

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

    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
6 голосов
/ 10 апреля 2020

Я вижу, что вы уже нашли этот ответ, и он упоминался в комментариях, но я решил, что из этого сделаю ответ. Вы хотите использовать 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

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

4 голосов
/ 12 апреля 2020

вы можете использовать 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 голосов
/ 11 апреля 2020

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

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 голосов
/ 11 апреля 2020

Я не думаю, что вы можете получить отброшенное значение с базисом c для l oop, потому что исчерпанный итератор, взятый из 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))
...