Đặt lại đối tượng trình tạo trong Python


153

Tôi có một đối tượng máy phát được trả về bởi nhiều sản lượng. Chuẩn bị để gọi máy phát điện này là hoạt động khá tốn thời gian. Đó là lý do tại sao tôi muốn sử dụng lại máy phát điện nhiều lần.

y = FunctionWithYield()
for x in y: print(x)
#here must be something to reset 'y'
for x in y: print(x)

Tất nhiên, tôi đang ghi nhớ sao chép nội dung vào danh sách đơn giản. Có cách nào để thiết lập lại máy phát điện của tôi không?

Câu trả lời:


119

Một tùy chọn khác là sử dụng itertools.tee()chức năng để tạo phiên bản thứ hai của trình tạo của bạn:

y = FunctionWithYield()
y, y_backup = tee(y)
for x in y:
    print(x)
for x in y_backup:
    print(x)

Điều này có thể có lợi từ quan điểm sử dụng bộ nhớ nếu việc lặp ban đầu có thể không xử lý tất cả các mục.


33
Nếu bạn đang tự hỏi về những gì nó sẽ làm trong trường hợp này, thì về cơ bản, đó là các yếu tố lưu trữ trong danh sách. Vì vậy, bạn cũng có thể sử dụng y = list(y)với phần còn lại của mã không thay đổi.
ilya n.

5
tee () sẽ tạo một danh sách nội bộ để lưu trữ dữ liệu, vì vậy đó giống như tôi đã làm trong câu trả lời của mình.
nosklo

