คำหลัก "ผลผลิต" ทำหน้าที่อะไร


10190

การใช้yieldคำหลักใน Python คืออะไรและใช้ทำอะไร?

ตัวอย่างเช่นฉันพยายามที่จะเข้าใจรหัสนี้1 :

def _get_child_candidates(self, distance, min_dist, max_dist):
    if self._leftchild and distance - max_dist < self._median:
        yield self._leftchild
    if self._rightchild and distance + max_dist >= self._median:
        yield self._rightchild  

และนี่คือผู้โทร:

result, candidates = [], [self]
while candidates:
    node = candidates.pop()
    distance = node._get_dist(obj)
    if distance <= max_dist and distance >= min_dist:
        result.extend(node._values)
    candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))
return result

จะเกิดอะไรขึ้นเมื่อมีการ_get_child_candidatesเรียกใช้เมธอด มีการส่งคืนรายการหรือไม่ องค์ประกอบเดียว มันจะเรียกอีกครั้ง? สายที่ตามมาจะหยุดเมื่อใด


1. โค้ดชิ้นนี้เขียนโดย Jochen Schulz (jrschulz) ผู้สร้างไลบรารี Python ที่ยอดเยี่ยมสำหรับการเว้นวรรค นี้คือการเชื่อมโยงไปยังแหล่งที่สมบูรณ์: โมดูล mspace

คำตอบ:


14642

เพื่อให้เข้าใจถึงสิ่งที่yieldไม่คุณต้องเข้าใจสิ่งที่กำเนิดเป็น และก่อนที่คุณสามารถเข้าใจกำเนิดคุณต้องเข้าใจiterables

Iterables

เมื่อคุณสร้างรายการคุณสามารถอ่านรายการได้ทีละรายการ การอ่านรายการหนึ่งต่อหนึ่งเรียกว่าการวนซ้ำ:

>>> mylist = [1, 2, 3]
>>> for i in mylist:
...    print(i)
1
2
3

mylistเป็นซ้ำได้ เมื่อคุณใช้ความเข้าใจในรายการคุณจะสร้างรายการและทำซ้ำได้:

>>> mylist = [x*x for x in range(3)]
>>> for i in mylist:
...    print(i)
0
1
4

ทุกสิ่งที่คุณสามารถใช้ได้คือ " for... in..." เปิดใช้ซ้ำ lists,, stringsไฟล์ ...

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

เครื่องปั่นไฟ

เครื่องกำเนิดเป็นตัววนซ้ำคุณสามารถวนซ้ำได้หนึ่งครั้งเท่านั้น เครื่องกำเนิดไฟฟ้าจะไม่เก็บค่าทั้งหมดไว้ในหน่วยความจำ แต่จะสร้างค่าได้ทันที :

>>> mygenerator = (x*x for x in range(3))
>>> for i in mygenerator:
...    print(i)
0
1
4

มันเป็นเพียงเหมือนเดิมยกเว้นที่คุณใช้แทน() []แต่คุณไม่สามารถดำเนินการfor i in mygeneratorเป็นครั้งที่สองได้เนื่องจากเครื่องกำเนิดไฟฟ้าสามารถใช้งานได้เพียงครั้งเดียว: พวกเขาคำนวณ 0 แล้วลืมเกี่ยวกับมันและคำนวณ 1 และสิ้นสุดการคำนวณ 4 ทีละคน

ผล

yieldเป็นคำหลักที่ใช้เช่นreturnยกเว้นฟังก์ชันจะส่งคืนตัวกำเนิด

>>> def createGenerator():
...    mylist = range(3)
...    for i in mylist:
...        yield i*i
...
>>> mygenerator = createGenerator() # create a generator
>>> print(mygenerator) # mygenerator is an object!
<generator object createGenerator at 0xb7555c34>
>>> for i in mygenerator:
...     print(i)
0
1
4

นี่เป็นตัวอย่างที่ไร้ประโยชน์ แต่มันมีประโยชน์เมื่อคุณรู้ว่าฟังก์ชั่นของคุณจะคืนค่าจำนวนมากที่คุณจะต้องอ่านเพียงครั้งเดียว

หากต้องการหลักyieldคุณต้องเข้าใจว่าเมื่อคุณเรียกใช้ฟังก์ชันรหัสที่คุณเขียนในส่วนของฟังก์ชันจะไม่ทำงาน ฟังก์ชั่นส่งกลับเฉพาะวัตถุเครื่องกำเนิดไฟฟ้าซึ่งเป็นเรื่องยุ่งยากเล็กน้อย :-)

จากนั้นรหัสของคุณจะดำเนินการต่อจากจุดที่ค้างไว้ในแต่ละครั้งที่forใช้ตัวสร้าง

ตอนนี้ส่วนที่ยาก:

ครั้งแรกที่forเรียกวัตถุตัวกำเนิดที่สร้างจากฟังก์ชั่นของคุณมันจะเรียกใช้รหัสในฟังก์ชั่นของคุณตั้งแต่เริ่มต้นจนกระทั่งมันกระทบyieldแล้วมันจะคืนค่าแรกของลูป จากนั้นแต่ละการเรียกที่ตามมาจะรันการวนซ้ำอีกครั้งของลูปที่คุณเขียนในฟังก์ชันและส่งคืนค่าถัดไป yieldนี้จะดำเนินต่อไปจนถึงเครื่องกำเนิดไฟฟ้าถือว่าเป็นที่ว่างเปล่าซึ่งเกิดขึ้นเมื่อฟังก์ชั่นทำงานได้โดยไม่ต้องกดปุ่ม "if/else"ที่สามารถจะเป็นเพราะวงได้มาถึงจุดสิ้นสุดหรือเพราะคุณไม่ได้ความพึงพอใจ


รหัสของคุณอธิบาย

เครื่องกำเนิดไฟฟ้า:

# Here you create the method of the node object that will return the generator
def _get_child_candidates(self, distance, min_dist, max_dist):

    # Here is the code that will be called each time you use the generator object:

    # If there is still a child of the node object on its left
    # AND if the distance is ok, return the next child
    if self._leftchild and distance - max_dist < self._median:
        yield self._leftchild

    # If there is still a child of the node object on its right
    # AND if the distance is ok, return the next child
    if self._rightchild and distance + max_dist >= self._median:
        yield self._rightchild

    # If the function arrives here, the generator will be considered empty
    # there is no more than two values: the left and the right children

Caller:

# Create an empty list and a list with the current object reference
result, candidates = list(), [self]

# Loop on candidates (they contain only one element at the beginning)
while candidates:

    # Get the last candidate and remove it from the list
    node = candidates.pop()

    # Get the distance between obj and the candidate
    distance = node._get_dist(obj)

    # If distance is ok, then you can fill the result
    if distance <= max_dist and distance >= min_dist:
        result.extend(node._values)

    # Add the children of the candidate in the candidate's list
    # so the loop will keep running until it will have looked
    # at all the children of the children of the children, etc. of the candidate
    candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))

return result

รหัสนี้มีส่วนประกอบอัจฉริยะหลายส่วน:

  • วนซ้ำในรายการ แต่รายการจะขยายในขณะที่วนซ้ำจะถูกทำซ้ำ :-) มันเป็นวิธีที่กระชับในการอ่านข้อมูลที่ซ้อนกันเหล่านี้แม้ว่ามันจะค่อนข้างอันตรายเพราะคุณสามารถจบด้วยวงวนไม่สิ้นสุด ในกรณีนี้ให้candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))หมดค่าทั้งหมดของตัวสร้าง แต่whileสร้างวัตถุตัวสร้างใหม่ซึ่งจะสร้างค่าที่แตกต่างจากค่าก่อนหน้าเนื่องจากไม่ได้ใช้กับโหนดเดียวกัน

  • extend()วิธีเป็นวิธีที่รายการวัตถุที่คาดว่า iterable และเพิ่มคุณค่าในรายการ

เรามักจะส่งรายการไปที่:

>>> a = [1, 2]
>>> b = [3, 4]
>>> a.extend(b)
>>> print(a)
[1, 2, 3, 4]

แต่ในรหัสของคุณจะได้รับตัวสร้างซึ่งดีเพราะ:

  1. คุณไม่จำเป็นต้องอ่านค่าสองครั้ง
  2. คุณอาจมีลูกจำนวนมากและคุณไม่ต้องการให้เก็บไว้ในความทรงจำ

และใช้งานได้เพราะ Python ไม่สนใจว่าอาร์กิวเมนต์ของเมธอดนั้นเป็นรายการหรือไม่ Python คาดว่าจะใช้งานได้ดังนั้นมันจะทำงานกับสตริงรายการ tuples และเครื่องกำเนิดไฟฟ้า! นี่เรียกว่าการพิมพ์เป็ดและเป็นหนึ่งในสาเหตุที่ Python เจ๋งมาก แต่นี่เป็นอีกเรื่องหนึ่งสำหรับคำถามอื่น ...

คุณสามารถหยุดที่นี่หรืออ่านนิดหน่อยเพื่อดูการใช้เครื่องกำเนิดไฟฟ้าขั้นสูง:

การควบคุมความเหนื่อยล้าของเครื่องกำเนิดไฟฟ้า

>>> class Bank(): # Let's create a bank, building ATMs
...    crisis = False
...    def create_atm(self):
...        while not self.crisis:
...            yield "$100"
>>> hsbc = Bank() # When everything's ok the ATM gives you as much as you want
>>> corner_street_atm = hsbc.create_atm()
>>> print(corner_street_atm.next())
$100
>>> print(corner_street_atm.next())
$100
>>> print([corner_street_atm.next() for cash in range(5)])
['$100', '$100', '$100', '$100', '$100']
>>> hsbc.crisis = True # Crisis is coming, no more money!
>>> print(corner_street_atm.next())
<type 'exceptions.StopIteration'>
>>> wall_street_atm = hsbc.create_atm() # It's even true for new ATMs
>>> print(wall_street_atm.next())
<type 'exceptions.StopIteration'>
>>> hsbc.crisis = False # The trouble is, even post-crisis the ATM remains empty
>>> print(corner_street_atm.next())
<type 'exceptions.StopIteration'>
>>> brand_new_atm = hsbc.create_atm() # Build a new one to get back in business
>>> for cash in brand_new_atm:
...    print cash
$100
$100
$100
$100
$100
$100
$100
$100
$100
...

หมายเหตุ:สำหรับ Python 3 ให้ใช้print(corner_street_atm.__next__())หรือprint(next(corner_street_atm))

มันจะมีประโยชน์สำหรับสิ่งต่าง ๆ เช่นการควบคุมการเข้าถึงทรัพยากร

Itertools เพื่อนที่ดีที่สุดของคุณ

โมดูล itertools มีฟังก์ชั่นพิเศษเพื่อจัดการ iterables เคยต้องการที่จะทำสำเนากำเนิด? เชนสองกำเนิด? ค่ากลุ่มในรายการซ้อนกับหนึ่งซับ? Map / Zipโดยไม่ต้องสร้างรายการอื่น?

import itertoolsแล้วก็

ตัวอย่าง? ลองดูคำสั่งที่เป็นไปได้สำหรับการแข่งม้าสี่ตัว

