คำว่า "ผลผลิต" มีความหมายสองประการ: เพื่อผลิตบางสิ่ง (เช่นเพื่อให้ได้ข้าวโพด) และหยุดให้คน / สิ่งอื่นดำเนินต่อไป (เช่นรถยนต์ที่ยอมให้คนเดินเท้า) คำจำกัดความทั้งสองนี้ใช้กับyieldคำหลักของ Python ; สิ่งที่ทำให้ฟังก์ชั่นเครื่องกำเนิดไฟฟ้าพิเศษนั้นไม่เหมือนในฟังก์ชั่นปกติค่าสามารถ "คืน" ให้กับผู้โทรในขณะที่เพียงหยุดชั่วคราวไม่สิ้นสุดฟังก์ชันเครื่องกำเนิดไฟฟ้า
เป็นการง่ายที่สุดที่จะจินตนาการว่าเครื่องกำเนิดเป็นปลายด้านหนึ่งของท่อสองทิศทางที่มีปลาย "ซ้าย" และปลาย "ขวา" ไปป์นี้เป็นสื่อกลางที่ค่าจะถูกส่งระหว่างตัวกำเนิดและตัวฟังก์ชันของตัวกำเนิด ปลายแต่ละด้านของไปป์มีการดำเนินการสองอย่าง: pushซึ่งจะส่งค่าและบล็อกจนกระทั่งปลายอีกด้านหนึ่งของไปป์ดึงค่าและไม่ส่งคืนอะไร และpullซึ่งจะบล็อกจนกระทั่งปลายอีกด้านหนึ่งของไปป์ดันค่าและส่งกลับค่าที่ผลัก ที่รันไทม์การประมวลผลจะตีกลับไปมาระหว่างบริบทที่ด้านใดด้านหนึ่งของไปป์ - แต่ละด้านจะทำงานจนกว่าจะส่งค่าไปยังอีกฝั่งหนึ่ง ณ จุดนั้นหยุดการทำงานอนุญาตให้อีกฝ่ายทำงานและรอให้ค่าใน กลับไปที่จุดที่ด้านอื่น ๆ หยุดและดำเนินการต่อ กล่าวอีกนัยหนึ่งปลายท่อแต่ละท่อวิ่งจากช่วงเวลาที่ได้รับค่าไปยังช่วงเวลาที่มันส่งค่า
ไปป์นั้นสมมาตรตามหน้าที่ แต่ - โดยการประชุมที่ฉันกำหนดในคำตอบนี้ - ปลายด้านซ้ายใช้ได้เฉพาะภายในร่างกายของฟังก์ชันของเครื่องกำเนิดไฟฟ้าและสามารถเข้าถึงได้ผ่านทางyieldคำหลักในขณะที่ปลายด้านขวาเป็นเครื่องกำเนิดไฟฟ้าและสามารถเข้าถึงได้ผ่านทางsendฟังก์ชั่นเครื่องกำเนิดไฟฟ้า ในฐานะที่เป็นอินเทอร์เฟซเอกพจน์ไปยังปลายของท่อyieldและsendทำหน้าที่สอง: พวกเขาทั้งสองผลักและดึงค่าไปยัง / จากปลายของท่อyieldผลักดันไปทางขวาและดึงไปทางซ้ายในขณะที่sendทำตรงกันข้าม x = yield yหน้าที่สองนี้เป็นปมของความสับสนรอบความหมายของงบเช่นนั้น การแตกyieldและย่อsendลงในสองขั้นตอนการผลัก / ดึงชัดเจนจะทำให้ความหมายชัดเจนยิ่งขึ้น:
- สมมติว่า
gเป็นเครื่องกำเนิดไฟฟ้า g.sendผลักดันค่าไปทางซ้ายจนถึงปลายด้านขวาของท่อ
- การดำเนินการภายในบริบทของการ
gหยุดชั่วคราวทำให้ร่างกายของฟังก์ชันเครื่องกำเนิดสามารถทำงานได้
- ค่าที่ถูกผลักโดย
g.sendถูกดึงไปทางซ้ายโดยyieldและรับที่ปลายด้านซ้ายของท่อ ในx = yield y, xได้รับมอบหมายให้คุ้มค่าดึง
- การประมวลผลดำเนินการต่อภายในร่างกายของฟังก์ชันของตัวสร้างจนกว่าจะถึงบรรทัดถัดไปที่มี
yieldอยู่
yieldg.sendดันขวาค่าผ่านทางปลายด้านซ้ายของท่อกลับไปที่ ในx = yield y, yถูกผลักขวาผ่านท่อ
- การประมวลผลภายในตัวเครื่องของฟังก์ชั่นเครื่องกำเนิดหยุดชั่วคราวทำให้ขอบเขตด้านนอกสามารถดำเนินการต่อจากจุดที่ค้างไว้
g.send ดำเนินการต่อและดึงค่าและส่งกลับไปยังผู้ใช้
- เมื่อ
g.sendเรียกครั้งถัดไปให้กลับไปที่ขั้นตอนที่ 1
ในขณะที่วัฏจักรขั้นตอนนี้จะมีจุดเริ่มต้น: เมื่อg.send(None)- ซึ่งเป็นสิ่งที่next(g)สั้นสำหรับ - ถูกเรียกครั้งแรก (มันผิดกฎหมายที่จะผ่านสิ่งอื่นนอกเหนือNoneจากการsendโทรครั้งแรก) และอาจมีจุดสิ้นสุด: เมื่อไม่มีyieldคำสั่งเพิ่มเติมในเนื้อหาของฟังก์ชันของตัวสร้าง
คุณเห็นสิ่งที่ทำให้yieldคำสั่ง (หรือถูกต้องมากขึ้นกำเนิด) พิเศษหรือไม่? ซึ่งแตกต่างจากreturnคำหลักที่เลวทรามต่ำช้าyieldสามารถส่งผ่านค่าไปยังผู้โทรและรับค่าจากผู้โทรทั้งหมดโดยไม่ต้องยกเลิกฟังก์ชั่นที่มันอาศัยอยู่! (แน่นอนถ้าคุณต้องการยกเลิกฟังก์ชั่น - หรือตัวสร้าง - มันมีประโยชน์ที่จะมีreturnคีย์เวิร์ดเช่นกัน) เมื่อyieldพบคำสั่งฟังก์ชันตัวสร้างจะหยุดทำงานชั่วคราวเท่านั้น ปิดเมื่อถูกส่งค่าอื่น และsendเป็นเพียงอินเทอร์เฟซสำหรับสื่อสารกับภายในของฟังก์ชันตัวสร้างจากภายนอก
ถ้าเราต้องการที่จะทำลายการผลัก / ดึง / ท่อนี้ลงไปให้ไกลที่สุดเท่าที่จะทำได้เราจะพบกับ pseudocode ต่อไปนี้ซึ่งขับรถกลับบ้านจริงๆนอกเหนือจากขั้นตอนที่ 1-5 yieldและsendเป็นสองด้านของท่อเหรียญเดียวกัน:
right_end.push(None) # the first half of g.send; sending None is what starts a generator
right_end.pause()
left_end.start()
initial_value = left_end.pull()
if initial_value is not None: raise TypeError("can't send non-None value to a just-started generator")
left_end.do_stuff()
left_end.push(y) # the first half of yield
left_end.pause()
right_end.resume()
value1 = right_end.pull() # the second half of g.send
right_end.do_stuff()
right_end.push(value2) # the first half of g.send (again, but with a different value)
right_end.pause()
left_end.resume()
x = left_end.pull() # the second half of yield
goto 6
การเปลี่ยนแปลงที่สำคัญคือเราได้แยกx = yield yและvalue1 = g.send(value2)แต่ละคนออกเป็นสองประโยค: left_end.push(y)และx = left_end.pull(); และและvalue1 = right_end.pull() right_end.push(value2)มีสองกรณีพิเศษที่มีyieldคำสำคัญ: และx = yield yield yเหล่านี้เป็นน้ำตาลประโยคตามลำดับสำหรับและx = yield None_ = yield y # discarding value
สำหรับรายละเอียดเฉพาะเกี่ยวกับลำดับที่แม่นยำซึ่งค่าจะถูกส่งผ่านไปป์ดูด้านล่าง
สิ่งที่ตามมาคือรูปแบบที่ค่อนข้างยาวของด้านบน ครั้งแรกก็ควรแรกถูกตั้งข้อสังเกตว่าสำหรับเครื่องกำเนิดไฟฟ้าใด ๆg, เป็นสิ่งที่เทียบเท่ากับnext(g) g.send(None)ด้วยสิ่งนี้ในใจเราสามารถมุ่งเน้นเฉพาะวิธีการsendทำงานและพูดคุยเกี่ยวกับการสร้างเครื่องกำเนิดไฟฟ้าด้วยsendเท่านั้น
สมมติว่าเรามี
def f(y): # This is the "generator function" referenced above
while True:
x = yield y
y = x
g = f(1)
g.send(None) # yields 1
g.send(2) # yields 2
ตอนนี้คำจำกัดความของfdesugars คร่าวๆถึงฟังก์ชั่นสามัญ (ไม่ใช่ตัวกำเนิด):
def f(y):
bidirectional_pipe = BidirectionalPipe()
left_end = bidirectional_pipe.left_end
right_end = bidirectional_pipe.right_end
def impl():
initial_value = left_end.pull()
if initial_value is not None:
raise TypeError(
"can't send non-None value to a just-started generator"
)
while True:
left_end.push(y)
x = left_end.pull()
y = x
def send(value):
right_end.push(value)
return right_end.pull()
right_end.send = send
# This isn't real Python; normally, returning exits the function. But
# pretend that it's possible to return a value from a function and then
# continue execution -- this is exactly the problem that generators were
# designed to solve!
return right_end
impl()
ต่อไปนี้ได้เกิดขึ้นในการเปลี่ยนแปลงของf:
- เราได้ย้ายการใช้ไปสู่ฟังก์ชั่นที่ซ้อนกัน
- เราได้สร้างไปป์สองทิศทางที่
left_endจะสามารถเข้าถึงได้โดยฟังก์ชั่นที่ซ้อนกันและใครright_endจะถูกส่งคืนและเข้าถึงได้โดยขอบเขตด้านนอก - right_endคือสิ่งที่เรารู้ว่าเป็นวัตถุกำเนิด
- ภายในฟังก์ชั่นที่ซ้อนกันสิ่งแรกที่เราทำคือตรวจสอบนั่น
left_end.pull()คือNoneใช้ค่าที่ถูกผลักเข้าไปในกระบวนการ
- ภายในฟังก์ชั่นที่ซ้อนกันคำสั่งที่
x = yield yได้ถูกแทนที่โดยสองสาย: และleft_end.push(y)x = left_end.pull()
- เราได้กำหนด
sendฟังก์ชันสำหรับright_endซึ่งเป็นคู่กับสองบรรทัดที่เราแทนที่x = yield yคำสั่งด้วยในขั้นตอนก่อนหน้า
ในโลกแฟนตาซีนี้ที่ฟังก์ชันสามารถดำเนินการต่อหลังจากกลับมาgจะถูกกำหนดright_endและจากนั้นimpl()จะถูกเรียก ดังนั้นในตัวอย่างของเราข้างต้นคือเราจะต้องติดตามการดำเนินการทีละบรรทัดสิ่งที่จะเกิดขึ้นคือคร่าว ๆ ต่อไปนี้:
left_end = bidirectional_pipe.left_end
right_end = bidirectional_pipe.right_end
y = 1 # from g = f(1)
# None pushed by first half of g.send(None)
right_end.push(None)
# The above push blocks, so the outer scope halts and lets `f` run until
# *it* blocks
# Receive the pushed value, None
initial_value = left_end.pull()
if initial_value is not None: # ok, `g` sent None
raise TypeError(
"can't send non-None value to a just-started generator"
)
left_end.push(y)
# The above line blocks, so `f` pauses and g.send picks up where it left off
# y, aka 1, is pulled by right_end and returned by `g.send(None)`
right_end.pull()
# Rinse and repeat
# 2 pushed by first half of g.send(2)
right_end.push(2)
# Once again the above blocks, so g.send (the outer scope) halts and `f` resumes
# Receive the pushed value, 2
x = left_end.pull()
y = x # y == x == 2
left_end.push(y)
# The above line blocks, so `f` pauses and g.send(2) picks up where it left off
# y, aka 2, is pulled by right_end and returned to the outer scope
right_end.pull()
x = left_end.pull()
# blocks until the next call to g.send
แผนที่นี้ตรงกับรหัสปลอม 16 ขั้นตอนด้านบน
มีรายละเอียดอื่น ๆ เช่นการเผยแพร่ข้อผิดพลาดและสิ่งที่เกิดขึ้นเมื่อคุณไปถึงจุดสิ้นสุดของตัวสร้าง (ท่อปิด) แต่สิ่งนี้จะต้องชัดเจนว่าการควบคุมการไหลพื้นฐานทำงานอย่างไรเมื่อsendมีการใช้งาน
การใช้กฎการแยกคู่แบบเดียวกันนี้มาดูสองกรณีพิเศษ:
def f1(x):
while True:
x = yield x
def f2(): # No parameter
while True:
x = yield x
ส่วนใหญ่พวกเขา desugar เช่นเดียวกับfความแตกต่างเพียงอย่างเดียวคือวิธีที่yieldจะเปลี่ยนคำสั่ง:
def f1(x):
# ... set up pipe
def impl():
# ... check that initial sent value is None
while True:
left_end.push(x)
x = left_end.pull()
# ... set up right_end
def f2():
# ... set up pipe
def impl():
# ... check that initial sent value is None
while True:
left_end.push(x)
x = left_end.pull()
# ... set up right_end
ในครั้งแรกค่าที่ส่งไปยังจะf1ถูกผลัก (ให้ผล) เริ่มแรกจากนั้นค่าที่ดึง (ส่ง) ทั้งหมดจะถูกผลัก (ให้ผล) กลับมาทันที ในวินาทีxไม่มีค่า (ยัง) เมื่อมันมาครั้งแรกpushดังนั้นจึงUnboundLocalErrorถูกยกขึ้น