6
Nhìn vào implmentation ( docs.python.org/library/itertools.html#itertools.tee ) - này sử dụng chiến lược tải lười biếng, vì vậy các mặt hàng vào danh sách sao chép chỉ theo yêu cầu
Dewfy

11
@Dewfy: Sẽ chậm hơn vì tất cả các mục sẽ phải được sao chép.
nosklo

8
có, list () là tốt hơn trong trường hợp này. tee chỉ hữu ích nếu bạn không tiêu thụ toàn bộ danh sách
trọng lực

148

Máy phát điện không thể được tua lại. Bạn có các tùy chọn sau:

  1. Chạy lại chức năng trình tạo, khởi động lại thế hệ:

    y = FunctionWithYield()
    for x in y: print(x)
    y = FunctionWithYield()
    for x in y: print(x)
  2. Lưu trữ kết quả của trình tạo trong cấu trúc dữ liệu trên bộ nhớ hoặc đĩa mà bạn có thể lặp lại lần nữa:

    y = list(FunctionWithYield())
    for x in y: print(x)
    # can iterate again:
    for x in y: print(x)

Nhược điểm của tùy chọn 1 là nó tính toán lại các giá trị. Nếu đó là CPU, bạn sẽ phải tính toán hai lần. Mặt khác, nhược điểm của 2 là lưu trữ. Toàn bộ danh sách các giá trị sẽ được lưu trữ trên bộ nhớ. Nếu có quá nhiều giá trị, điều đó có thể không thực tế.

Vì vậy, bạn có bộ nhớ cổ điển so với xử lý đánh đổi . Tôi không thể tưởng tượng ra cách tua lại trình tạo mà không lưu trữ các giá trị hoặc tính toán lại chúng.


Có thể tồn tại một cách để lưu chữ ký của chức năng gọi? HàmWithYield, param1, param2 ...
Dewfy

3
@Dewfy: chắc chắn: def call_my_func (): return FunctionWithYield (param1, param2)
nosklo

@Dewfy Ý bạn là gì khi "lưu chữ ký của chức năng gọi"? Bạn có thể vui lòng giải thích? Bạn có nghĩa là lưu các tham số được truyền cho máy phát điện?
Tiếng Việt

2
Một nhược điểm khác của (1) cũng là FunctionWithYield () có thể không chỉ tốn kém mà còn không thể tính toán lại, ví dụ nếu nó đang đọc từ stdin.
Tối đa

2
Để lặp lại những gì @Max đã nói, nếu đầu ra của chức năng có thể (hoặc sẽ) thay đổi giữa các cuộc gọi, (1) có thể cho kết quả không mong muốn và / hoặc không mong muốn.
Sam_Butler

36
>>> def gen():
...     def init():
...         return 0
...     i = init()
...     while True:
...         val = (yield i)
...         if val=='restart':
...             i = init()
...         else:
...             i += 1

>>> g = gen()
>>> g.next()
0
>>> g.next()
1
>>> g.next()
2
>>> g.next()
3
>>> g.send('restart')
0
>>> g.next()
1
>>> g.next()
2

29

Có lẽ giải pháp đơn giản nhất là bọc phần đắt tiền trong một vật thể và chuyển nó cho máy phát điện:

data = ExpensiveSetup()
for x in FunctionWithYield(data): pass
for x in FunctionWithYield(data): pass

Bằng cách này, bạn có thể lưu trữ các tính toán đắt tiền.

Nếu bạn có thể giữ tất cả các kết quả trong RAM cùng một lúc, thì hãy sử dụng list()để cụ thể hóa kết quả của trình tạo trong một danh sách đơn giản và làm việc với điều đó.


23

Tôi muốn đưa ra một giải pháp khác cho một vấn đề cũ

class IterableAdapter:
    def __init__(self, iterator_factory):
        self.iterator_factory = iterator_factory

    def __iter__(self):
        return self.iterator_factory()

squares = IterableAdapter(lambda: (x * x for x in range(5)))

for x in squares: print(x)
for x in squares: print(x)

Lợi ích của điều này khi so sánh với một cái gì đó giống như list(iterator)đây là O(1)sự phức tạp không gian và list(iterator)O(n). Nhược điểm là, nếu bạn chỉ có quyền truy cập vào iterator chứ không phải chức năng tạo ra iterator, thì bạn không thể sử dụng phương thức này. Ví dụ, có vẻ hợp lý để làm như sau, nhưng nó sẽ không hoạt động.

g = (x * x for x in range(5))

squares = IterableAdapter(lambda: g)

for x in squares: print(x)
for x in squares: print(x)

@Dewfy Trong đoạn trích đầu tiên, trình tạo nằm trên dòng "hình vuông = ...". Các biểu thức của trình tạo hoạt động giống như cách gọi một hàm sử dụng năng suất và tôi chỉ sử dụng một hàm vì nó ít dài dòng hơn so với viết một hàm có năng suất cho một ví dụ ngắn như vậy. Trong đoạn mã thứ hai, tôi đã sử dụng FunctionWithYield làm trình tạo_factory, do đó, nó sẽ được gọi bất cứ khi nào iter được gọi, đó là bất cứ khi nào tôi viết "cho x in y".
michaelsnowden

Giải pháp tốt. Điều này thực sự làm cho một đối tượng lặp không trạng thái thay vì một đối tượng lặp trạng thái, vì vậy chính đối tượng đó có thể tái sử dụng. Đặc biệt hữu ích nếu bạn muốn truyền một đối tượng lặp lại cho một chức năng và chức năng đó sẽ sử dụng đối tượng nhiều lần.
Cosyn

5

Nếu câu trả lời của GrzegorzOledzki không đủ, bạn có thể sử dụng send()để hoàn thành mục tiêu của mình. Xem PEP-0342 để biết thêm chi tiết về máy phát điện nâng cao và biểu thức năng suất.

CẬP NHẬT: Cũng xem itertools.tee(). Nó liên quan đến một số bộ nhớ so với xử lý sự đánh đổi được đề cập ở trên, nhưng nó có thể tiết kiệm một số bộ nhớ hơn khi chỉ lưu trữ kết quả của trình tạo trong một list; nó phụ thuộc vào cách bạn sử dụng máy phát điện.


5

Nếu trình tạo của bạn thuần túy theo nghĩa là đầu ra của nó chỉ phụ thuộc vào các đối số được truyền và số bước và bạn muốn trình tạo kết quả có thể được khởi động lại, thì đây là đoạn trích có thể tiện dụng:

import copy

def generator(i):
    yield from range(i)

g = generator(10)
print(list(g))
print(list(g))

class GeneratorRestartHandler(object):
    def __init__(self, gen_func, argv, kwargv):
        self.gen_func = gen_func
        self.argv = copy.copy(argv)
        self.kwargv = copy.copy(kwargv)
        self.local_copy = iter(self)

    def __iter__(self):
        return self.gen_func(*self.argv, **self.kwargv)

    def __next__(self):
        return next(self.local_copy)

def restartable(g_func: callable) -> callable:
    def tmp(*argv, **kwargv):
        return GeneratorRestartHandler(g_func, argv, kwargv)

    return tmp

@restartable
def generator2(i):
    yield from range(i)

g = generator2(10)
print(next(g))
print(list(g))
print(list(g))
print(next(g))

đầu ra:

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

3

Từ tài liệu chính thức của tee :

Nói chung, nếu một iterator sử dụng hầu hết hoặc tất cả dữ liệu trước khi một iterator khác bắt đầu, thì việc sử dụng list () thay vì tee () sẽ nhanh hơn.

Vì vậy, tốt nhất là sử dụng list(iterable)thay thế trong trường hợp của bạn.


6
Còn máy phát điện vô hạn thì sao?
Dewfy

1
Tốc độ không phải là sự cân nhắc duy nhất; list()đặt toàn bộ lặp vào bộ nhớ
Chris_Rands

@Chris_Rands Sẽ như vậy tee()nếu một iterator tiêu thụ tất cả các giá trị - đó là cách teehoạt động.
AChampion

2
@Dewfy: đối với các trình tạo vô hạn, hãy sử dụng giải pháp của Aaron Digulla (Hàm Expensivesetup trả về dữ liệu quý giá.)
Jeff Learman

3

Sử dụng chức năng bao bọc để xử lý StopIteration

Bạn có thể viết một hàm bao bọc đơn giản cho hàm tạo trình tạo của mình để theo dõi khi hết trình tạo. Nó sẽ làm như vậy bằng cách sử dụng StopIterationngoại lệ một trình tạo ném khi nó kết thúc vòng lặp.

import types

def generator_wrapper(function=None, **kwargs):
    assert function is not None, "Please supply a function"
    def inner_func(function=function, **kwargs):
        generator = function(**kwargs)
        assert isinstance(generator, types.GeneratorType), "Invalid function"
        try:
            yield next(generator)
        except StopIteration:
            generator = function(**kwargs)
            yield next(generator)
    return inner_func

Như bạn có thể nhận ra ở trên, khi hàm bao bọc của chúng ta bắt được một StopIterationngoại lệ, nó chỉ đơn giản khởi tạo lại đối tượng trình tạo (sử dụng một thể hiện khác của lệnh gọi hàm).

Và sau đó, giả sử bạn xác định hàm cung cấp trình tạo của mình ở đâu đó như bên dưới, bạn có thể sử dụng cú pháp trang trí hàm Python để bao hàm nó:

@generator_wrapper
def generator_generating_function(**kwargs):
    for item in ["a value", "another value"]
        yield item

2

Bạn có thể xác định một hàm trả về trình tạo của bạn

def f():
  def FunctionWithYield(generator_args):
    code here...

  return FunctionWithYield

Bây giờ bạn có thể làm bao nhiêu lần tùy thích:

for x in f()(generator_args): print(x)
for x in f()(generator_args): print(x)

1
Cảm ơn bạn đã trả lời, nhưng điểm chính của câu hỏi là tránh sáng tạo , gọi chức năng bên trong chỉ che giấu sự sáng tạo - bạn tạo nó hai lần
Dewfy

1

Tôi không chắc ý của bạn là gì khi chuẩn bị đắt tiền, nhưng tôi đoán bạn thực sự có

data = ... # Expensive computation
y = FunctionWithYield(data)
for x in y: print(x)
#here must be something to reset 'y'
# this is expensive - data = ... # Expensive computation
# y = FunctionWithYield(data)
for x in y: print(x)

Nếu đó là trường hợp, tại sao không sử dụng lại data?


1

Không có tùy chọn để thiết lập lại các vòng lặp. Iterator thường bật ra khi lặp đi lặp lạinext() chức năng. Cách duy nhất là sao lưu trước khi lặp trên đối tượng iterator. Kiểm tra bên dưới.

Tạo đối tượng lặp với các mục từ 0 đến 9

i=iter(range(10))

Lặp lại qua hàm next () sẽ bật ra

print(next(i))

Chuyển đổi đối tượng iterator thành danh sách

L=list(i)
print(L)
output: [1, 2, 3, 4, 5, 6, 7, 8, 9]

vì vậy mục 0 đã được bật ra. Ngoài ra tất cả các mục được bật lên khi chúng tôi chuyển đổi iterator thành danh sách.

next(L) 

Traceback (most recent call last):
  File "<pyshell#129>", line 1, in <module>
    next(L)
StopIteration

Vì vậy, bạn cần chuyển đổi iterator thành danh sách để sao lưu trước khi bắt đầu lặp. Danh sách có thể được chuyển đổi thành iterator vớiiter(<list-object>)


1

Bây giờ bạn có thể sử dụng more_itertools.seekable (một công cụ của bên thứ ba) cho phép đặt lại các trình vòng lặp.

Cài đặt qua > pip install more_itertools

import more_itertools as mit


y = mit.seekable(FunctionWithYield())
for x in y:
    print(x)

y.seek(0)                                              # reset iterator
for x in y:
    print(x)

Lưu ý: mức tiêu thụ bộ nhớ tăng lên trong khi tiến bộ iterator, vì vậy hãy cảnh giác với các iterables lớn.


1

Bạn có thể làm điều đó bằng cách sử dụng itertools. Motorcycle () bạn có thể tạo một trình vòng lặp với phương thức này và sau đó thực hiện một vòng lặp for trên iterator sẽ lặp qua các giá trị của nó.

Ví dụ:

def generator():
for j in cycle([i for i in range(5)]):
    yield j

gen = generator()
for i in range(20):
    print(next(gen))

sẽ tạo ra 20 số, 0 đến 4 liên tục.

Một lưu ý từ các tài liệu:

Note, this member of the toolkit may require significant auxiliary storage (depending on the length of the iterable).

+1 vì nó hoạt động, nhưng tôi thấy có 2 vấn đề ở đó 1) dấu chân bộ nhớ lớn do tài liệu ghi "tạo bản sao" 2) Vòng lặp vô hạn chắc chắn không phải là điều tôi muốn
Dewfy

