การรีเซ็ตออบเจ็กต์ตัวสร้างใน Python


153

ฉันมีวัตถุเครื่องกำเนิดไฟฟ้าที่ส่งคืนโดยผลผลิตหลายรายการ การเตรียมการเพื่อเรียกเครื่องมือสร้างนี้ค่อนข้างใช้เวลานาน นั่นคือเหตุผลที่ฉันต้องการใช้ตัวกำเนิดซ้ำหลายครั้ง

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

แน่นอนฉันกำลังคำนึงถึงการคัดลอกเนื้อหาลงในรายการง่าย ๆ มีวิธีรีเซ็ตตัวสร้างของฉันหรือไม่?

คำตอบ:


119

อีกทางเลือกหนึ่งคือการใช้itertools.tee()ฟังก์ชั่นเพื่อสร้างตัวสร้างเวอร์ชั่นที่สองของคุณ:

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

นี่อาจเป็นประโยชน์จากมุมมองการใช้หน่วยความจำหากการวนซ้ำดั้งเดิมอาจไม่ประมวลผลรายการทั้งหมด


33
หากคุณสงสัยเกี่ยวกับสิ่งที่จะทำในกรณีนี้มันเป็นองค์ประกอบแคชในรายการ ดังนั้นคุณอาจใช้y = list(y)กับส่วนที่เหลือของรหัสของคุณไม่เปลี่ยนแปลง
ilya n

5
tee () จะสร้างรายการภายในเพื่อเก็บข้อมูลดังนั้นนั่นก็เหมือนกับที่ฉันทำในคำตอบ
nosklo

6
ดูที่ implmentation ( docs.python.org/library/itertools.html#itertools.tee ) - นี้จะใช้กลยุทธ์การโหลดขี้เกียจดังนั้นรายการที่จะคัดลอกรายการเฉพาะตามความต้องการ
Dewfy

11
@Dewfy: ซึ่งจะช้ากว่าเนื่องจากรายการทั้งหมดจะต้องคัดลอกต่อไป
nosklo

8
ใช่รายการ () ดีกว่าในกรณีนี้ tee มีประโยชน์เฉพาะในกรณีที่คุณไม่บริโภครายการทั้งหมด
ความโน้มถ่วง

148

เครื่องปั่นไฟไม่สามารถกรอ คุณมีตัวเลือกดังต่อไปนี้:

  1. เรียกใช้ฟังก์ชันตัวสร้างอีกครั้งเริ่มการสร้างใหม่:

    y = FunctionWithYield()
    for x in y: print(x)
    y = FunctionWithYield()
    for x in y: print(x)
  2. เก็บผลลัพธ์ของตัวสร้างในโครงสร้างข้อมูลในหน่วยความจำหรือดิสก์ซึ่งคุณสามารถทำซ้ำได้อีกครั้ง:

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

ข้อเสียของตัวเลือกที่1คือการคำนวณค่าอีกครั้ง ถ้านั่นเป็น CPU-intensive คุณจะสิ้นสุดการคำนวณสองครั้ง ในทางกลับกันข้อเสียของ2คือที่จัดเก็บ รายการทั้งหมดของค่าจะถูกเก็บไว้ในหน่วยความจำ หากมีค่ามากเกินไปนั่นอาจไม่คุ้มค่า

เพื่อให้คุณมีความคลาสสิกของหน่วยความจำกับการถ่วงดุลอำนาจในการประมวลผล ฉันไม่สามารถจินตนาการถึงวิธีการย้อนกลับของตัวสร้างโดยไม่เก็บค่าหรือคำนวณอีกครั้ง


อาจมีวิธีบันทึกลายเซ็นของการเรียกใช้ฟังก์ชันหรือไม่ FunctionWithYield, param1, param2 ...
Dewfy

3
@Dewfy: แน่ใจว่า: def call_my_func (): return FunctionWithYield (param1, param2)
nosklo

@Dewfy คุณหมายถึงอะไรโดย "บันทึกลายเซ็นของการเรียกใช้ฟังก์ชัน"? คุณช่วยอธิบายได้มั้ย คุณหมายถึงการบันทึกพารามิเตอร์ที่ส่งผ่านไปยังเครื่องกำเนิดหรือไม่?
АндрейБеньковский

2
ข้อเสียอีกประการของ (1) ก็คือ FunctionWithYield () สามารถไม่เพียง แต่มีค่าใช้จ่ายสูง แต่เป็นไปไม่ได้ที่จะคำนวณอีกครั้งเช่นหากอ่านจาก stdin
Max

2
เพื่อสะท้อนสิ่งที่ @Max กล่าวว่าหากผลลัพธ์ของฟังก์ชันอาจ (หรือจะ) เปลี่ยนไประหว่างการโทร (1) อาจให้ผลลัพธ์ที่ไม่คาดคิดและ / หรือไม่พึงประสงค์
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

อาจเป็นวิธีที่ง่ายที่สุดคือห่อชิ้นส่วนที่มีราคาแพงในวัตถุและส่งไปยังเครื่องกำเนิด:

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

ด้วยวิธีนี้คุณสามารถแคชการคำนวณราคาแพง

หากคุณสามารถเก็บผลลัพธ์ทั้งหมดใน RAM ในเวลาเดียวกันจากนั้นใช้list()เพื่อสร้างผลลัพธ์ของตัวสร้างในรายการธรรมดาและทำงานกับมัน


23

ฉันต้องการเสนอทางออกที่แตกต่างให้กับปัญหาเก่า

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)

