Các trình tạo Python được nén với cái thứ hai ngắn hơn: cách lấy phần tử được tiêu thụ âm thầm


50

Tôi muốn phân tích 2 trình tạo có độ dài khác nhau (có khả năng) bằng zip:

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

Tuy nhiên, nếu gen2có ít yếu tố hơn, một yếu tố phụ gen1là "tiêu thụ".

Ví dụ,

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

Rõ ràng, một giá trị bị thiếu ( 8trong ví dụ trước của tôi) vì gen1được đọc (do đó tạo ra giá trị 8) trước khi nó nhận ra gen2không có nhiều phần tử. Nhưng giá trị này biến mất trong vũ trụ. Khi gen2"dài hơn", không có "vấn đề" như vậy.

HỎI : Có cách nào để lấy lại giá trị còn thiếu này không (ví dụ 8trong ví dụ trước của tôi)? ... lý tưởng với số lượng đối số thay đổi (như ziphiện).

LƯU Ý : Hiện tại tôi đã triển khai theo cách khác bằng cách sử dụng itertools.zip_longestnhưng tôi thực sự tự hỏi làm thế nào để có được giá trị còn thiếu này bằng cách sử dụng ziphoặc tương đương.

LƯU Ý 2 : Tôi đã tạo một số thử nghiệm về các triển khai khác nhau trong REPL này trong trường hợp bạn muốn gửi và thử triển khai mới :) https://repl.it/@jfthuong/MadPhysicistChester


19
Các tài liệu lưu ý rằng "zip () chỉ nên được sử dụng với các đầu vào có độ dài không bằng nhau khi bạn không quan tâm đến dấu vết, các giá trị không khớp từ các iterables dài hơn. Nếu các giá trị đó quan trọng, thay vào đó hãy sử dụng itertools.zip_longest ()."
Carcigenicate

2
@ Ch3steR. Nhưng câu hỏi không liên quan gì đến "tại sao". Nó có nghĩa đen là "Có cách nào để lấy lại giá trị còn thiếu này không ...?" Dường như tất cả các câu trả lời nhưng tôi thuận tiện quên đọc phần đó.
Nhà vật lý điên

@MadPhysicist Lạ thật. Tôi đã viết lại câu hỏi để rõ ràng hơn về khía cạnh đó.
Jean-Francois T.

1
Vấn đề cơ bản là không có cách nào để nhìn trộm hoặc đẩy lùi vào máy phát điện. Vì vậy, một khi zip()đã đọc 8từ gen1, nó đã biến mất.
Barmar

1
@Barmar chắc chắn, tất cả chúng ta đều đồng ý về điều đó. Câu hỏi là làm thế nào để lưu trữ nó ở đâu đó để có thể sử dụng nó.
Jean-Francois T.

Câu trả lời:


28

Một cách sẽ là triển khai một trình tạo cho phép bạn lưu trữ giá trị cuối cùng:

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

Để sử dụng, bọc các đầu vào để zip:

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

Điều quan trọng là tạo gen2một iterator chứ không phải là iterable, vì vậy bạn có thể biết cái nào đã cạn kiệt. Nếu gen2kiệt sức, bạn không cần kiểm tra gen1.last.

Một cách tiếp cận khác là ghi đè zip để chấp nhận một chuỗi các lần lặp có thể thay đổi thay vì các lần lặp riêng biệt. Điều đó sẽ cho phép bạn thay thế các lần lặp bằng một phiên bản xích bao gồm mục "bị lén" của bạn:

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ách tiếp cận này có vấn đề vì nhiều lý do. Nó không chỉ mất đi lặp lại ban đầu, mà còn mất bất kỳ thuộc tính hữu ích nào mà đối tượng ban đầu có thể có bằng cách thay thế nó bằng một chainđối tượng.


@MadPhysicist. Yêu câu trả lời của bạn cache_lastvà thực tế là nó không làm thay đổi nexthành vi ... thật tệ, nó không mang tính đối xứng (chuyển đổi gen1gen2trong zip sẽ dẫn đến kết quả khác nhau) .Cheers
Jean-Francois T.

1
@ Jean-Francois. Tôi đã cập nhật trình lặp để trả lời đúng lastcác cuộc gọi sau khi nó hết. Điều đó sẽ giúp tìm ra nếu bạn cần giá trị cuối cùng hay không. Cũng làm cho nó nhiều sản xuất-y.
Nhà vật lý điên

@MadPhysicist Tôi đã chạy mã và đầu ra print(gen1.last) print(next(gen1)) None and 9
Ch3steR

@MadPhysicist với một số tài liệu và tất cả. Nice;) Tôi sẽ kiểm tra sau khi có thời gian. Cảm ơn bạn đã dành thời gian
Jean-Francois T.

@ Ch3steR. Cảm ơn đã bắt. Tôi đã quá phấn khích và xóa câu trả lời khỏi last.
Nhà vật lý điên

17

Đây là zipthực hiện tương đương được đưa ra trong tài liệu

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)