0

Ok, bạn nói rằng bạn muốn gọi một trình tạo nhiều lần, nhưng việc khởi tạo rất tốn kém ... Còn cái gì đó như thế này thì sao?

class InitializedFunctionWithYield(object):
    def __init__(self):
        # do expensive initialization
        self.start = 5

    def __call__(self, *args, **kwargs):
        # do cheap iteration
        for i in xrange(5):
            yield self.start + i

y = InitializedFunctionWithYield()

for x in y():
    print x

for x in y():
    print x

Ngoài ra, bạn chỉ có thể tạo lớp của riêng mình theo giao thức lặp và xác định một số loại chức năng 'đặt lại'.

class MyIterator(object):
    def __init__(self):
        self.reset()

    def reset(self):
        self.i = 5

    def __iter__(self):
        return self

    def next(self):
        i = self.i
        if i > 0:
            self.i -= 1
            return i
        else:
            raise StopIteration()

my_iterator = MyIterator()

for x in my_iterator:
    print x

print 'resetting...'
my_iterator.reset()

for x in my_iterator:
    print x

https://docs.python.org/2/l Library / stdtypes.html # iterator-type http://anandology.com/python-practice-book/iterators.html


Bạn chỉ cần ủy thác vấn đề để bọc. Giả sử rằng khởi tạo đắt tiền tạo ra máy phát điện. Câu hỏi của tôi là về cách thiết lập lại bên trong của bạn__call__
Dewfy