>>> horses = [1, 2, 3, 4]
>>> races = itertools.permutations(horses)
>>> print(races)
<itertools.permutations object at 0xb754f1dc>
>>> print(list(itertools.permutations(horses)))
[(1, 2, 3, 4),
 (1, 2, 4, 3),
 (1, 3, 2, 4),
 (1, 3, 4, 2),
 (1, 4, 2, 3),
 (1, 4, 3, 2),
 (2, 1, 3, 4),
 (2, 1, 4, 3),
 (2, 3, 1, 4),
 (2, 3, 4, 1),
 (2, 4, 1, 3),
 (2, 4, 3, 1),
 (3, 1, 2, 4),
 (3, 1, 4, 2),
 (3, 2, 1, 4),
 (3, 2, 4, 1),
 (3, 4, 1, 2),
 (3, 4, 2, 1),
 (4, 1, 2, 3),
 (4, 1, 3, 2),
 (4, 2, 1, 3),
 (4, 2, 3, 1),
 (4, 3, 1, 2),
 (4, 3, 2, 1)]

ทำความเข้าใจกลไกภายในของการทำซ้ำ

การวนซ้ำเป็นกระบวนการที่อ้างถึง iterables (การนำ__iter__()เมธอดไปใช้) และตัววนซ้ำ (การใช้__next__()เมธอด) Iterables เป็นวัตถุใด ๆ ที่คุณสามารถรับตัววนซ้ำได้ การวนซ้ำเป็นวัตถุที่ให้คุณทำซ้ำบน iterables

มีมากขึ้นเกี่ยวกับเรื่องนี้ในบทความนี้เกี่ยวกับวิธีการforวนรอบการทำงาน


355
yieldไม่ได้วิเศษเท่าคำตอบนี้แสดงให้เห็น เมื่อคุณเรียกใช้ฟังก์ชันที่มีyieldคำสั่งใดก็ได้คุณจะได้รับวัตถุตัวสร้าง แต่ไม่มีโค้ดที่ทำงาน จากนั้นทุกครั้งที่คุณดึงวัตถุออกจากเครื่องกำเนิด Python จะเรียกใช้รหัสในฟังก์ชันจนกว่าจะมีyieldคำสั่งจากนั้นหยุดและส่งวัตถุ เมื่อคุณแยกวัตถุอื่นออก Python จะดำเนินการต่อหลังจากนั้นyieldและดำเนินต่อไปจนกว่าจะถึงวัตถุอื่นyield(มักจะเป็นวัตถุเดียวกัน แต่ทำซ้ำในภายหลัง) สิ่งนี้จะดำเนินต่อไปจนกระทั่งฟังก์ชั่นจะสิ้นสุดลง ณ จุดที่เครื่องกำเนิดไฟฟ้านั้นถือว่าหมด
Matthias Fripp

29
"การทำซ้ำเหล่านี้มีประโยชน์ ... แต่คุณเก็บค่าทั้งหมดไว้ในหน่วยความจำและนี่ไม่ใช่สิ่งที่คุณต้องการเสมอไป" ไม่ถูกต้องหรือสับสน iterable ส่งคืน iterator เมื่อเรียก iter () บน iterable และ iterator ไม่จำเป็นต้องเก็บค่าไว้ในหน่วยความจำเสมอขึ้นอยู่กับการใช้งานของiter method มันยังสามารถสร้างค่าในลำดับตามความต้องการ
picmate 涅

มันคงจะดีถ้าหากเพิ่มเข้าไปในคำตอบที่ดีนี้ทำไมมันเป็นแบบเดียวกันยกเว้นที่คุณใช้()แทน[]โดยเฉพาะสิ่งที่()(อาจมีความสับสนกับ tuple)
WoJ

ฉันอาจจะผิด แต่ตัวสร้างไม่ได้เป็นตัววนซ้ำ "ตัวกำเนิดที่เรียกว่า" คือตัววนซ้ำ
aderchox

2006

ทางลัดสู่ความเข้าใจ yield

เมื่อคุณเห็นฟังก์ชันที่มีyieldคำสั่งให้ใช้เคล็ดลับง่าย ๆ นี้เพื่อทำความเข้าใจว่าจะเกิดอะไรขึ้น:

  1. แทรกบรรทัดresult = []ที่จุดเริ่มต้นของฟังก์ชัน
  2. แทนที่แต่ละกับyield exprresult.append(expr)
  3. แทรกบรรทัดreturn resultที่ด้านล่างของฟังก์ชัน
  4. ยะ - ไม่มีyieldงบอีกแล้ว! อ่านและคิดรหัส
  5. ฟังก์ชั่นเปรียบเทียบกับความหมายเดิม

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

อย่าสับสน Iterables, Iterators และ Generators ของคุณ

ก่อนอื่นโปรโตคอล iterator - เมื่อคุณเขียน

for x in mylist:
    ...loop body...

Python ดำเนินการสองขั้นตอนต่อไปนี้:

  1. รับตัววนซ้ำสำหรับmylist:

    การเรียกiter(mylist)-> สิ่งนี้จะคืนค่าออบเจกต์ด้วยnext()เมธอด (หรือ__next__()ใน Python 3)

    [นี่คือขั้นตอนที่คนส่วนใหญ่ลืมบอกคุณเกี่ยวกับ]

  2. ใช้ตัววนซ้ำเพื่อวนซ้ำรายการ:

    เรียกnext()วิธีการบนตัววนซ้ำที่ส่งคืนจากขั้นตอนที่ 1 ค่าส่งคืนจากnext()ถูกกำหนดให้กับxและเนื้อความลูปจะถูกดำเนินการ หากมีการStopIterationยกข้อยกเว้นจากภายในnext()หมายความว่าไม่มีค่าเพิ่มเติมในตัววนซ้ำและออกจากลูป

ความจริงก็คือ Python ดำเนินการสองขั้นตอนข้างต้นเมื่อใดก็ตามที่ต้องการวนลูปเนื้อหาของวัตถุ - ดังนั้นจึงอาจเป็นการวนซ้ำ แต่มันอาจเป็นโค้ดเช่นotherlist.extend(mylist)(ซึ่งotherlistเป็นรายการ Python)

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

นั่นคือโปรโตคอลตัววนซ้ำวัตถุหลายตัวใช้โปรโตคอลนี้:

  1. รายการพจนานุกรม tuples ชุดไฟล์ในตัว
  2. __iter__()เรียนผู้ใช้กำหนดที่ใช้
  3. เครื่องปั่นไฟ

หมายเหตุว่าforห่วงไม่ทราบชนิดของวัตถุมันจัดการกับ - มันก็เป็นไปตามโปรโตคอล iterator next()และมีความสุขที่จะได้รับหลังจากรายการเป็นรายการที่เรียก รายการในตัวส่งคืนรายการของพวกเขาทีละพจนานุกรมพจนานุกรมคืนกุญแจทีละไฟล์ไฟล์คืนบรรทัดหนึ่งโดยหนึ่งและเครื่องกำเนิดไฟฟ้ากลับ ... ดีนั่นคือที่yieldมา:

def f123():
    yield 1
    yield 2
    yield 3

for item in f123():
    print item

แทนที่จะเป็นyieldคำสั่งถ้าคุณมีสามreturnคำสั่งในf123()ครั้งแรกเท่านั้นที่จะถูกดำเนินการและฟังก์ชั่นจะออก แต่f123()ไม่มีฟังก์ชั่นธรรมดา เมื่อf123()ถูกเรียกมันจะไม่ส่งคืนค่าใด ๆ ในงบผลตอบแทน! ส่งคืนวัตถุเครื่องกำเนิดไฟฟ้า นอกจากนี้ฟังก์ชั่นไม่ได้ออกจริงๆ - มันจะเข้าสู่สถานะที่ถูกระงับ เมื่อforลูปพยายามวนลูปวัตถุตัวสร้างฟังก์ชันจะดำเนินการต่อจากสถานะที่ถูกระงับที่บรรทัดถัดไปหลังจากที่yieldมันถูกส่งกลับมาก่อนหน้านี้จะประมวลผลโค้ดบรรทัดถัดไปในกรณีนี้yieldคำสั่งและส่งคืนเป็นถัดไป สิ่งของ. สิ่งนี้จะเกิดขึ้นจนกระทั่งฟังก์ชันออกจากที่จุดกำเนิดและยกStopIterationออก

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

ทำไมต้องใช้เครื่องกำเนิดไฟฟ้า

โดยปกติแล้วคุณสามารถเขียนโค้ดที่ไม่ได้ใช้เครื่องกำเนิดไฟฟ้า แต่ใช้ตรรกะเดียวกัน ทางเลือกหนึ่งคือใช้ 'เคล็ดลับ' รายการชั่วคราวที่ฉันพูดถึงก่อนหน้านี้ ซึ่งจะไม่ทำงานในทุกกรณีเช่นหากคุณมีลูปไม่สิ้นสุดหรืออาจใช้หน่วยความจำอย่างไม่มีประสิทธิภาพเมื่อคุณมีรายการที่ยาวมาก อีกวิธีหนึ่งคือการนำ SomethingIter คลาส iterable ใหม่ที่รักษาสถานะในสมาชิกอินสแตนซ์และดำเนินการตามขั้นตอนตรรกะถัดไปในเมธอดnext()(หรือ__next__()ใน Python 3) โค้ดภายในnext()วิธีอาจจบลงด้วยความซับซ้อนและมีแนวโน้มที่จะเกิดข้อบกพร่องขึ้นอยู่กับตรรกะ ที่นี่เครื่องกำเนิดไฟฟ้าให้ทางออกที่สะอาดและง่าย


20
"เมื่อคุณเห็นฟังก์ชั่นที่มีข้อความประกาศผลให้ใช้เคล็ดลับง่ายๆนี้เพื่อทำความเข้าใจว่าจะเกิดอะไรขึ้น"นี่ไม่ได้เพิกเฉยต่อความจริงที่ว่าคุณสามารถsendเป็นเครื่องกำเนิดไฟฟ้าได้ซึ่งเป็นส่วนสำคัญของเครื่องกำเนิดไฟฟ้า
DanielSank

10
"อาจเป็น for loop แต่อาจเป็นรหัสเช่นotherlist.extend(mylist)" -> นี่ไม่ถูกต้อง extend()ปรับเปลี่ยนรายการในสถานที่และไม่กลับ iterable การพยายามวนซ้ำotherlist.extend(mylist)จะล้มเหลวด้วยTypeErrorเพราะextend()ส่งกลับโดยปริยายNoneและคุณไม่สามารถวนซ้ำNoneได้
Pedro

4
@pedro คุณเข้าใจผิดประโยคนั้น ก็หมายความว่างูหลามดำเนินการทั้งสองขั้นตอนดังกล่าวเมื่อวันที่mylist(ไม่otherlist) otherlist.extend(mylist)เมื่อมีการดำเนิน
วันนี้

555

ลองวิธีนี้ดู:

ตัววนซ้ำเป็นคำที่ทำให้เกิดเสียงแฟนซีสำหรับวัตถุที่มีnext()วิธีการ ดังนั้นฟังก์ชั่นผลผลิต -ed จึงเป็นดังนี้:

รุ่นเดิม:

def some_function():
    for i in xrange(4):
        yield i

for i in some_function():
    print i

นี่คือสิ่งที่ Python interpreter ทำกับโค้ดด้านบน:

class it:
    def __init__(self):
        # Start at -1 so that we get 0 when we add 1 below.
        self.count = -1

    # The __iter__ method will be called once by the 'for' loop.
    # The rest of the magic happens on the object returned by this method.
    # In this case it is the object itself.
    def __iter__(self):
        return self

    # The next method will be called repeatedly by the 'for' loop
    # until it raises StopIteration.
    def next(self):
        self.count += 1
        if self.count < 4:
            return self.count
        else:
            # A StopIteration exception is raised
            # to signal that the iterator is done.
            # This is caught implicitly by the 'for' loop.
            raise StopIteration

def some_func():
    return it()

for i in some_func():
    print i

