2番目のものが短いzip圧縮されたPythonジェネレーター:サイレントに消費される要素を取得する方法


50

(潜在的に)異なる長さの2つのジェネレーターを解析したいzip

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

ただし、gen2要素の数が少ない場合、の1つの追加要素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()を使用してください。」
カルシジェネート

2
@ Ch3steR。しかし、問題は「なぜ」とは何の関係もありません。それは文字通り「この欠落している値を取得する方法はありますか?」私の答え以外のすべての答えは都合よくその部分を読むのを忘れたようです。
マッド物理学者

@MadPhysicistおかしい。私はその点についてより明確になるように質問を言い換えました。
Jean-Francois T.

1
基本的な問題は、ジェネレータをのぞき見したり押し戻したりする方法がないことです。したがって、から一度zip()読んだら、それはなくなっています。8gen1
Barmar

1
@Barmarは間違いなく、全員が同意しました。問題は、それを使用できるようにどこに保存するかということでした。
Jean-Francois T.

回答:


28

1つの方法は、最後の値をキャッシュできるジェネレータを実装することです。

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振る舞いを変えないという事実...とても悪くて対称的ではありません(切り替えgen1gen2ジップで異なる結果が導かれます)乾杯
Jean-Francois T.

1
@ジャン・フランソワ。last使い果たされた呼び出しに正しく応答するようにイテレーターを更新しました。これは、最後の値が必要かどうかを判断するのに役立ちます。また、より生産的になります。
マッド物理学者

@MadPhysicist私はコードを実行し、その出力print(gen1.last) print(next(gen1)) None and 9
Ch3steR

@MadPhysicistといくつかのドキュメント文字列。いいね;)時間があるときに後でチェックします。過ごした時間をありがとう
Jean-Francois T.

@ Ch3steR。キャッチありがとうございます。興奮しすぎて、returnステートメントをから削除しました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が満たされ、関数はreturnを実行して停止します。今next(gen1)9を返します。

あなたの第二の例ではgen1 = gen(8)gen2 = gen(10)。両方のジェネレータが7番目の反復まで消費された後。これで、8回目の反復gen1呼び出しelem = next(it, sentinel)で戻りますsentinel(この時点gen1で使い果たされたため)。これif elem is sentinelは満たされ、関数はreturnを実行して停止します。現在next(gen2)は8を返します。

Mad Physicistの回答に触発されて、このGenラッパーを使用してそれを打ち消すことができます。

編集Jean-Francois Tが指摘したケースを処理するため

イテレーターから値が消費されると、その値はイテレーターから永久に失われ、イテレーターがイテレーターに戻すためのインプレースの変更メソッドはありません。回避策の1つは、最後に消費された値を保存することです。

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ソリューションの変更には、いくつかの制限があります。実行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の回答とTHEの回答にマークを付けます。ありがとう!
Jean-Francois T.

@ Jean-FrancoisT。はい、同意しました。あなたは彼の答えを答えとしてマークするべきです。これには制限があります。私はすべてのケースに対抗するためにこの答えを改善するよう努めます。;)
Ch3steR

@ Ch3steR必要に応じて、揺さぶるお手伝いをします。私はソフトウェア検証の分野の専門家です:)
Jean-Francois T.

@ Jean-FrancoisT。私はしたいです。それは多くのことを意味します。私は3年生です。
Ch3steR

2
私がここで書いたすべてのテストに合格しました。repl.it / @ jfthuong / MadPhysicistChester オンラインで実行できます。非常に便利です。)
Jean-Francois T.

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に呼び出すときに引数を指定することもできますが、基本的には、forループで(またはのいずれか)を押すと、他の変数にが含まれます。zip_longestNoneNoneij8


ありがとう。私は確かにすでに思いついたのですがzip_longest、それは実際に私の質問にありました。:)
Jean-Francois T.

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.teeitertools.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__てドロップされてアクセスできなくなるためです。

あなたはあなたのzipを変異させるべきです、そしてあなたはいくつかのハッキーコードでドロップされたアイテムの位置を得ることができます)

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))
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.