Đã thêm một ví dụ thứ hai để đáp lại bình luận của bạn. Đây thực chất là một trình tạo tùy chỉnh với phương thức thiết lập lại.
tvt173

0

Câu trả lời của tôi giải quyết vấn đề hơi khác nhau: Nếu trình tạo tốn kém để khởi tạo và mỗi đối tượng được tạo thì tốn kém để tạo. Nhưng chúng ta cần tiêu thụ máy phát điện nhiều lần trong nhiều chức năng. Để gọi trình tạo và từng đối tượng được tạo chính xác một lần, chúng ta có thể sử dụng các luồng và Chạy từng phương thức tiêu thụ trong luồng khác nhau. Chúng tôi có thể không đạt được sự song song thực sự do GIL, nhưng chúng tôi sẽ đạt được mục tiêu của mình.

Cách tiếp cận này đã làm rất tốt trong trường hợp sau: mô hình học sâu xử lý rất nhiều hình ảnh. Kết quả là rất nhiều mặt nạ cho rất nhiều đối tượng trên ảnh. Mỗi mặt nạ tiêu thụ bộ nhớ. Chúng tôi có khoảng 10 phương pháp tạo ra các số liệu thống kê và số liệu khác nhau, nhưng chúng lấy tất cả các hình ảnh cùng một lúc. Tất cả các hình ảnh không thể phù hợp trong bộ nhớ. Các moethod có thể dễ dàng được viết lại để chấp nhận iterator.