สำหรับข้อมูลเชิงลึกเพิ่มเติมเกี่ยวกับสิ่งที่เกิดขึ้นเบื้องหลังการforวนซ้ำสามารถเขียนใหม่ได้:

iterator = some_func()
try:
    while 1:
        print iterator.next()
except StopIteration:
    pass

นั่นทำให้รู้สึกมากขึ้นหรือเพียงแค่ทำให้คุณสับสนมากขึ้น? :)

ฉันควรทราบว่าสิ่งนี้ เป็นสิ่งที่เกินความจริงเพื่อจุดประสงค์ในการอธิบาย :)


1
__getitem____iter__อาจจะกำหนดแทน ตัวอย่างเช่น: class it: pass; it.__getitem__ = lambda self, i: i*10 if i < 10 else [][0]; for i in it(): print(i)มันจะพิมพ์: 0, 10, 20, ... , 90
jfs

17
ฉันลองตัวอย่างนี้ใน Python 3.6 และถ้าฉันสร้างiterator = some_function()ตัวแปรiteratorไม่มีฟังก์ชั่นที่เรียกnext()อีกต่อไป แต่เป็นเพียง__next__()ฟังก์ชั่น คิดว่าฉันจะพูดถึงมัน
ปีเตอร์

การforใช้งานลูปที่คุณเขียนเรียก__iter__วิธีการของiteratorอินสแตนซ์อินสแตนซ์ของอยู่itที่ไหน
SystematicDisintegration

455

yieldคำหลักจะลดลงไปสองข้อเท็จจริงง่ายๆ

  1. หากคอมไพเลอร์ตรวจพบyieldคำสำคัญที่ใดก็ได้ภายในฟังก์ชั่นฟังก์ชั่นนั้นจะไม่ส่งคืนผ่านทางreturnคำสั่งอีกต่อไป แต่จะส่งคืนวัตถุ "รายการที่รอดำเนินการ" ที่ไม่แสดงผลทันทีที่เรียกว่าเครื่องกำเนิดไฟฟ้า
  2. เครื่องกำเนิดไฟฟ้าสามารถทำซ้ำได้ คืออะไรiterable ? มันเป็นอะไรก็ได้เช่นมุมมองlistหรือsetหรือrangeหรือ dict-view ที่มีโปรโตคอลในตัวสำหรับการเยี่ยมชมแต่ละองค์ประกอบในลำดับที่แน่นอน

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

generator = myYieldingFunction(...)
x = list(generator)

   generator
       v
[x[0], ..., ???]

         generator
             v
[x[0], x[1], ..., ???]

               generator
                   v
[x[0], x[1], x[2], ..., ???]

                       StopIteration exception
[x[0], x[1], x[2]]     done

list==[x[0], x[1], x[2]]

ตัวอย่าง

มานิยามฟังก์ชั่นmakeRangeที่เหมือนกับ Python rangeกัน โทรmakeRange(n)กลับเครื่องกำเนิด:

def makeRange(n):
    # return 0,1,2,...,n-1
    i = 0
    while i < n:
        yield i
        i += 1

>>> makeRange(5)
<generator object makeRange at 0x19e4aa0>

ในการบังคับให้เครื่องกำเนิดคืนค่าที่รอดำเนินการทันทีคุณสามารถส่งมันเข้าไปlist()(เช่นเดียวกับที่คุณทำซ้ำได้):

>>> list(makeRange(5))
[0, 1, 2, 3, 4]

เปรียบเทียบตัวอย่างกับ "เพิ่งส่งคืนรายการ"

ตัวอย่างข้างต้นอาจเป็นเพียงการสร้างรายการที่คุณผนวกและส่งคืน:

# list-version                   #  # generator-version
def makeRange(n):                #  def makeRange(n):
    """return [0,1,2,...,n-1]""" #~     """return 0,1,2,...,n-1"""
    TO_RETURN = []               #>
    i = 0                        #      i = 0
    while i < n:                 #      while i < n:
        TO_RETURN += [i]         #~         yield i
        i += 1                   #          i += 1  ## indented
    return TO_RETURN             #>

>>> makeRange(5)
[0, 1, 2, 3, 4]

มีความแตกต่างที่สำคัญอย่างหนึ่งแม้ว่า; ดูส่วนสุดท้าย


คุณอาจใช้เครื่องกำเนิดไฟฟ้าอย่างไร

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

#                   _ITERABLE_
>>> [x+10 for x in makeRange(5)]
[10, 11, 12, 13, 14]

เพื่อให้ได้ความรู้สึกที่ดีขึ้นสำหรับเครื่องกำเนิดไฟฟ้าคุณสามารถเล่นกับitertoolsโมดูลได้ (อย่าลืมใช้งานchain.from_iterableแทนการchainรับประกัน) itertools.count()ตัวอย่างเช่นคุณอาจจะใช้เครื่องกำเนิดไฟฟ้าที่จะใช้รายการขี้เกียจอนันต์ยาวเช่น คุณสามารถใช้ของคุณเองdef enumerate(iterable): zip(count(), iterable)หรืออีกวิธีหนึ่งกับyieldคำหลักในขณะที่วง

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


เบื้องหลัง

นี่คือวิธีการทำงานของ "Python iteration protocol" list(makeRange(5))นั่นคือสิ่งที่เกิดขึ้นเมื่อคุณทำ นี่คือสิ่งที่ฉันอธิบายก่อนหน้านี้ว่าเป็น "รายการสันหลังยาวส่วนเพิ่ม"

>>> x=iter(range(5))
>>> next(x)
0
>>> next(x)
1
>>> next(x)
2
>>> next(x)
3
>>> next(x)
4
>>> next(x)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

ฟังก์ชันในตัวnext()จะเรียกใช้.next()ฟังก์ชันวัตถุซึ่งเป็นส่วนหนึ่งของ "โปรโตคอลการทำซ้ำ" และพบได้ในตัววนซ้ำทั้งหมด คุณสามารถใช้next()ฟังก์ชั่น (และส่วนอื่น ๆ ของโพรโทคอลการวนซ้ำ) เพื่อใช้สิ่งที่แปลกใหม่โดยปกติจะเป็นค่าใช้จ่ายในการอ่านได้ดังนั้นพยายามหลีกเลี่ยงการทำเช่นนั้น ...


ข้อปลีกย่อย

โดยปกติแล้วคนส่วนใหญ่ไม่สนใจความแตกต่างดังต่อไปนี้และอาจต้องการหยุดอ่านที่นี่

ในหลามพูดเป็นiterableเป็นวัตถุใด ๆ ที่ "เข้าใจแนวคิดของสำหรับวง" ชอบรายการ[1,2,3]และiterator[1,2,3].__iter__()เป็นกรณีเฉพาะของขอวงเช่น ตัวกำเนิดนั้นเหมือนกับตัววนซ้ำใด ๆ ยกเว้นวิธีการเขียน (พร้อมกับฟังก์ชันไวยากรณ์)

เมื่อคุณร้องขอตัววนซ้ำจากรายการมันจะสร้างตัววนซ้ำใหม่ อย่างไรก็ตามเมื่อคุณร้องขอตัววนซ้ำจากตัววนซ้ำ (ซึ่งคุณไม่ค่อยทำ) มันจะให้สำเนาตัวเองแก่คุณ

ดังนั้นในกรณีที่คุณไม่สามารถทำอะไรเช่นนี้ ...

> x = myRange(5)
> list(x)
[0, 1, 2, 3, 4]
> list(x)
[]

... แล้วจำได้ว่ากำเนิดเป็นiterator ; นั่นคือมันใช้เพียงครั้งเดียว หากคุณต้องการนำมาใช้ซ้ำคุณควรโทรmyRange(...)อีกครั้ง x = list(myRange(5))หากคุณจำเป็นต้องใช้ผลที่สองแปลงผลให้รายการและเก็บไว้ในตัวแปร ผู้ที่ต้องการโคลนเครื่องกำเนิดไฟฟ้า (ตัวอย่างเช่นผู้ที่ทำการแฮ็ก metaprogramming แฮ็คที่น่ากลัว) สามารถใช้itertools.teeหากจำเป็นอย่างยิ่งเนื่องจากข้อเสนอมาตรฐานตัวทำซ้ำที่คัดลอกได้ของPython PEPได้ถูกเลื่อนออกไป


377

อะไรyieldคำหลักทำในงูใหญ่?

ตอบสรุป / สรุป

  • ฟังก์ชั่นที่มีyieldเมื่อเรียกว่าส่งกลับปั่นไฟ
  • เครื่องกำเนิดไฟฟ้าเป็นตัววนซ้ำเนื่องจากใช้โปรโตคอลตัววนซ้ำดังนั้นคุณจึงสามารถทำซ้ำได้
  • เครื่องกำเนิดไฟฟ้ายังสามารถส่งข้อมูลทำให้เป็นแนวคิดcoroutine coroutine
  • ใน Python 3 คุณสามารถมอบหมายจากเครื่องกำเนิดหนึ่งไปยังอีกเครื่องหนึ่งได้ทั้งสองทิศทางด้วยyield fromจากที่หนึ่งไปยังอีกเครื่องกำเนิดไฟฟ้าในทั้งสองทิศทางด้วย
  • (คำวิจารณ์ภาคผนวกสองคำตอบรวมถึงคำถามอันดับต้น ๆ และพูดถึงการใช้งานของreturnเครื่องกำเนิดไฟฟ้า)

กำเนิด:

yieldเป็นสิ่งที่ถูกต้องภายในนิยามของฟังก์ชันและการรวมyieldในนิยามฟังก์ชันทำให้ส่งคืนตัวกำเนิด

แนวคิดสำหรับเครื่องกำเนิดไฟฟ้ามาจากภาษาอื่น (ดูเชิงอรรถ 1) ด้วยการใช้งานที่แตกต่างกัน ใน Python Generators การเรียกใช้โค้ดจะถูกทำให้เย็นลงณ จุดที่ให้ผลผลิต เมื่อเครื่องกำเนิดไฟฟ้าเรียกว่า (วิธีการที่กล่าวถึงด้านล่าง) การดำเนินการดำเนินการต่อแล้วค้างที่ผลผลิตต่อไป

yieldจัดเตรียมวิธีง่าย ๆ ในการปรับใช้โปรโตคอลตัววนซ้ำซึ่งกำหนดโดยสองวิธีต่อไปนี้: __iter__และnext(Python 2) หรือ__next__(Python 3) ทั้งสองวิธีเหล่านี้ทำให้วัตถุเป็นตัววนซ้ำที่คุณสามารถตรวจสอบพิมพ์ด้วยIteratorAbstract Base Class จากcollectionsโมดูล

>>> def func():
...     yield 'I am'
...     yield 'a generator!'
... 
>>> type(func)                 # A function with yield is still a function
<type 'function'>
>>> gen = func()
>>> type(gen)                  # but it returns a generator
<type 'generator'>
>>> hasattr(gen, '__iter__')   # that's an iterable
True
>>> hasattr(gen, 'next')       # and with .next (.__next__ in Python 3)
True                           # implements the iterator protocol.

ประเภทเครื่องกำเนิดเป็นประเภทย่อยของตัววนซ้ำ:

>>> import collections, types
>>> issubclass(types.GeneratorType, collections.Iterator)
True

และหากจำเป็นเราสามารถตรวจสอบได้ดังนี้:

>>> isinstance(gen, types.GeneratorType)
True
>>> isinstance(gen, collections.Iterator)
True

คุณลักษณะหนึ่งIterator คือเมื่อหมดแล้วคุณจะไม่สามารถใช้ซ้ำหรือรีเซ็ตได้:

>>> list(gen)
['I am', 'a generator!']
>>> list(gen)
[]