ประโยชน์ของการนี้เมื่อเทียบกับสิ่งที่ต้องการlist(iterator)คือว่าเป็นO(1)ความซับซ้อนของพื้นที่และเป็นlist(iterator) O(n)ข้อเสียคือถ้าคุณเข้าถึง iterator เท่านั้น แต่ไม่ใช่ฟังก์ชั่นที่สร้าง iterator คุณจะไม่สามารถใช้วิธีนี้ได้ ตัวอย่างเช่นอาจดูเหมือนสมเหตุสมผลในการทำสิ่งต่อไปนี้ แต่จะไม่ทำงาน

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 ในตัวอย่างแรกตัวสร้างอยู่บนบรรทัด "squares = ... " นิพจน์ตัวสร้างจะทำงานในลักษณะเดียวกับการเรียกใช้ฟังก์ชันที่ใช้อัตราผลตอบแทนและฉันใช้เพียงอย่างเดียวเพราะมันให้รายละเอียดน้อยกว่าการเขียนฟังก์ชันที่มีผลตอบแทนเป็นตัวอย่างสั้น ๆ ในตัวอย่างที่สองฉันใช้ FunctionWithYield เป็น generator_factory ดังนั้นมันจะถูกเรียกเมื่อใดก็ตามที่เรียกว่าiterซึ่งเมื่อใดก็ตามที่ฉันเขียน "for x in y"
michaelsnowden

ทางออกที่ดี สิ่งนี้ทำให้วัตถุ iterable ไร้สัญชาติแทนที่จะเป็นวัตถุ iterator stateful ดังนั้นวัตถุนั้นสามารถนำกลับมาใช้ใหม่ได้ มีประโยชน์อย่างยิ่งหากคุณต้องการส่งวัตถุที่กล่าวซ้ำไปยังฟังก์ชันและฟังก์ชันนั้นจะใช้วัตถุหลายครั้ง
Cosyn

5

หากคำตอบของ GrzegorzOledzki ไม่เพียงพอคุณอาจใช้send()เพื่อบรรลุเป้าหมาย ดูPEP-0342สำหรับรายละเอียดเพิ่มเติมเกี่ยวกับเครื่องกำเนิดไฟฟ้าที่ปรับปรุงแล้วและการแสดงออกของผลผลิต

ปรับปรุง: ดูitertools.tee()ด้วย มันเกี่ยวข้องกับหน่วยความจำบางส่วนกับการประมวลผลการแลกเปลี่ยนที่กล่าวถึงข้างต้น แต่อาจบันทึกหน่วยความจำบางส่วนโดยเพียงแค่เก็บผลลัพธ์ตัวสร้างใน a list; ขึ้นอยู่กับว่าคุณใช้เครื่องกำเนิดไฟฟ้าอย่างไร


