Генератори на Zipped 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 : Я створив кілька тестів різних реалізацій в цьому реплєї в разі , якщо ви хочете , щоб представити і спробувати нову реалізацію :) https://repl.it/@jfthuong/MadPhysicistChester


19
Документи зазначають, що "zip () слід використовувати лише з входами неоднакової довжини, коли вам не байдуже тривалі, незрівняні значення з довших ітерабелів. Якщо ці значення важливі, використовуйте замість нього itertools.zip_logest ()."
Carcigenicate

2
@ Ch3steR. Але питання не має нічого спільного з "чому". Буквально написано "Чи є спосіб отримати це відсутнє значення ...?" Здається, що всі відповіді, але мої зручно забули прочитати цю частину.
Божевільний фізик

@ MadPhysicist Странно справді. Я перефразував питання, щоб бути зрозумілішим у цьому аспекті.
Жан-Франсуа Т.

1
Основна проблема полягає в тому, що немає можливості зазирнути або відштовхнутися назад в генератор. Тому , як тільки zip()прочитав 8з gen1, це пішло.
Бармар

1
@Barmar, безумовно, ми всі з цим домовилися. Питання було більше в тому, як його десь зберігати, щоб мати можливість ним користуватися.
Жан-Франсуа Т.

Відповіді:


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поштовий замовлення призведе до різних результатів). Привіт
Жан-Франсуа Т.

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)

У вашому 1-му прикладі 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.

У вашому 2-му прикладі 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 та відповідь. Дякую!
Жан-Франсуа Т.

@ Жан-ФрансуаT. Так, погоджено. Ви повинні позначити його відповідь як відповідь. Це має обмеження. Я спробую вдосконалити цю відповідь, щоб протиставити всі випадки. ;)
Ch3steR

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

@ Жан-ФрансуаT. Я б хотів. Це означало б багато чого. Я студентка 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

я не думаю, що ви можете отримати скинуте значення за допомогою базового для циклу, тому що вичерпаний ітератор, взятий з 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.