class GeneratorSplitter:
'''
Split a generator object into multiple generators which will be sincronised. Each call to each of the sub generators will cause only one call in the input generator. This way multiple methods on threads can iterate the input generator , and the generator will cycled only once.
'''

def __init__(self, gen):
    self.gen = gen
    self.consumers: List[GeneratorSplitter.InnerGen] = []
    self.thread: threading.Thread = None
    self.value = None
    self.finished = False
    self.exception = None

def GetConsumer(self):
    # Returns a generator object. 
    cons = self.InnerGen(self)
    self.consumers.append(cons)
    return cons

def _Work(self):
    try:
        for d in self.gen:
            for cons in self.consumers:
                cons.consumed.wait()
                cons.consumed.clear()

            self.value = d

            for cons in self.consumers:
                cons.readyToRead.set()

        for cons in self.consumers:
            cons.consumed.wait()

        self.finished = True

        for cons in self.consumers:
            cons.readyToRead.set()
    except Exception as ex:
        self.exception = ex
        for cons in self.consumers:
            cons.readyToRead.set()

def Start(self):
    self.thread = threading.Thread(target=self._Work)
    self.thread.start()

class InnerGen:
    def __init__(self, parent: "GeneratorSplitter"):
        self.parent: "GeneratorSplitter" = parent
        self.readyToRead: threading.Event = threading.Event()
        self.consumed: threading.Event = threading.Event()
        self.consumed.set()

    def __iter__(self):
        return self

    def __next__(self):
        self.readyToRead.wait()
        self.readyToRead.clear()
        if self.parent.finished:
            raise StopIteration()
        if self.parent.exception:
            raise self.parent.exception
        val = self.parent.value
        self.consumed.set()
        return val

Sử dụng:

genSplitter = GeneratorSplitter(expensiveGenerator)

metrics={}
executor = ThreadPoolExecutor(max_workers=3)
f1 = executor.submit(mean,genSplitter.GetConsumer())
f2 = executor.submit(max,genSplitter.GetConsumer())
f3 = executor.submit(someFancyMetric,genSplitter.GetConsumer())
genSplitter.Start()

metrics.update(f1.result())
metrics.update(f2.result())
metrics.update(f3.result())

Bạn chỉ cần phát minh lại itertools.islicehoặc không đồng bộ aiostream.stream.take, và bài đăng này cho phép bạn thực hiện điều đó trong asyn / await way stackoverflow.com/a/42379188/149818
Dewfy

-3

Nó có thể được thực hiện bởi đối tượng mã. Dưới đây là ví dụ.

code_str="y=(a for a in [1,2,3,4])"
code1=compile(code_str,'<string>','single')
exec(code1)
for i in y: print i

1 2 3 4

for i in y: print i


exec(code1)
for i in y: print i

1 2 3 4


4
tốt, thực sự thiết lập lại trình tạo là cần thiết để tránh hai lần thực thi mã khởi tạo. Cách tiếp cận của bạn (1) thực hiện khởi tạo hai lần bằng mọi cách, (2) nó liên quan đến việc exechơi không được khuyến nghị cho trường hợp đơn giản như vậy.
Dewfy
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.