คุณจะต้องสร้างใหม่หากคุณต้องการใช้ฟังก์ชันการทำงานอีกครั้ง (ดูเชิงอรรถ 2):

>>> list(func())
['I am', 'a generator!']

หนึ่งสามารถให้ข้อมูลทางโปรแกรมตัวอย่างเช่น:

def func(an_iterable):
    for item in an_iterable:
        yield item

ตัวสร้างแบบง่ายดังกล่าวข้างต้นนั้นเทียบเท่ากับด้านล่าง - ตั้งแต่ Python 3.3 (และไม่มีใน Python 2) คุณสามารถใช้yield from:

def func(an_iterable):
    yield from an_iterable

อย่างไรก็ตามyield fromยังอนุญาตให้มีการมอบหมายให้กับผู้สร้างรายย่อยซึ่งจะอธิบายในหัวข้อต่อไปนี้เกี่ยวกับการมอบหมายความร่วมมือกับกลุ่มย่อย

coroutines:

yield จัดทำนิพจน์ที่อนุญาตให้ส่งข้อมูลไปยังเครื่องกำเนิด (ดูเชิงอรรถ 3)

ต่อไปนี้เป็นตัวอย่างจดreceivedตัวแปรซึ่งจะชี้ไปยังข้อมูลที่ส่งไปยังเครื่องกำเนิด:

def bank_account(deposited, interest_rate):
    while True:
        calculated_interest = interest_rate * deposited 
        received = yield calculated_interest
        if received:
            deposited += received


>>> my_account = bank_account(1000, .05)

nextอันดับแรกเราต้องคิวขึ้นเครื่องกำเนิดไฟฟ้าที่มีฟังก์ชั่นในตัวที่ มันจะเรียกวิธีที่เหมาะสมnextหรือ__next__ขึ้นอยู่กับเวอร์ชันของ Python ที่คุณใช้:

>>> first_year_interest = next(my_account)
>>> first_year_interest
50.0

และตอนนี้เราสามารถส่งข้อมูลไปยังเครื่องกำเนิด (การส่งNoneเหมือนกับการโทรnext ):

>>> next_year_interest = my_account.send(first_year_interest + 1000)
>>> next_year_interest
102.5

คณะผู้แทนสหกรณ์ไปยัง Sub-Coroutine ด้วย yield from

ตอนนี้จำได้ว่าyield fromมีอยู่ใน Python 3 ซึ่งช่วยให้เราสามารถมอบหมาย Coroutines ให้กับ subcoroutine:

def money_manager(expected_rate):
    under_management = yield     # must receive deposited value
    while True:
        try:
            additional_investment = yield expected_rate * under_management 
            if additional_investment:
                under_management += additional_investment
        except GeneratorExit:
            '''TODO: write function to send unclaimed funds to state'''
        finally:
            '''TODO: write function to mail tax info to client'''


def investment_account(deposited, manager):
    '''very simple model of an investment account that delegates to a manager'''
    next(manager) # must queue up manager
    manager.send(deposited)
    while True:
        try:
            yield from manager
        except GeneratorExit:
            return manager.close()

และตอนนี้เราสามารถมอบหมายการทำงานให้กับเครื่องกำเนิดไฟฟ้าย่อยและมันสามารถใช้งานได้โดยเครื่องกำเนิดไฟฟ้าดังกล่าวข้างต้น:

>>> my_manager = money_manager(.06)
>>> my_account = investment_account(1000, my_manager)
>>> first_year_return = next(my_account)
>>> first_year_return
60.0
>>> next_year_return = my_account.send(first_year_return + 1000)
>>> next_year_return
123.6

คุณสามารถอ่านเพิ่มเติมเกี่ยวกับความหมายที่แม่นยำของyield fromในPEP 380

วิธีการอื่น: ปิดและโยน

closeวิธีการยกGeneratorExitที่จุดฟังก์ชั่นการดำเนินการถูกแช่แข็ง สิ่งนี้จะถูกเรียกโดย__del__เพื่อให้คุณสามารถใส่รหัสการล้างข้อมูลที่คุณจัดการกับGeneratorExit:

>>> my_account.close()

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

>>> import sys
>>> try:
...     raise ValueError
... except:
...     my_manager.throw(*sys.exc_info())
... 
Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
  File "<stdin>", line 2, in <module>
ValueError

ข้อสรุป

ฉันเชื่อว่าฉันได้ครอบคลุมทุกด้านของคำถามต่อไปนี้:

อะไรyieldคำหลักทำในงูใหญ่?

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


ภาคผนวก:

คำติชมของคำตอบที่ได้รับการยอมรับ **

  • มันสับสนกับสิ่งที่ทำให้iterableเพียงใช้ list เป็นตัวอย่าง ดูการอ้างอิงของฉันข้างต้น แต่ในการสรุป: การ iterable มี__iter__วิธีการส่งคืนiterator ตัววนซ้ำจัดเตรียมเมธอด.next(Python 2 หรือ.__next__(Python 3) ซึ่งเรียกโดยforอ้อมโดยลูปจนกว่าจะเพิ่มขึ้นStopIterationและเมื่อมันเกิดขึ้นก็จะดำเนินการต่อไป
  • จากนั้นใช้นิพจน์ตัวสร้างเพื่ออธิบายว่าตัวกำเนิดคืออะไร เนื่องจากเครื่องกำเนิดไฟฟ้าเป็นวิธีที่สะดวกในการสร้างตัววนซ้ำมันทำให้เกิดความสับสนเท่านั้นและเรายังไม่ได้รับyieldส่วนนี้
  • ในการควบคุมความอ่อนเพลียของเครื่องกำเนิดไฟฟ้าเขาเรียก.nextวิธีนี้เมื่อเขาควรใช้ฟังก์ชัน builtin nextแทน มันจะเป็นเลเยอร์ทางอ้อมที่เหมาะสมเพราะรหัสของเขาไม่ทำงานใน Python 3
  • Itertools? สิ่งนี้ไม่เกี่ยวข้องกับสิ่งyieldใดเลย
  • ไม่มีการอภิปรายถึงวิธีการที่yieldให้มาพร้อมกับฟังก์ชั่นใหม่yield fromใน Python 3 คำตอบยอดนิยม / คำตอบที่ยอมรับได้เป็นคำตอบที่ไม่สมบูรณ์

คำติชมของคำตอบที่แนะนำyieldในการแสดงออกกำเนิดหรือความเข้าใจ

ไวยากรณ์ในปัจจุบันช่วยให้การแสดงออกใด ๆ ในรายการความเข้าใจ

expr_stmt: testlist_star_expr (annassign | augassign (yield_expr|testlist) |
                     ('=' (yield_expr|testlist_star_expr))*)
...
yield_expr: 'yield' [yield_arg]
yield_arg: 'from' test | testlist

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

ผู้พัฒนาหลักของ CPython กำลังหารือเรื่องค่าเผื่อที่ลดลง นี่คือโพสต์ที่เกี่ยวข้องจากรายการจดหมาย:

วันที่ 30 มกราคม 2560 เวลา 19:05 น. Brett Cannon เขียนว่า:

On Sun, 29 Jan 2017 at 16:39 Craig Rodrigues wrote:

ฉันก็โอเคกับทั้งสองวิธี การออกจากสิ่งที่เป็นอยู่ใน Python 3 นั้นไม่ดีเลย IMHO

การลงคะแนนของฉันมันคือ SyntaxError เนื่องจากคุณไม่ได้รับสิ่งที่คุณคาดหวังจากไวยากรณ์

ฉันยอมรับว่าเป็นสถานที่ที่เหมาะสมสำหรับเราที่จะจบลงเพราะรหัสใด ๆ ที่อาศัยพฤติกรรมปัจจุบันนั้นฉลาดเกินกว่าที่จะรักษาได้

ในแง่ของการเดินทางเราอาจต้องการ:

  • SyntaxWarning หรือ DeprecationWarning ใน 3.7
  • การเตือน Py3k ใน 2.7.x
  • SyntaxError ใน 3.8

ไชโยนิค

- Nick Coghlan | ncoghlan ที่ gmail.com | บริสเบนออสเตรเลีย

นอกจากนี้ยังมีปัญหาที่โดดเด่น (10544)ซึ่งดูเหมือนว่าจะชี้ไปในทิศทางของสิ่งนี้ไม่เคยเป็นความคิดที่ดี (PyPy, การดำเนินการของ Python ที่เขียนใน Python กำลังเพิ่มการเตือนไวยากรณ์แล้ว)

บรรทัดล่างจนกว่าผู้พัฒนาของ CPython จะบอกเราเป็นอย่างอื่น: อย่าใส่yieldนิพจน์ตัวสร้างหรือความเข้าใจ

returnคำสั่งในเครื่องกำเนิดไฟฟ้า

ในPython 2 :

ในการทำงานของเครื่องกำเนิดไฟฟ้าที่คำสั่งไม่อนุญาตให้มีreturn expression_listในบริบทนั้นเปลือยreturnบ่งบอกว่าเครื่องกำเนิดเสร็จแล้วและจะทำให้เกิดStopIterationขึ้น

expression_listเป็นพื้นจำนวนของการแสดงออกใด ๆ คั่นด้วยเครื่องหมายจุลภาค - หลักในหลาม 2 คุณสามารถหยุดเครื่องกำเนิดไฟฟ้าที่มีreturnแต่คุณไม่สามารถคืนค่า

ในPython 3 :

ในฟังก์ชั่นเครื่องกำเนิดไฟฟ้าreturnคำสั่งระบุว่าเครื่องกำเนิดเสร็จแล้วและจะทำให้เกิดStopIterationการยกขึ้น ค่าที่ส่งคืน (ถ้ามี) ใช้เป็นอาร์กิวเมนต์เพื่อสร้างStopIterationและกลายเป็นStopIteration.valueแอตทริบิวต์

เชิงอรรถ

  1. ภาษา CLU, Sather และ Icon ถูกอ้างอิงในข้อเสนอเพื่อแนะนำแนวคิดของเครื่องกำเนิดไฟฟ้าให้กับ Python แนวคิดทั่วไปคือฟังก์ชั่นสามารถรักษาสถานะภายในและให้จุดข้อมูลกลางตามความต้องการของผู้ใช้ สิ่งนี้สัญญาว่าจะเหนือกว่าในด้านประสิทธิภาพของวิธีการอื่นรวมถึง Python threadingซึ่งไม่มีในบางระบบ

  2. ยกตัวอย่างเช่นนี่หมายความว่าxrangeวัตถุ ( rangeใน Python 3) ไม่ใช่Iterators แม้ว่ามันจะทำซ้ำได้เพราะพวกมันสามารถนำกลับมาใช้ใหม่ได้ เช่นเดียวกับรายการ__iter__วิธีการของพวกเขากลับวัตถุตัววนซ้ำ

  3. yieldถูกนำมาใช้เป็นคำสั่งซึ่งหมายความว่ามันจะปรากฏเฉพาะที่จุดเริ่มต้นของบรรทัดในบล็อกรหัส ตอนนี้yieldสร้างการแสดงออกของผลตอบแทน https://docs.python.org/2/reference/simple_stmts.html#grammar-token-yield_stmt การเปลี่ยนแปลงนี้เสนอเพื่อให้ผู้ใช้สามารถส่งข้อมูลไปยังเครื่องกำเนิดไฟฟ้าตามที่อาจได้รับ ในการส่งข้อมูลคุณจะต้องสามารถกำหนดข้อมูลให้กับบางสิ่งได้และคำสั่งนั้นจะไม่ทำงาน


328

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

ในกรณีของรหัสของคุณฟังก์ชั่นget_child_candidatesจะทำหน้าที่เหมือนตัววนซ้ำเพื่อที่เมื่อคุณขยายรายการของคุณมันจะเพิ่มองค์ประกอบทีละรายการในรายการใหม่

list.extendเรียกใช้ตัววนซ้ำจนกว่าจะหมด ในกรณีของตัวอย่างโค้ดที่คุณโพสต์มันจะชัดเจนมากขึ้นเพียงแค่ส่งคืนสิ่งอันดับและผนวกเข้ากับรายการ


107
นี่คือปิด แต่ไม่ถูกต้อง ทุกครั้งที่คุณเรียกใช้ฟังก์ชันที่มีคำสั่ง return มันจะส่งคืนวัตถุกำเนิดใหม่ เฉพาะเมื่อคุณเรียกใช้เมธอด. Next () ของตัวสร้างที่ดำเนินการต่อหลังจากได้รับผลผลิตครั้งสุดท้าย
kurosch

239

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

def fib():
    last, cur = 0, 1
    while True: 
        yield cur
        last, cur = cur, last + cur

จากนั้นฉันสามารถใช้มันในรหัสอื่นเช่นนี้:

for f in fib():
    if some_condition: break
    coolfuncs(f);

มันช่วยให้ปัญหาบางอย่างง่ายขึ้นและทำให้ทำงานง่ายขึ้นด้วย


233

สำหรับผู้ที่ต้องการตัวอย่างการทำงานที่น้อยที่สุดให้ทำสมาธิในเซสชัน Python แบบโต้ตอบนี้:

>>> def f():
...   yield 1
...   yield 2
...   yield 3
... 
>>> g = f()
>>> for i in g:
...   print(i)
... 
1
2
3
>>> for i in g:
...   print(i)
... 
>>> # Note that this time nothing was printed

209

TL; DR

แทนสิ่งนี้:

def square_list(n):
    the_list = []                         # Replace
    for x in range(n):
        y = x * x
        the_list.append(y)                # these
    return the_list                       # lines

ทำเช่นนี้:

def square_yield(n):
    for x in range(n):
        y = x * x
        yield y                           # with this one.

เมื่อใดก็ตามที่คุณพบว่าตัวเองสร้างรายการจากศูนย์yieldแต่ละชิ้นแทน

นี่เป็นช่วงเวลา "aha" ครั้งแรกของฉันพร้อมผลตอบแทน


yieldเป็นวิธีหวานที่จะพูด

สร้างชุดข้อมูล

พฤติกรรมเดียวกัน:

>>> for square in square_list(4):
...     print(square)
...
0
1
4
9
>>> for square in square_yield(4):
...     print(square)
...
0
1
4
9

พฤติกรรมที่แตกต่าง:

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

อัตราผลตอบแทนขี้เกียจมันทำให้การคำนวณ ฟังก์ชั่นที่ให้ผลตอบแทนนั้นไม่ได้ดำเนินการจริงเมื่อคุณเรียกใช้ มันส่งคืนวัตถุตัววนซ้ำที่จดจำตำแหน่งที่มันค้าง ทุกครั้งที่คุณเรียกnext()ใช้ตัววนซ้ำ (สิ่งนี้เกิดขึ้นใน for-loop) การประมวลผลนิ้วไปข้างหน้าเพื่อผลตอบแทนต่อไป returnยก StopIteration และสิ้นสุดซีรีส์ (นี่คือจุดสิ้นสุดตามธรรมชาติของ for-loop)

ผลผลิตที่หลากหลาย ข้อมูลไม่จำเป็นต้องจัดเก็บทั้งหมดเข้าด้วยกันสามารถจัดทำทีละรายการได้ มันไม่มีที่สิ้นสุด

>>> def squares_all_of_them():
...     x = 0
...     while True:
...         yield x * x
...         x += 1
...
>>> squares = squares_all_of_them()
>>> for _ in range(4):
...     print(next(squares))
...
0
1
4
9

หากคุณต้องการผ่านหลายรอบและซีรีส์ไม่ยาวเกินไปเพียงโทรหาlist():

>>> list(square_yield(4))
[0, 1, 4, 9]

ตัวเลือกที่ยอดเยี่ยมของคำyieldเพราะใช้ความหมายทั้งสอง :

ผลผลิต - ผลิตหรือจัดหา (เหมือนในภาคเกษตรกรรม)

... ระบุข้อมูลถัดไปในซีรี่ส์

ผลผลิต - หลีกทางหรือละทิ้ง (ในอำนาจทางการเมือง)

... ยกเลิกการทำงานของ CPU จนกว่าตัววนซ้ำจะเลื่อนไป


194

Yield ให้กำเนิดคุณ

def get_odd_numbers(i):
    return range(1, i, 2)
def yield_odd_numbers(i):
    for x in range(1, i, 2):
       yield x
foo = get_odd_numbers(10)
bar = yield_odd_numbers(10)
foo
[1, 3, 5, 7, 9]
bar
<generator object yield_odd_numbers at 0x1029c6f50>
bar.next()
1
bar.next()
3
bar.next()
5

อย่างที่คุณเห็นในกรณีแรกfooเก็บรายการทั้งหมดไว้ในหน่วยความจำพร้อมกัน ไม่ใช่เรื่องใหญ่สำหรับรายการที่มี 5 องค์ประกอบ แต่ถ้าคุณต้องการรายชื่อ 5 ล้านล่ะ ไม่เพียงแค่กินหน่วยความจำขนาดใหญ่เท่านั้น แต่ยังมีค่าใช้จ่ายในการสร้างในเวลาที่เรียกใช้ฟังก์ชั่น

ในกรณีที่สองbarให้กำเนิดกับคุณ เครื่องกำเนิดไฟฟ้าเป็น iterable - ซึ่งหมายความว่าคุณสามารถใช้มันในการforวนซ้ำ ฯลฯ แต่แต่ละค่าสามารถเข้าถึงได้เพียงครั้งเดียว ค่าทั้งหมดจะไม่ถูกเก็บไว้ในหน่วยความจำในเวลาเดียวกัน วัตถุเครื่องกำเนิดไฟฟ้า "จำ" ที่อยู่ในการวนซ้ำครั้งล่าสุดที่คุณเรียกมันว่า - ด้วยวิธีนี้ถ้าคุณใช้การนับ (พูด) นับเป็น 50 พันล้านคุณไม่ต้องนับทั้งหมด 50 พันล้าน ในทันทีและเก็บตัวเลข 50 พันล้านให้นับ

นี่เป็นอีกตัวอย่างที่คุณควรใช้ itertools หากคุณต้องการนับถึง 50 พันล้าน :)