5

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

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))

เอาท์พุท:

[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

จากเอกสารอย่างเป็นทางการของที :

โดยทั่วไปหากตัววนซ้ำหนึ่งตัวใช้ข้อมูลส่วนใหญ่หรือทั้งหมดก่อนที่ตัววนซ้ำตัวอื่นจะเริ่มทำงานจะเร็วกว่าที่จะใช้ list () แทน tee ()

ดังนั้นควรใช้list(iterable)แทนในกรณีของคุณ


6
แล้วเครื่องกำเนิดไม่มีที่สิ้นสุดล่ะ?
Dewfy

1
ความเร็วไม่ใช่เพียงการพิจารณาเท่านั้น list()ทำให้ทั้ง iterable ลงในหน่วยความจำ
Chris_Rands

@Chris_Rands ดังนั้นtee()ถ้าผู้วนซ้ำหนึ่งใช้ค่าทั้งหมด - นั่นคือวิธีการteeทำงาน
AChampion

2
@Dewfy: สำหรับเครื่องกำเนิดไฟฟ้าที่ไม่มีที่สิ้นสุดให้ใช้โซลูชันของ Aaron Digulla (ฟังก์ชั่น ExpensiveSetup คืนค่าข้อมูลที่มีค่า)
Jeff Learman

3

การใช้ฟังก์ชั่น wrapper ในการจัดการ StopIteration

คุณสามารถเขียนฟังก์ชั่น wrapper ง่ายๆไปยังฟังก์ชั่นสร้างเครื่องกำเนิดไฟฟ้าของคุณที่ติดตามเมื่อเครื่องกำเนิดไฟฟ้าหมด มันจะทำเช่นนั้นโดยใช้StopIterationข้อยกเว้นที่ตัวกำเนิดโยนเมื่อถึงจุดสิ้นสุดของการทำซ้ำ

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

อย่างที่คุณเห็นด้านบนเมื่อฟังก์ชั่น wrapper ของเราจับStopIterationข้อยกเว้นมันก็เริ่มต้นวัตถุกำเนิดใหม่อีกครั้ง (โดยใช้อินสแตนซ์อื่นของการเรียกฟังก์ชัน)

จากนั้นสมมติว่าคุณกำหนดฟังก์ชั่นการสร้างเครื่องกำเนิดของคุณที่ใดที่หนึ่งด้านล่างคุณสามารถใช้ไวยากรณ์ Python function decorator เพื่อตัดมันโดยปริยาย:

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

2

คุณสามารถกำหนดฟังก์ชั่นที่คืนเครื่องกำเนิดของคุณ

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

  return FunctionWithYield

ตอนนี้คุณสามารถทำได้หลายครั้งตามที่คุณต้องการ:

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

1
ขอบคุณสำหรับคำตอบ แต่จุดสำคัญของคำถามคือหลีกเลี่ยงการสร้างการเรียกใช้ฟังก์ชันภายในเพียงซ่อนการสร้าง - คุณสร้างมันสองครั้ง
Dewfy

1

ฉันไม่แน่ใจว่าคุณหมายถึงอะไรโดยการเตรียมการที่มีราคาแพง แต่ฉันคิดว่าคุณมี

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)

หากเป็นกรณีที่ทำไมไม่นำมาใช้ใหม่data?


1

ไม่มีตัวเลือกในการรีเซ็ตตัววนซ้ำ Iterator มักปรากฏขึ้นเมื่อมันวนซ้ำผ่านnext()ฟังก์ชั่น วิธีเดียวคือการสำรองข้อมูลก่อนที่จะวนซ้ำบนวัตถุตัววนซ้ำ ตรวจสอบด้านล่าง

การสร้างวัตถุตัววนซ้ำด้วยรายการ 0 ถึง 9

i=iter(range(10))

วนซ้ำผ่านฟังก์ชั่นถัดไป () ซึ่งจะปรากฏขึ้น

print(next(i))

การแปลงวัตถุตัววนซ้ำเพื่อแสดงรายการ

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

