เครื่องกำเนิด 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 แปลกแน่นอน ฉัน rephrased คำถามที่จะชัดเจนในด้านนั้น
Jean-Francois T.

1
ปัญหาพื้นฐานคือไม่มีทางที่จะมองหรือดันกลับเข้าไปในเครื่องกำเนิดไฟฟ้า ดังนั้นเมื่อzip()ได้อ่าน8จากgen1มันหายไป
Barmar

1
@Barar แน่นอนเราทุกคนเห็นด้วยกับที่ คำถามคือวิธีการเก็บไว้ที่ไหนสักแห่งเพื่อให้สามารถใช้งานได้มากกว่า
Jean-Francois T.

คำตอบ:


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ตัววนซ้ำนั้นสำคัญกว่าการทำซ้ำดังนั้นคุณจึงสามารถรู้ได้ว่าตัวใดตัวหนึ่งถูกใช้หมด ถ้าหมดคุณไม่จำเป็นต้องตรวจสอบgen2gen1.last

อีกวิธีคือการแทนที่ zip เพื่อยอมรับลำดับที่ผันแปรได้ของ iterables แทนที่จะแยก iterables ที่จะช่วยให้คุณสามารถแทนที่มันเป็นรุ่นที่ถูกโยงซึ่งรวมถึงรายการ "peeked" ของคุณ:

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 จะนำไปสู่ผลลัพธ์ที่แตกต่างกัน) เก้าอี้
Jean-Francois T.

1
@ Jean-Francois ฉันได้อัพเดตตัววนซ้ำเพื่อตอบสนองการlastโทรอย่างถูกต้องหลังจากที่หมดแล้ว ที่จะช่วยในการหาถ้าคุณต้องการค่าสุดท้ายหรือไม่ ทำให้การผลิตมากขึ้นด้วย
นักฟิสิกส์บ้า

@ MadPhysicist ฉันรันโค้ดและผลลัพธ์ของprint(gen1.last) print(next(gen1)) คือNone and 9
Ch3steR

@MadPhysicist ที่มีเอกสารและทั้งหมด ดี;) ฉันจะตรวจสอบในภายหลังเมื่อฉันมีเวลา ขอบคุณสำหรับเวลาที่ใช้
Jean-Francois T.

@ 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 ซ้ำ ตอนนี้ในการgen1เรียกซ้ำครั้งที่ 8 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 ซ้ำ ตอนนี้ในการgen1เรียกซ้ำครั้งที่ 8 elem = next(it, sentinel)ซึ่งส่งคืนsentinel(เพราะ ณ จุดgen1นี้หมดลง) และif elem is sentinelเป็นที่พอใจและฟังก์ชั่นการดำเนินการกลับและหยุด ตอนนี้next(gen2)ส่งคืน 8

แรงบันดาลใจจากคำตอบของนักฟิสิกส์ของ Madคุณสามารถใช้Genกระดาษห่อนี้เพื่อตอบโต้:

แก้ไข : เพื่อจัดการกรณีที่ชี้โดยJean-Francois T.

เมื่อมีการใช้ค่าจากตัววนซ้ำมันจะหายไปตลอดกาลจากตัววนซ้ำและไม่มีวิธีการกลายพันธุ์แบบแทนที่ in-place สำหรับตัววนซ้ำเพื่อเพิ่มกลับไปที่ตัววนซ้ำ วิธีหนึ่งคือแก้ไขค่าที่ใช้ไปครั้งสุดท้าย

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 และคำตอบ ขอบคุณ!
Jean-Francois T.

@ ฌองฟรานโคอิส ใช่เห็นด้วย คุณควรทำเครื่องหมายคำตอบของเขาว่าเป็นคำตอบ สิ่งนี้มีข้อ จำกัด ฉันจะพยายามปรับปรุงคำตอบนี้เพื่อตอบโต้ทุกกรณี ;)
Ch3steR

@ Ch3steR ฉันสามารถช่วยให้คุณเขย่าถ้าคุณต้องการ ฉันเป็นมืออาชีพในด้านการตรวจสอบซอฟแวร์ :)
Jean-Francois T.

@ ฌองฟรานโคอิส ฉันอยากจะ. มันจะมีความหมายมาก ฉันเป็นนักศึกษาปริญญาตรีปีที่ 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โต้เถียงตอนที่เธอเรียกร้องzip_longestที่จะแทนที่Noneด้วยค่าเริ่มต้น แต่โดยทั่วไปสำหรับการแก้ปัญหาของคุณเมื่อคุณตีNone(อย่างใดอย่างหนึ่งiหรือj) ในการห่วงตัวแปรอื่น ๆ 8จะมีของคุณ


ขอบคุณ ฉันมาแล้วจริง ๆ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.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))
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.