นี่เป็นกรณีที่ใช้ง่ายที่สุดของเครื่องกำเนิดไฟฟ้า ดังที่คุณกล่าวว่ามันสามารถใช้ในการเขียนเรียงสับเปลี่ยนที่มีประสิทธิภาพโดยใช้ผลผลิตเพื่อผลักดันสิ่งต่าง ๆ ผ่าน call stack แทนการใช้ตัวแปร stack บางชนิด เครื่องกำเนิดไฟฟ้ายังสามารถใช้สำหรับการสำรวจเส้นทางด้วยต้นไม้แบบพิเศษและทุกอย่างของสิ่งอื่น ๆ


เพียงโน้ต - ใน Python 3 rangeก็ส่งคืนตัวสร้างแทนรายการดังนั้นคุณจะเห็นแนวคิดที่คล้ายกันยกเว้นว่า__repr__/ __str__ถูกเขียนทับเพื่อแสดงผลลัพธ์ที่ดีกว่าในกรณีrange(1, 10, 2)นี้
It'sNotALie

189

กำลังส่งคืนตัวกำเนิด ฉันไม่คุ้นเคยกับ Python เป็นพิเศษ แต่ฉันเชื่อว่ามันเป็นสิ่งเดียวกันกับตัวทำซ้ำของC #หากคุณคุ้นเคยกับสิ่งเหล่านี้

ความคิดที่สำคัญคือคอมไพเลอร์ / ล่าม / สิ่งไม่ใช้กลอุบายบางอย่างเพื่อให้ไกลที่สุดเท่าที่โทรเป็นห่วงพวกเขาสามารถเก็บโทรถัดไป () และมันจะทำให้ค่ากลับ - เช่นถ้าวิธีการกำเนิดไฟฟ้าถูกหยุดชั่วคราว ตอนนี้เห็นได้ชัดว่าคุณไม่สามารถ "หยุด" วิธีการจริงๆได้ดังนั้นคอมไพเลอร์จะสร้างเครื่องสถานะเพื่อให้คุณจำได้ว่าคุณอยู่ที่ไหนและตัวแปรในตัวเครื่องมีลักษณะอย่างไร มันง่ายกว่าการเขียนตัววนซ้ำด้วยตัวเอง


167

มีคำตอบประเภทหนึ่งที่ฉันยังไม่ได้รับในคำตอบที่ยอดเยี่ยมมากมายที่อธิบายถึงวิธีการใช้เครื่องกำเนิดไฟฟ้า นี่คือคำตอบทฤษฎีภาษาโปรแกรม:

yieldคำสั่งในหลามผลตอบแทนที่เครื่องกำเนิดไฟฟ้า เครื่องกำเนิดไฟฟ้าใน Python เป็นฟังก์ชั่นที่คืนค่าการต่อเนื่อง (และโดยเฉพาะประเภทของ coroutine แต่การทำต่อเนื่องนั้นแสดงถึงกลไกทั่วไปที่มากขึ้นเพื่อทำความเข้าใจกับสิ่งที่เกิดขึ้น)

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

ต่อเนื่องในรูปแบบทั่วไปนี้สามารถนำมาใช้ได้สองวิธี ในcall/ccทางสแต็กของโปรแกรมจะถูกบันทึกอย่างแท้จริงจากนั้นเมื่อมีการเรียกใช้ความต่อเนื่องสแต็กจะถูกกู้คืน

ในรูปแบบการส่งต่อ (CPS) การทำต่อเนื่องเป็นเพียงฟังก์ชั่นปกติ (เฉพาะในภาษาที่ฟังก์ชั่นเป็นเฟิร์สคลาส) ซึ่งโปรแกรมเมอร์ทำการจัดการและส่งผ่านไปยังรูทีนย่อยอย่างชัดเจน ในรูปแบบนี้สถานะของโปรแกรมจะถูกแทนด้วยการปิด (และตัวแปรที่จะถูกเข้ารหัสใน) แทนที่จะเป็นตัวแปรที่อยู่ในสแต็ก ฟังก์ชั่นที่จัดการโฟลว์ควบคุมยอมรับความต่อเนื่องในฐานะอาร์กิวเมนต์ (ในรูปแบบบางอย่างของ CPS, ฟังก์ชั่นอาจยอมรับหลายอินเทอร์เฟซ) และจัดการกับโฟลว์ควบคุมโดยเรียกพวกมัน ตัวอย่างง่ายๆของการส่งผ่านสไตล์ที่ต่อเนื่องมีดังนี้:

def save_file(filename):
  def write_file_continuation():
    write_stuff_to_file(filename)

  check_if_file_exists_and_user_wants_to_overwrite(write_file_continuation)

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

ส่วนที่เหลือของโพสต์นี้จะไม่ทำให้สูญเสียความคิดทั่วไปคิดต่อเนื่องเป็น CPS เพราะมันเป็นนรกที่ง่ายต่อการเข้าใจและอ่าน


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

def f():
  while True:
    yield 4