ดังนั้นไอเท็ม 0 ถูกดึงออกมาแล้ว นอกจากนี้ยังมีไอเท็มทั้งหมดที่ผุดขึ้นเมื่อเราแปลงตัววนซ้ำเป็นลิสต์

next(L) 

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

ดังนั้นคุณต้องแปลงตัววนซ้ำเป็นรายการสำหรับการสำรองข้อมูลก่อนเริ่มการวนซ้ำ รายการสามารถแปลงเป็นตัววนซ้ำด้วยiter(<list-object>)


1

ตอนนี้คุณสามารถใช้ more_itertools.seekable (เครื่องมือของบุคคลที่สาม) ซึ่งเปิดใช้งานการรีเซ็ตตัววนซ้ำ

ติดตั้งผ่าน > 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)

หมายเหตุ: ปริมาณการใช้หน่วยความจำเพิ่มขึ้นขณะที่เลื่อนตัววนดังนั้นจึงควรระวังการวนซ้ำขนาดใหญ่


1

คุณสามารถทำได้โดยใช้itertools.cycle () คุณสามารถสร้างตัววนซ้ำด้วยวิธีนี้แล้วทำการวนรอบสำหรับวนรอบตัววนซ้ำซึ่งจะวนซ้ำค่า

ตัวอย่างเช่น:

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

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

จะสร้างตัวเลข 20, 0 ถึง 4 ซ้ำ ๆ

บันทึกจากเอกสาร:

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

+1 เพราะมันใช้งานได้ แต่ฉันเห็น 2 ประเด็นที่นั่น 1) footprint หน่วยความจำขนาดใหญ่เนื่องจากเอกสารระบุ "สร้างสำเนา" 2) การวนซ้ำไม่สิ้นสุดไม่ใช่สิ่งที่ฉันต้องการแน่นอน
Dewfy

0

ตกลงคุณบอกว่าคุณต้องการโทรหาเครื่องกำเนิดไฟฟ้าหลายครั้ง แต่การเริ่มต้นมีราคาแพง ... แล้วเรื่องแบบนี้ล่ะ?

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

หรือคุณสามารถสร้างคลาสของคุณเองตามโปรโตคอลตัววนซ้ำและกำหนดฟังก์ชัน 'รีเซ็ต' บางประเภท

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/library/stdtypes.html#iterator-types http://anandology.com/python-practice-book/iterators.html


คุณเพียงมอบหมายปัญหาให้ผู้ห่อหุ้ม สมมติว่าการเริ่มต้นมีราคาแพงสร้างเครื่องกำเนิดไฟฟ้า คำถามของฉันเกี่ยวกับวิธีการรีเซ็ตภายในของคุณ__call__
Dewfy

เพิ่มตัวอย่างที่สองในการตอบกลับความคิดเห็นของคุณ นี่คือตัวสร้างที่กำหนดเองเป็นหลักด้วยวิธีการรีเซ็ต
tvt173

0

คำตอบของฉันแก้ปัญหาที่แตกต่างกันเล็กน้อย: หากเครื่องกำเนิดไฟฟ้ามีราคาแพงในการเริ่มต้นและวัตถุที่สร้างขึ้นแต่ละรายการมีราคาแพงในการสร้าง แต่เราจำเป็นต้องใช้เครื่องกำเนิดไฟฟ้าหลาย ๆ ครั้งในหลายฟังก์ชั่น เพื่อที่จะเรียกใช้ตัวกำเนิดและแต่ละวัตถุที่สร้างขึ้นอย่างแน่นอนเมื่อเราสามารถใช้เธรดและเรียกใช้แต่ละวิธีการบริโภคในเธรดที่แตกต่างกัน เราอาจไม่บรรลุการขนานที่แท้จริงเนื่องจาก GIL แต่เราจะบรรลุเป้าหมายของเรา

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

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

ussage:

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())

คุณเพิ่งสร้างใหม่itertools.isliceหรือสำหรับ async aiostream.stream.takeและโพสต์นี้จะช่วยให้คุณทำในรูปแบบ asyn / รอคอยstackoverflow.com/a/42379188/149818
Dewfy

-3

มันสามารถทำได้โดยรหัสวัตถุ นี่คือตัวอย่าง

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