Trong ví dụ đầu tiên của bạn gen1 = my_gen(10)gen2 = my_gen(8). Sau khi cả hai máy phát điện được tiêu thụ cho đến lần lặp thứ 7. Bây giờ trong lần lặp thứ 8, gen1cuộc gọi elem = next(it, sentinel)sẽ trả về 8 nhưng khi gen2cuộc gọi elem = next(it, sentinel)lại trả về sentinel(vì lúc này đã gen2hết) và if elem is sentinelđược thỏa mãn và hàm thực hiện trả về và dừng lại. Bây giờ next(gen1)trả về 9.

Trong ví dụ thứ 2 của bạn gen1 = gen(8)gen2 = gen(10). Sau khi cả hai máy phát điện được tiêu thụ cho đến lần lặp thứ 7. Bây giờ trong lần gen1gọi thứ 8 elem = next(it, sentinel)sẽ trả về sentinel(vì tại thời điểm gen1này đã hết) và if elem is sentinelđược thỏa mãn và hàm thực hiện trả về và dừng lại. Bây giờ next(gen2)trả về 8.

Lấy cảm hứng từ câu trả lời của Mad Physicist , bạn có thể sử dụng Gentrình bao bọc này để chống lại nó:

Chỉnh sửa : Để xử lý các trường hợp được chỉ ra bởi Jean-Francois T.

Khi một giá trị được tiêu thụ từ iterator, nó sẽ biến mất vĩnh viễn khỏi iterator và không có phương thức biến đổi tại chỗ nào cho các iterator để đưa nó trở lại iterator. Một công việc xung quanh là lưu trữ giá trị tiêu thụ cuối cùng.

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

Ví dụ:

# 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`

Cảm ơn bạn @ Ch3steR đã dành thời gian cho vấn đề này. Việc sửa đổi giải pháp MadPhysicist của bạn có một số hạn chế: # 1. Nếu gen1 = cache_last(range(0))gen2 = cache_last(range(2))sau đó sau khi thực hiện list(zip(gen1, gen2), một cuộc gọi đến next(gen2)sẽ tăng một AttributeError: 'cache_last' object has no attribute 'prev'. # 2. Nếu gen1 dài hơn gen2, sau khi tiêu thụ tất cả các yếu tố, next(gen2)sẽ tiếp tục trả về giá trị cuối cùng thay vì StopIteration. Tôi sẽ đánh dấu câu trả lời của MadPhysicist và câu trả lời. Cảm ơn!
Jean-Francois T.

@ Jean-FrancoisT. Có đồng ý. Bạn nên đánh dấu câu trả lời của anh ấy là câu trả lời. Điều này có những hạn chế. Tôi sẽ cố gắng cải thiện câu trả lời này để chống lại tất cả các trường hợp. ;)
Ch3steR

@ Ch3steR Tôi có thể giúp bạn lắc nó nếu bạn muốn. Tôi là một chuyên gia trong lĩnh vực Xác thực phần mềm :)
Jean-Francois T.

@ Jean-FrancoisT. Tôi rất thích Nó có nghĩa là rất nhiều. Tôi là sinh viên năm thứ 3.
Ch3steR

2
Tốt lắm, nó vượt qua tất cả các bài kiểm tra tôi đã viết ở đây: repl.it/@jfthuong/MadPhysicistChester Bạn có thể chạy chúng trực tuyến, khá thuận tiện :)
Jean-Francois T.

6

Tôi có thể thấy bạn đã tìm thấy câu trả lời này và nó đã được đưa ra trong các bình luận nhưng tôi đoán rằng tôi sẽ đưa ra câu trả lời từ nó. Bạn muốn sử dụng itertools.zip_longest(), nó sẽ thay thế các giá trị trống của trình tạo ngắn hơn bằng 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)

Bản in:

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

Bạn cũng có thể cung cấp một fillvalueđối số khi gọi zip_longestđể thay thế Nonebằng một giá trị mặc định, nhưng về cơ bản cho giải pháp của bạn một khi bạn nhấn một None(hoặc ihoặc j) trong vòng lặp for, biến khác sẽ có của bạn 8.


Cảm ơn. Tôi thực sự đã nghĩ ra zip_longestvà nó thực sự nằm trong câu hỏi của tôi. :)
Jean-Francois T.

6

Lấy cảm hứng từ sự làm sáng tỏ của @ GrandPhuba zip, hãy tạo ra một biến thể "an toàn" (đơn vị được thử nghiệm tại đây ):

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

Đây là một bài kiểm tra cơ bản:

    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

bạn có thể sử dụng 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

Nếu bạn muốn sử dụng lại mã, giải pháp đơn giản nhất là:

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.

Bạn có thể kiểm tra mã này bằng cách sử dụng thiết lập của mình:

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

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

Nó sẽ in:

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

2

Tôi không nghĩ rằng bạn có thể truy xuất giá trị bị giảm với vòng lặp cơ bản, bởi vì trình vòng lặp đã cạn kiệt, được lấy từ zip(..., ...).__iter__ việc bị bỏ đi một khi đã cạn kiệt và bạn không thể truy cập nó.

Bạn nên thay đổi mã zip của mình, sau đó bạn có thể nhận được vị trí của vật phẩm bị rơi với một số mã hacky)

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))
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.