นี่คือการทำซ้ำที่สมเหตุสมผลซึ่งชัดเจนว่ามีพฤติกรรมที่กำหนดไว้อย่างดี - ทุกครั้งที่ตัวกำเนิดซ้ำทำซ้ำจะส่งคืนค่า 4 (และจะทำตลอดไป) แต่มันก็ไม่น่าจะเป็นประเภทต้นแบบของ iterable ที่นึกถึงเมื่อคิดถึง iterators (เช่นfor x in collection: do_something(x)) ตัวอย่างนี้แสดงให้เห็นถึงพลังของเครื่องกำเนิดไฟฟ้า: หากมีสิ่งใดเป็นตัววนซ้ำตัวกำเนิดสามารถบันทึกสถานะการวนซ้ำได้

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

แต่คุณสามารถปรับใช้เครื่องกำเนิดไฟฟ้า (และสร้างแนวคิด) ได้อย่างง่ายดายในรูปแบบที่เรียบง่ายและเฉพาะเจาะจงของรูปแบบการส่งต่อความต่อเนื่อง:

เมื่อใดก็ตามที่yieldถูกเรียกมันจะบอกให้ฟังก์ชั่นเพื่อกลับมามีความต่อเนื่อง เมื่อเรียกใช้ฟังก์ชันอีกครั้งฟังก์ชันจะเริ่มจากทุกที่ที่เหลือ ดังนั้นใน pseudo-pseudocode (เช่นไม่ใช่ pseudocode แต่ไม่ใช่รหัส) nextเมธอดของ generator นั้นเป็นดังนี้:

class Generator():
  def __init__(self,iterable,generatorfun):
    self.next_continuation = lambda:generatorfun(iterable)

  def next(self):
    value, next_continuation = self.next_continuation()
    self.next_continuation = next_continuation
    return value

โดยที่yieldคำหลักนั้นเป็นน้ำตาลซินแทคติกสำหรับฟังก์ชันตัวสร้างที่แท้จริงโดยทั่วไปจะมีลักษณะดังนี้:

def generatorfun(iterable):
  if len(iterable) == 0:
    raise StopIteration
  else:
    return (iterable[0], lambda:generatorfun(iterable[1:]))

โปรดจำไว้ว่านี่เป็นเพียง pseudocode และการใช้งานจริงของเครื่องกำเนิดไฟฟ้าใน Python นั้นซับซ้อนกว่ามาก แต่เป็นแบบฝึกหัดที่จะเข้าใจสิ่งที่เกิดขึ้นให้ลองใช้รูปแบบการส่งต่อความต่อเนื่องในการใช้วัตถุเครื่องกำเนิดไฟฟ้าโดยไม่ต้องใช้yieldคำหลัก


152

นี่คือตัวอย่างในภาษาธรรมดา ฉันจะให้การติดต่อระหว่างแนวคิดมนุษย์ระดับสูงกับแนวคิด Python ระดับต่ำ

ฉันต้องการดำเนินการตามลำดับของตัวเลข แต่ฉันไม่ต้องการรบกวนตัวเองด้วยการสร้างลำดับนั้นฉันต้องการเพียงเน้นการดำเนินการที่ฉันต้องการทำ ดังนั้นฉันทำต่อไปนี้:

  • ฉันโทรหาคุณและบอกคุณว่าฉันต้องการลำดับของตัวเลขที่สร้างขึ้นในแบบเฉพาะเจาะจงและฉันแจ้งให้คุณทราบว่าอัลกอริทึมคืออะไร
    ขั้นตอนนี้จะสอดคล้องกับdefining yieldฟังก์ชั่นเครื่องกำเนิดไฟฟ้าเช่นฟังก์ชั่นที่มี
  • บางครั้งฉันก็บอกคุณว่า "โอเคเตรียมพร้อมที่จะบอกลำดับของตัวเลข"
    ขั้นตอนนี้สอดคล้องกับการเรียกใช้ฟังก์ชันตัวสร้างซึ่งส่งคืนวัตถุตัวสร้าง โปรดทราบว่าคุณยังไม่ได้บอกตัวเลขใด ๆ คุณเพิ่งคว้ากระดาษและดินสอของคุณ
  • ฉันถามคุณว่า "บอกหมายเลขถัดไป" และคุณบอกหมายเลขแรกให้ฉัน หลังจากนั้นคุณรอฉันเพื่อขอหมายเลขถัดไป เป็นหน้าที่ของคุณในการจำตำแหน่งที่คุณอยู่ตัวเลขที่คุณพูดไปแล้วและหมายเลขถัดไปคืออะไร ฉันไม่สนใจรายละเอียด
    ขั้นตอนนี้สอดคล้องกับการเรียก.next()วัตถุกำเนิด
  • ... ทำซ้ำขั้นตอนก่อนหน้าจนกระทั่ง ...
  • ในที่สุดคุณอาจสิ้นสุด คุณไม่บอกตัวเลข คุณเพียงแค่ตะโกนว่า "จับม้าของคุณ! ฉันเสร็จแล้วไม่มีตัวเลขอีกต่อไป!"
    ขั้นตอนนี้สอดคล้องกับวัตถุตัวสร้างสิ้นสุดงานและเพิ่มStopIterationข้อยกเว้นฟังก์ชันตัวสร้างไม่จำเป็นต้องยกข้อยกเว้น returnมันยกขึ้นโดยอัตโนมัติเมื่อมีฟังก์ชั่นปลายหรือปัญหา

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

ผู้ใช้โปรโตคอล iterator ที่มีชื่อเสียงที่สุดคือforคำสั่งใน Python ดังนั้นเมื่อใดก็ตามที่คุณทำ:

for item in sequence:

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

โปรดทราบว่าdefการใช้ฟังก์ชันที่มีyieldคำหลักไม่ใช่วิธีเดียวในการสร้างเครื่องกำเนิด มันเป็นวิธีที่ง่ายที่สุดในการสร้าง

สำหรับข้อมูลที่ถูกต้องมากขึ้นอ่านเกี่ยวกับประเภท iteratorที่งบผลผลิตและเครื่องกำเนิดไฟฟ้าในเอกสารหลาม


130

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

เพื่อช่วยให้เข้าใจสิ่งที่yieldไม่อยู่ในรหัสต่อไปนี้คุณสามารถใช้นิ้วของคุณเพื่อติดตามวงจรรหัสผ่านใด ๆ yieldที่มี เวลาที่นิ้วของคุณฮิตทุกyieldคุณจะต้องรอnextหรือsendจะป้อน เมื่อมีการnextเรียกa คุณจะติดตามรหัสจนกว่าคุณจะกดปุ่มyield... รหัสทางด้านขวาของรหัสyieldจะถูกประเมินและส่งกลับไปยังผู้โทร…จากนั้นคุณรอ เมื่อnextมีการเรียกอีกครั้งคุณจะทำการวนซ้ำอีกครั้งผ่านรหัส อย่างไรก็ตามคุณจะทราบว่าใน coroutine yieldสามารถใช้กับsend... ซึ่งจะส่งค่าจากผู้เรียกไปยังฟังก์ชันการให้ผลผลิต หากsendได้รับจากนั้นyieldรับค่าที่ส่งและแยกออกทางด้านซ้ายมือ…จากนั้นการติดตามผ่านโค้ดจะดำเนินต่อไปจนกว่าคุณจะกดถูกเรียก)yield อีกครั้ง (คืนค่าในตอนท้ายราวกับว่า next

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

>>> def coroutine():
...     i = -1
...     while True:
...         i += 1
...         val = (yield i)
...         print("Received %s" % val)
...
>>> sequence = coroutine()
>>> sequence.next()
0
>>> sequence.next()
Received None
1
>>> sequence.send('hello')
Received hello
2
>>> sequence.close()

น่ารัก! ผ้าใบ (ในความรู้สึกชัด) ไม่บ่อยนักที่จะเห็นสิ่งเหล่านั้น!
00prometheus

129

มีการyieldใช้และความหมายอื่น (ตั้งแต่ Python 3.3):

yield from <expr>

จากPEP 380 - ไวยากรณ์สำหรับการมอบสิทธิ์ให้กับ Subgenerator :

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

ไวยากรณ์ใหม่ยังเปิดโอกาสในการปรับให้เหมาะสมเมื่อตัวสร้างตัวหนึ่งให้ค่าที่ผลิตโดยอีกตัวหนึ่ง

นอกจากนี้สิ่งนี้จะแนะนำ (ตั้งแต่ Python 3.5):

async def new_coroutine(data):
   ...
   await blocking_action()

เพื่อหลีกเลี่ยงการถูกสับสนกับเครื่องกำเนิด coroutines (วันนี้yieldใช้ทั้งคู่)


117

คำตอบที่ดีทั้งหมด แต่เป็นเรื่องยากสำหรับมือใหม่

ฉันคิดว่าคุณได้เรียนรู้ returnแล้ว

ในฐานะที่เป็นการเปรียบเทียบreturnและyieldเป็นฝาแฝด returnหมายถึง 'ผลตอบแทนและหยุด' ในขณะที่ 'ผลตอบแทน' หมายถึง 'ผลตอบแทน แต่ดำเนินต่อไป'

  1. พยายามที่จะรับ num_list returnด้วย
def num_list(n):
    for i in range(n):
        return i

เรียกใช้:

In [5]: num_list(3)
Out[5]: 0

ดูว่าคุณจะได้รับเพียงตัวเลขเดียวแทนที่จะเป็นรายการของพวกเขา returnไม่เคยช่วยให้คุณชนะอย่างมีความสุขเพียงแค่ใช้ครั้งเดียวและออก

  1. มาถึงแล้ว yield

แทนที่returnด้วยyield:

In [10]: def num_list(n):
    ...:     for i in range(n):
    ...:         yield i
    ...:

In [11]: num_list(3)
Out[11]: <generator object num_list at 0x10327c990>

In [12]: list(num_list(3))
Out[12]: [0, 1, 2]

ตอนนี้คุณชนะเพื่อรับหมายเลขทั้งหมด

เปรียบเทียบกับreturnสิ่งที่ทำงานครั้งเดียวและหยุดyieldวิ่งเวลาที่คุณวางแผน คุณสามารถตีความreturnเป็นreturn one of themและเป็นyield นี้เรียกว่าreturn all of themiterable

  1. อีกหนึ่งขั้นตอนเราสามารถเขียนyieldคำสั่งอีกครั้งด้วยreturn
In [15]: def num_list(n):
    ...:     result = []
    ...:     for i in range(n):
    ...:         result.append(i)
    ...:     return result

In [16]: num_list(3)
Out[16]: [0, 1, 2]

มันเป็นแกนกลางเกี่ยวกับ yieldมันเป็นหลักเกี่ยวกับ

ความแตกต่างระหว่างรายการreturnผลลัพธ์และวัตถุyieldผลลัพธ์คือ:

คุณจะได้รับ [0, 1, 2] จากวัตถุรายการ แต่สามารถเรียกคืนได้จาก ' yieldผลลัพธ์วัตถุ' เพียงครั้งเดียว ดังนั้นจึงมีgeneratorวัตถุชื่อใหม่ตามที่ปรากฏในOut[11]: <generator object num_list at 0x10327c990>วัตถุที่แสดงใน

โดยสรุปเป็นคำอุปมาที่จะ grok มัน:

  • return และ yieldเป็นฝาแฝด
  • listและgeneratorเป็นฝาแฝด

สิ่งนี้สามารถเข้าใจได้ แต่ความแตกต่างที่สำคัญอย่างหนึ่งก็คือคุณสามารถมีผลตอบแทนหลายอย่างในฟังก์ชัน / เมธอด การเปรียบเทียบทั้งหมดลงไปที่จุดนั้น อัตราผลตอบแทนจำสถานที่ในการทำงานดังนั้นครั้งต่อไปที่คุณโทรถัดไป () yieldฟังก์ชั่นของคุณยังคงที่ไปข้างหน้า ฉันคิดว่านี่เป็นสิ่งสำคัญและควรแสดงออก
Mike S

104

นี่คือตัวอย่างของ Python เกี่ยวกับวิธีการนำเครื่องกำเนิดไฟฟ้าไปใช้จริงราวกับว่า Python ไม่ได้ให้น้ำตาล syntactic สำหรับพวกเขา:

เป็นตัวสร้าง Python:

from itertools import islice

def fib_gen():
    a, b = 1, 1
    while True:
        yield a
        a, b = b, a + b

assert [1, 1, 2, 3, 5] == list(islice(fib_gen(), 5))

การใช้คำศัพท์ปิดแทนเครื่องกำเนิดไฟฟ้า

def ftake(fnext, last):
    return [fnext() for _ in xrange(last)]

def fib_gen2():
    #funky scope due to python2.x workaround
    #for python 3.x use nonlocal
    def _():
        _.a, _.b = _.b, _.a + _.b
        return _.a
    _.a, _.b = 0, 1
    return _

assert [1,1,2,3,5] == ftake(fib_gen2(), 5)

การใช้การปิดวัตถุแทนเครื่องกำเนิดไฟฟ้า (เพราะClosuresAndObjectsAreEquivalent )

class fib_gen3:
    def __init__(self):
        self.a, self.b = 1, 1

    def __call__(self):
        r = self.a
        self.a, self.b = self.b, self.a + self.b
        return r

assert [1,1,2,3,5] == ftake(fib_gen3(), 5)

97

ฉันกำลังจะโพสต์ "อ่านหน้า 19 ของ 'Python: Essential Reference' ของ Beazley สำหรับคำอธิบายอย่างรวดเร็วของเครื่องกำเนิดไฟฟ้า" แต่มีคนอื่นอีกหลายคนที่โพสต์คำอธิบายที่ดีอยู่แล้ว

นอกจากนี้โปรดทราบว่าyieldสามารถใช้ใน coroutines เป็นคู่ของการใช้งานในฟังก์ชั่นเครื่องกำเนิดไฟฟ้า แม้ว่าจะไม่ใช่การใช้งานแบบเดียวกับข้อมูลโค้ดของคุณ แต่(yield)สามารถใช้เป็นนิพจน์ในฟังก์ชันได้ เมื่อผู้เรียกส่งค่าไปยังเมธอดโดยใช้send()เมธอด coroutine จะดำเนินการจนกระทั่งต่อไป(yield)จะพบคำสั่ง

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


97

จากมุมมองการเขียนโปรแกรม iterators จะถูกนำมาใช้เป็นthunks

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

http://en.wikipedia.org/wiki/Message_passing

" ถัดไป " เป็นข้อความที่ส่งถึงการปิดสร้างโดยการเรียก " iter "

มีวิธีมากมายในการนำการคำนวณนี้ไปใช้ ฉันใช้การกลายพันธุ์ แต่มันง่ายที่จะทำโดยไม่มีการกลายพันธุ์โดยการคืนค่าปัจจุบันและ yielder ถัดไป

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

Welcome to Racket v6.5.0.3.

-> (define gen
     (lambda (l)
       (define yield
         (lambda ()
           (if (null? l)
               'END
               (let ((v (car l)))
                 (set! l (cdr l))
                 v))))
       (lambda(m)
         (case m
           ('yield (yield))
           ('init  (lambda (data)
                     (set! l data)
                     'OK))))))
-> (define stream (gen '(1 2 3)))
-> (stream 'yield)
1
-> (stream 'yield)
2
-> (stream 'yield)
3
-> (stream 'yield)
'END
-> ((stream 'init) '(a b))
'OK
-> (stream 'yield)
'a
-> (stream 'yield)
'b
-> (stream 'yield)
'END
-> (stream 'yield)
'END
->

84

นี่คือตัวอย่างง่ายๆ:

def isPrimeNumber(n):
    print "isPrimeNumber({}) call".format(n)
    if n==1:
        return False
    for x in range(2,n):
        if n % x == 0:
            return False
    return True

def primes (n=1):
    while(True):
        print "loop step ---------------- {}".format(n)
        if isPrimeNumber(n): yield n
        n += 1

for n in primes():
    if n> 10:break
    print "wiriting result {}".format(n)

เอาท์พุท:

loop step ---------------- 1
isPrimeNumber(1) call
loop step ---------------- 2
isPrimeNumber(2) call
loop step ---------------- 3
isPrimeNumber(3) call
wiriting result 3
loop step ---------------- 4
isPrimeNumber(4) call
loop step ---------------- 5
isPrimeNumber(5) call
wiriting result 5
loop step ---------------- 6
isPrimeNumber(6) call
loop step ---------------- 7
isPrimeNumber(7) call
wiriting result 7
loop step ---------------- 8
isPrimeNumber(8) call
loop step ---------------- 9
isPrimeNumber(9) call
loop step ---------------- 10
isPrimeNumber(10) call
loop step ---------------- 11
isPrimeNumber(11) call

ฉันไม่ใช่นักพัฒนางูหลาม แต่มันดูน่าสนใจสำหรับฉัน yieldดำรงตำแหน่งของโฟลว์โปรแกรมและลูปถัดไปเริ่มต้นจากตำแหน่ง "yield" ดูเหมือนว่ากำลังรอที่ตำแหน่งนั้นและก่อนหน้านั้นคืนค่านอกและครั้งต่อไปยังคงทำงาน

ดูเหมือนว่าจะเป็นความสามารถที่น่าสนใจและดี: D


คุณถูก. แต่อะไรคือผลกระทบต่อการไหลซึ่งจะเห็นพฤติกรรมของ "ผลผลิต"? ฉันสามารถเปลี่ยนอัลกอริทึมในชื่อคณิตศาสตร์ มันจะช่วยให้ได้รับการประเมินที่แตกต่างของ "ผลผลิต"?
Engin OZTURK

68

นี่คือภาพจิตของสิ่งที่yieldทำ

ฉันชอบคิดเธรดว่ามีสแต็ก (แม้ว่าจะไม่ถูกนำไปใช้ในทางนั้น)

เมื่อเรียกฟังก์ชั่นปกติมันจะวางตัวแปรท้องถิ่นของมันลงบนสแต็กทำการคำนวณบางอย่างจากนั้นล้างสแต็กและส่งคืน ค่าของตัวแปรท้องถิ่นจะไม่เคยเห็นอีกครั้ง

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

ดังนั้นมันจึงเป็นฟังก์ชั่นแช่แข็งที่เครื่องกำเนิดไฟฟ้าแขวนอยู่

เมื่อnext()เรียกใช้ในภายหลังมันจะเรียกคืนข้าวของของฟังก์ชั่นลงบนสแต็กและทำให้เคลื่อนไหวอีกครั้ง ฟังก์ชั่นยังคงคำนวณจากจุดที่มันค้างไว้โดยไม่สนใจความจริงที่ว่ามันเพิ่งใช้เวลาชั่วนิรันดร์ในห้องเย็น

เปรียบเทียบตัวอย่างต่อไปนี้:

def normalFunction():
    return
    if False:
        pass

def yielderFunction():
    return
    if False:
        yield 12

เมื่อเราเรียกฟังก์ชั่นที่สองมันจะทำงานแตกต่างไปจากฟังก์ชั่นแรกมาก yieldคำสั่งอาจจะไม่สามารถเข้าถึงได้ แต่ถ้ามันเป็นปัจจุบันทุกที่มันเปลี่ยนแปลงธรรมชาติของสิ่งที่เรากำลังติดต่อกับ

>>> yielderFunction()
<generator object yielderFunction at 0x07742D28>

การเรียกyielderFunction()ไม่เรียกใช้รหัส แต่สร้างตัวสร้างจากรหัส (อาจเป็นความคิดที่ดีที่จะตั้งชื่อสิ่งต่าง ๆ ด้วยyielderคำนำหน้าเพื่อให้สามารถอ่านได้)

>>> gen = yielderFunction()
>>> dir(gen)
['__class__',
 ...
 '__iter__',    #Returns gen itself, to make it work uniformly with containers
 ...            #when given to a for loop. (Containers return an iterator instead.)
 'close',
 'gi_code',
 'gi_frame',
 'gi_running',
 'next',        #The method that runs the function's body.
 'send',
 'throw']

gi_codeและgi_frameสาขาที่เป็นที่ที่รัฐแช่แข็งจะถูกเก็บไว้ สำรวจพวกเขาด้วยdir(..)เราสามารถยืนยันได้ว่าแบบจำลองจิตของเราด้านบนมีความน่าเชื่อถือ


59

เช่นเดียวกับทุกคำตอบที่แนะนำyieldใช้สำหรับการสร้างตัวสร้างลำดับ มันใช้สำหรับสร้างบางลำดับแบบไดนามิก ตัวอย่างเช่นในขณะที่อ่านไฟล์ทีละบรรทัดบนเครือข่ายคุณสามารถใช้yieldฟังก์ชั่นดังต่อไปนี้:

def getNextLines():
   while con.isOpen():
       yield con.read()

คุณสามารถใช้มันในรหัสของคุณดังต่อไปนี้:

for line in getNextLines():
    doSomeThing(line)

การโอนการควบคุมการดำเนินการ gotcha

การควบคุมการดำเนินการจะถูกโอนจาก getNextLines () ไปยังforลูปเมื่อมีการดำเนินการผลผลิต ดังนั้นทุกครั้งที่มีการเรียกใช้ getNextLines () การประมวลผลจะเริ่มต้นจากจุดที่หยุดชั่วคราวเป็นครั้งสุดท้าย

ดังนั้นในระยะสั้นฟังก์ชั่นที่มีรหัสต่อไปนี้

def simpleYield():
    yield "first time"
    yield "second time"
    yield "third time"
    yield "Now some useful value {}".format(12)

for i in simpleYield():
    print i

จะพิมพ์

"first time"
"second time"
"third time"
"Now some useful value 12"

59

ตัวอย่างง่าย ๆ ที่จะเข้าใจว่ามันคืออะไร: yield

def f123():
    for _ in range(4):
        yield 1
        yield 2


for i in f123():
    print (i)

ผลลัพธ์คือ:

1 2 1 2 1 2 1 2

5
คุณแน่ใจเกี่ยวกับผลลัพธ์นั้นหรือไม่ จะไม่พิมพ์บนบรรทัดเดียวถ้าคุณใช้คำสั่งพิมพ์ที่ใช้print(i, end=' ')? มิฉะนั้นฉันเชื่อว่าพฤติกรรมเริ่มต้นจะใส่แต่ละหมายเลขในบรรทัดใหม่
user9074332

@ user9074332 คุณพูดถูก แต่เขียนในบรรทัดเดียวเพื่อให้เข้าใจง่ายขึ้น
Gavriel Cohen

57

(คำตอบด้านล่างของฉันพูดจากมุมมองของการใช้ตัวสร้าง Python เท่านั้นไม่ใช่การใช้กลไกตัวสร้างซึ่งเกี่ยวข้องกับเทคนิคบางอย่างของกองซ้อนและการจัดการฮีป)

เมื่อyieldถูกนำมาใช้แทนreturnในการทำงานหลาม, generator functionฟังก์ชั่นที่กลายเป็นสิ่งที่พิเศษที่เรียกว่า ฟังก์ชันนั้นจะส่งคืนออบเจ็กต์generatorประเภท คำหลักคือธงที่จะแจ้งให้คอมไพเลอร์หลามฟังก์ชั่นในการรักษาดังกล่าวเป็นพิเศษ ฟังก์ชั่นปกติจะสิ้นสุดลงเมื่อมีการส่งคืนค่าบางอย่างจากมัน แต่ด้วยความช่วยเหลือของคอมไพเลอร์ฟังก์ชั่นเครื่องกำเนิดไฟฟ้าสามารถคิดว่าการทำงานต่อได้ นั่นคือบริบทการดำเนินการจะถูกกู้คืนและการดำเนินการจะดำเนินต่อจากการดำเนินการครั้งสุดท้าย จนกว่าคุณจะโทรกลับอย่างชัดเจนซึ่งจะเพิ่มข้อยกเว้น (ซึ่งเป็นส่วนหนึ่งของโปรโตคอลตัววนซ้ำ) หรือถึงจุดสิ้นสุดของฟังก์ชัน ผมพบว่าจำนวนมากของการอ้างอิงเกี่ยวกับแต่อย่างใดอย่างหนึ่งyieldStopIterationgeneratorจากการย่อยfunctional programming perspectiveได้มากที่สุด

(ตอนนี้ฉันต้องการพูดถึงเหตุผลเบื้องหลังgeneratorและiteratorตามความเข้าใจของฉันเองฉันหวังว่าสิ่งนี้จะช่วยให้คุณเข้าใจแรงจูงใจที่สำคัญของตัววนซ้ำและตัวสร้างแนวคิดนี้แสดงในภาษาอื่นเช่น C #)

ตามที่ฉันเข้าใจเมื่อเราต้องการประมวลผลข้อมูลจำนวนมากเรามักจะเก็บข้อมูลไว้ที่ใดที่หนึ่งแล้วจึงประมวลผลข้อมูลทีละรายการ แต่วิธีการไร้เดียงสานี้เป็นปัญหา ถ้าปริมาณข้อมูลมีขนาดใหญ่มันก็แพงที่จะเก็บไว้ทั้งหมด ดังนั้นแทนที่จะจัดเก็บdataเองโดยตรงทำไมไม่เก็บชนิดของmetadatathe logic how the data is computedทางอ้อมคือ

มี 2 ​​วิธีในการห่อข้อมูลเมตาดังกล่าว

  1. วิธี OO as a classเราห่อเมตาดาต้า นี่คือสิ่งที่เรียกว่าiteratorใช้โปรโตคอล iterator (เช่น__next__(), และ__iter__()วิธีการ) และนี่ก็เป็นปกติเห็นรูปแบบการออกแบบ iterator
  2. as a functionวิธีการทำงานเราห่อเมตาดาต้า generator functionนี่คือสิ่งที่เรียกว่า แต่ภายใต้ฝากระโปรงตัวส่งคืนgenerator objectยังคงเป็นตัวIS-Aวนซ้ำเนื่องจากใช้โปรโตคอลตัววนซ้ำ

ไม่ว่าจะด้วยวิธีใดจะมีการสร้างตัววนซ้ำนั่นคือวัตถุบางอย่างที่สามารถให้ข้อมูลที่คุณต้องการ วิธีการ OO อาจซับซ้อนเล็กน้อย อย่างไรก็ตามข้อใดที่จะใช้ขึ้นอยู่กับคุณ


54

โดยสรุปแล้วyieldคำสั่งนี้จะแปลงฟังก์ชันของคุณให้เป็นโรงงานที่สร้างวัตถุพิเศษที่เรียกว่า a generatorซึ่งล้อมรอบเนื้อหาของฟังก์ชันต้นฉบับของคุณ เมื่อgeneratorมีการซ้ำจะรันการทำงานของคุณจนกว่าจะถึงต่อไปแล้วระงับการดำเนินการและการประเมินค่าที่ส่งไปyield yieldมันทำซ้ำกระบวนการนี้ในการทำซ้ำแต่ละครั้งจนกว่าเส้นทางของการดำเนินการออกจากฟังก์ชั่น ตัวอย่างเช่น

def simple_generator():
    yield 'one'
    yield 'two'
    yield 'three'

for i in simple_generator():
    print i

เพียงแค่ส่งออก

one
two
three

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

สมมติว่าคุณต้องการสร้างrangeฟังก์ชั่นของคุณเองที่สร้างช่วงของตัวเลขที่ซ้ำได้คุณสามารถทำได้เช่นนั้น

def myRangeNaive(i):
    n = 0
    range = []
    while n < i:
        range.append(n)
        n = n + 1
    return range

และใช้มันแบบนี้

for i in myRangeNaive(10):
    print i

แต่สิ่งนี้ไม่มีประสิทธิภาพเพราะ

  • คุณสร้างอาร์เรย์ที่คุณใช้เพียงครั้งเดียวเท่านั้น (นี่เป็นการสูญเสียความทรงจำ)
  • รหัสนี้จะวนซ้ำไปมาอีกสองแถว! :(

โชคดีที่กุยโด้และทีมของเขามีน้ำใจมากพอที่จะพัฒนาเครื่องปั่นไฟเพื่อให้เราทำได้

def myRangeSmart(i):
    n = 0
    while n < i:
       yield n
       n = n + 1
    return

for i in myRangeSmart(10):
    print i

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


53

ผลตอบแทนเป็นวัตถุ

A returnในฟังก์ชั่นจะคืนค่าเดียว

หากคุณต้องการฟังก์ชั่นที่จะกลับมาเป็นชุดใหญ่ของค่าyieldใช้

มีความสำคัญมากyieldเป็นอุปสรรค

เหมือนกับสิ่งกีดขวางในภาษา CUDA มันจะไม่ถ่ายโอนการควบคุมจนกว่าจะเสร็จสมบูรณ์

yieldนั่นคือมันจะเรียกใช้รหัสในการทำงานของคุณตั้งแต่ต้นจนฮิต จากนั้นมันจะคืนค่าแรกของลูป

จากนั้นทุกการโทรอื่น ๆ จะเรียกใช้ลูปที่คุณเขียนในฟังก์ชั่นอีกครั้งหนึ่งคืนค่าถัดไปจนกว่าจะไม่มีค่าใด ๆ ที่จะส่งกลับ


52

หลายคนใช้returnมากกว่าyieldแต่ในบางกรณีyieldอาจมีประสิทธิภาพและทำงานได้ง่ายขึ้น

นี่คือตัวอย่างที่yieldดีที่สุดสำหรับ:

ผลตอบแทน (ในฟังก์ชั่น)

import random

def return_dates():
    dates = [] # With 'return' you need to create a list then return it
    for i in range(5):
        date = random.choice(["1st", "2nd", "3rd", "4th", "5th", "6th", "7th", "8th", "9th", "10th"])
        dates.append(date)
    return dates

ผลผลิต (ในฟังก์ชั่น)

def yield_dates():
    for i in range(5):
        date = random.choice(["1st", "2nd", "3rd", "4th", "5th", "6th", "7th", "8th", "9th", "10th"])
        yield date # 'yield' makes a generator automatically which works
                   # in a similar way. This is much more efficient.

ฟังก์ชั่นการโทร

dates_list = return_dates()
print(dates_list)
for i in dates_list:
    print(i)

dates_generator = yield_dates()
print(dates_generator)
for i in dates_generator:
    print(i)

ฟังก์ชั่นทั้งสองทำสิ่งเดียวกัน แต่yieldใช้สามบรรทัดแทนห้าและมีตัวแปรน้อยกว่าหนึ่งที่ต้องกังวล

นี่คือผลลัพธ์จากรหัส:

เอาท์พุต

อย่างที่คุณเห็นทั้งสองฟังก์ชั่นทำในสิ่งเดียวกัน ข้อแตกต่างคือreturn_dates()ให้รายการและyield_dates()ให้กำเนิด

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


43

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



36

ต่อไปนี้เป็นyieldวิธีการที่ใช้ง่ายในการคำนวณชุด fibonacci อธิบาย:

def fib(limit=50):
    a, b = 0, 1
    for i in range(limit):
       yield b
       a, b = b, a+b

เมื่อคุณป้อนสิ่งนี้ลงใน REPL ของคุณแล้วลองโทรหาคุณจะได้รับผลลัพธ์ที่น่าประหลาดใจ:

>>> fib()
<generator object fib at 0x7fa38394e3b8>

นี่เป็นเพราะการมีอยู่ของyieldสัญญาณไปยัง Python ที่คุณต้องการสร้างเครื่องกำเนิดไฟฟ้านั่นคือวัตถุที่สร้างค่าตามต้องการ

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

การใช้next()ฟังก์ชั่นในตัวคุณเรียกใช้.next/ __next__บังคับให้เครื่องกำเนิดไฟฟ้าสร้างมูลค่าโดยตรง:

>>> g = fib()
>>> next(g)
1
>>> next(g)
1
>>> next(g)
2
>>> next(g)
3
>>> next(g)
5

ทางอ้อมถ้าคุณให้fibกับforวงที่มีlistการเริ่มต้นเป็นtupleค่าเริ่มต้นหรือสิ่งอื่น ๆ ที่คาดว่าวัตถุที่สร้าง / ผลิตค่าคุณจะ "กิน" กำเนิดจนไม่มีค่าอื่น ๆ อีกมากมายสามารถผลิตได้โดยมัน (และก็จะส่งกลับ) :

results = []
for i in fib(30):       # consumes fib
    results.append(i) 
# can also be accomplished with
results = list(fib(30)) # consumes fib

ในทำนองเดียวกันด้วยtupleinitializer:

>>> tuple(fib(5))       # consumes fib
(1, 1, 2, 3, 5)

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

เมื่อคุณเรียกครั้งแรกfibด้วยการโทร:

f = fib()

Python รวบรวมฟังก์ชั่นการค้นหาyieldคำหลักและเพียงแค่ส่งคืนวัตถุกำเนิดกลับมาที่คุณ ดูเหมือนว่าไม่ค่อยมีประโยชน์

เมื่อคุณร้องขอมันจะสร้างค่าแรกไม่ว่าโดยตรงหรือโดยอ้อมมันจะประมวลผลคำสั่งทั้งหมดที่พบจนกว่าจะพบ a yieldจากนั้นจะให้ผลตอบแทนตามค่าที่คุณให้ไว้yieldและหยุดชั่วคราว สำหรับตัวอย่างที่แสดงให้เห็นถึงสิ่งนี้ได้ดีกว่าลองใช้การprintเรียกบางอย่าง(แทนที่ด้วยprint "text"ถ้าบน Python 2):

def yielder(value):
    """ This is an infinite generator. Only use next on it """ 
    while 1:
        print("I'm going to generate the value for you")
        print("Then I'll pause for a while")
        yield value
        print("Let's go through it again.")

ตอนนี้ใส่ใน REPL:

>>> gen = yielder("Hello, yield!")

คุณมีตัวสร้างวัตถุรอคำสั่งให้สร้างค่า ใช้nextและดูสิ่งที่ได้รับการพิมพ์:

>>> next(gen) # runs until it finds a yield
I'm going to generate the value for you
Then I'll pause for a while
'Hello, yield!'

ผลลัพธ์ที่ไม่ได้ยกมาเป็นสิ่งที่พิมพ์ออกมา yieldผลที่ยกมาคือสิ่งที่ถูกส่งกลับจาก โทรnextอีกครั้งตอนนี้:

>>> next(gen) # continues from yield and runs again
Let's go through it again.
I'm going to generate the value for you
Then I'll pause for a while
'Hello, yield!'

เครื่องกำเนิดไฟฟ้าจำได้ว่ามันหยุดชั่วคราวyield valueและดำเนินการต่อจากที่นั่น ข้อความถัดไปจะถูกพิมพ์และค้นหาyieldคำสั่งเพื่อหยุดที่จะดำเนินการอีกครั้ง (เนื่องจากการwhileวนซ้ำ)

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