ตัวสร้างหลาม“ ส่ง” จุดประสงค์ของฟังก์ชั่น?


164

ใครสามารถให้ฉันตัวอย่างทำไมฟังก์ชั่น "ส่ง" ที่เกี่ยวข้องกับฟังก์ชั่นเครื่องกำเนิด Python อยู่? ฉันเข้าใจฟังก์ชั่นผลผลิตอย่างเต็มที่ อย่างไรก็ตามฟังก์ชั่นส่งทำให้ฉันสับสน เอกสารเกี่ยวกับวิธีการนี้มีความซับซ้อน:

generator.send(value)

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

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

ใครสามารถให้ตัวอย่างของเครื่องกำเนิดไฟฟ้าที่ใช้การส่งซึ่งทำให้สิ่งที่ได้รับไม่สามารถทำได้?



3
เพิ่มอีกตัวอย่างชีวิตจริง (การอ่านจาก FTP) เมื่อมีการโทรกลับเป็นเครื่องกำเนิดไฟฟ้าที่ใช้จากภายใน
Jan Vlcinsky

2
เป็นมูลค่าการกล่าวขวัญว่า "เมื่อsend()ถูกเรียกให้เริ่มเครื่องกำเนิดไฟฟ้ามันจะต้องถูกเรียกว่าNoneเป็นอาร์กิวเมนต์เนื่องจากไม่มีนิพจน์ผลผลิตที่สามารถรับค่าได้" อ้างจากเอกสารทางการและการอ้างอิงในคำถามคือ หายไป
ริก

คำตอบ:


147

มันใช้เพื่อส่งค่าไปยังเครื่องกำเนิดไฟฟ้าที่เพิ่งให้ผล นี่คือตัวอย่างคำอธิบายที่ประดิษฐ์ขึ้น (ไม่มีประโยชน์):

>>> def double_inputs():
...     while True:
...         x = yield
...         yield x * 2
...
>>> gen = double_inputs()
>>> next(gen)       # run up to the first yield
>>> gen.send(10)    # goes into 'x' variable
20
>>> next(gen)       # run up to the next yield
>>> gen.send(6)     # goes into 'x' again
12
>>> next(gen)       # run up to the next yield
>>> gen.send(94.3)  # goes into 'x' again
188.5999999999999

yieldคุณไม่สามารถทำเช่นนี้เพียงกับ

@defer.inlineCallbacksว่าทำไมมันมีประโยชน์อย่างใดอย่างหนึ่งของกรณีการใช้งานที่ดีที่สุดที่ผมเคยเห็นเป็นบิดของ เป็นหลักจะช่วยให้คุณสามารถเขียนฟังก์ชั่นเช่นนี้:

@defer.inlineCallbacks
def doStuff():
    result = yield takesTwoSeconds()
    nextResult = yield takesTenSeconds(result * 10)
    defer.returnValue(nextResult / 10)

สิ่งที่เกิดขึ้นคือการtakesTwoSeconds()ส่งคืน a Deferredซึ่งเป็นค่าที่สัญญาว่าจะมีการคำนวณมูลค่าในภายหลัง Twisted สามารถรันการคำนวณในเธรดอื่นได้ เมื่อทำการคำนวณเสร็จแล้วมันจะส่งผ่านไปยังค่าที่เลื่อนออกไปและค่าจะถูกส่งกลับไปยังdoStuff()ฟังก์ชัน ดังนั้นจึงdoStuff()สามารถดูมากขึ้นหรือน้อยลงเช่นฟังก์ชั่นขั้นตอนปกติยกเว้นมันสามารถทำทุกประเภทของการคำนวณและการเรียกกลับ ฯลฯ ทางเลือกก่อนที่ฟังก์ชั่นนี้จะทำสิ่งที่ชอบ:

def doStuff():
    returnDeferred = defer.Deferred()
    def gotNextResult(nextResult):
        returnDeferred.callback(nextResult / 10)
    def gotResult(result):
        takesTenSeconds(result * 10).addCallback(gotNextResult)
    takesTwoSeconds().addCallback(gotResult)
    return returnDeferred

มันซับซ้อนและเทอะทะมากขึ้น


2
คุณช่วยอธิบายจุดประสงค์ของสิ่งนี้ได้อย่างไร เหตุใดจึงไม่สามารถสร้างใหม่ได้ด้วย double_inputs (startnumber) และผลตอบแทน
Tommy

@ ทอมมี่: โอ้เพราะค่าที่คุณได้มาไม่มีอะไรเกี่ยวข้องกับค่าก่อนหน้านี้ ขอเปลี่ยนตัวอย่าง
Claudiu

ทำไมคุณถึงใช้สิ่งนี้มากกว่าฟังก์ชั่นพื้นฐานที่เพิ่มอินพุตเป็นสองเท่า
Tommy

4
@ ทอมมี่: คุณจะไม่ ตัวอย่างแรกคือการอธิบายว่ามันทำอะไร ตัวอย่างที่สองสำหรับกรณีการใช้งานที่เป็นประโยชน์จริง ๆ
Claudiu

1
@ ทอมมี่: ฉันจะบอกว่าถ้าคุณอยากรู้ลองดูการนำเสนอนี้และทำงานผ่านมันทั้งหมด คำตอบสั้น ๆ จะไม่พอเพียงเพราะคุณจะพูดว่า "แต่ฉันทำแบบนี้ไม่ได้เหรอ?" ฯลฯ
Claudiu

96

ฟังก์ชั่นนี้คือการเขียน coroutines

def coroutine():
    for i in range(1, 10):
        print("From generator {}".format((yield i)))
c = coroutine()
c.send(None)
try:
    while True:
        print("From user {}".format(c.send(1)))
except StopIteration: pass

พิมพ์

From generator 1
From user 2
From generator 1
From user 3
From generator 1
From user 4
...

ดูว่าการควบคุมกำลังถูกส่งกลับไปกลับมาอย่างไร นั่นคือ coroutines สามารถใช้กับสิ่งดีๆทุกชนิดเช่น asynch IO และที่คล้ายกัน

คิดว่ามันเป็นอย่างนี้กับเครื่องกำเนิดไฟฟ้าและไม่มีการส่งเป็นถนนทางเดียว

==========       yield      ========
Generator |   ------------> | User |
==========                  ========

แต่ด้วยการส่งมันจะกลายเป็นถนนสองทาง

==========       yield       ========
Generator |   ------------>  | User |
==========    <------------  ========
                  send

ซึ่งเปิดประตูให้ผู้ใช้ปรับแต่งพฤติกรรมของเครื่องกำเนิดไฟฟ้าได้ทันทีและเครื่องกำเนิดไฟฟ้าตอบสนองต่อผู้ใช้


3
แต่ฟังก์ชั่นเครื่องกำเนิดไฟฟ้าสามารถรับพารามิเตอร์ได้ "ส่ง" ไปไกลกว่าการส่งพารามิเตอร์ไปยังเครื่องกำเนิดอย่างไร
Tommy

13
@Tommy เพราะคุณไม่สามารถเปลี่ยนพารามิเตอร์เป็นเครื่องกำเนิดไฟฟ้าในขณะที่มันทำงาน คุณให้พารามิเตอร์มันทำงานเสร็จแล้ว ด้วยการส่งคุณให้มันพารามิเตอร์มันทำงานได้นิดหน่อยคุณส่งค่าและมันทำสิ่งที่แตกต่างทำซ้ำ
Daniel Gratzer

2
@Tommy สิ่งนี้จะรีสตาร์ทเครื่องกำเนิดซึ่งจะทำให้คุณทำซ้ำงานจำนวนมาก
Daniel Gratzer

5
คุณช่วยอธิบายวัตถุประสงค์ในการส่ง None ก่อนทุกอย่างได้ไหม
Shubham Aggarwal

2
@ShubhamAggarwal ทำเพื่อ 'เริ่ม' เครื่องกำเนิดไฟฟ้า มันเป็นเพียงสิ่งที่ต้องทำ มันทำให้รู้สึกบางอย่างเมื่อคุณคิดเกี่ยวกับมันตั้งแต่ครั้งแรกที่คุณเรียกsend()เครื่องกำเนิดไฟฟ้าได้ไม่ถึงคำหลักyield
Michael

50

นี่อาจช่วยใครซักคน นี่คือตัวกำเนิดที่ไม่ได้รับผลกระทบจากฟังก์ชั่นส่ง ใช้ในพารามิเตอร์ number ในการเริ่มต้นและไม่ได้รับผลกระทบจากการส่ง:

>>> def double_number(number):
...     while True:
...         number *=2 
...         yield number
... 
>>> c = double_number(4)
>>> c.send(None)
8
>>> c.next()
16
>>> c.next()
32
>>> c.send(8)
64
>>> c.send(8)
128
>>> c.send(8)
256

ตอนนี้นี่คือวิธีที่คุณจะใช้ฟังก์ชันชนิดเดียวกันโดยใช้ send ดังนั้นในแต่ละการวนซ้ำคุณสามารถเปลี่ยนค่าของจำนวน:

def double_number(number):
    while True:
        number *= 2
        number = yield number

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

>>> def double_number(number):
...     while True:
...         number *= 2
...         number = yield number
...
>>> c = double_number(4)
>>> 
>>> c.send(None)
8
>>> c.send(5) #10
10
>>> c.send(1500) #3000
3000
>>> c.send(3) #6
6

นอกจากนี้คุณยังสามารถใส่สิ่งนี้ไว้ใน for for loop ได้เช่น:

for x in range(10):
    n = c.send(n)
    print n

สำหรับความช่วยเหลือเพิ่มเติมลองดูที่นี่ บทแนะนำที่ยอดเยี่ยมนี้


12
การเปรียบเทียบระหว่างฟังก์ชันที่ไม่ได้รับผลกระทบจาก send () กับฟังก์ชันที่ช่วยได้จริงๆ ขอบคุณ!
มนัสบาจาจ

สิ่งนี้เป็นตัวอย่างที่แสดงให้เห็นถึงจุดประสงค์ได้sendอย่างไร ง่าย ๆlambda x: x * 2ก็ทำแบบเดียวกันในทางที่ซับซ้อนน้อยกว่า
user209974

มันใช้ส่งหรือไม่ ไปและเพิ่มคำตอบของคุณ
radtek

17

บางกรณีใช้สำหรับการใช้เครื่องกำเนิดไฟฟ้าและ send()

เครื่องกำเนิดไฟฟ้าที่มีการsend()อนุญาต:

  • จดจำสถานะภายในของการดำเนินการ
    • เราอยู่ที่ขั้นตอนใด
    • สถานะปัจจุบันของข้อมูลของเราคืออะไร
  • กลับลำดับของค่า
  • ได้รับลำดับของอินพุต

นี่คือบางกรณีการใช้งาน:

ความพยายามในการติดตามดูสูตร

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

เราอาจ:

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

    def recipe():
        pot = []
        action = yield pot
        assert action == ("add", "water")
        pot.append(action[1])
    
        action = yield pot
        assert action == ("add", "salt")
        pot.append(action[1])
    
        action = yield pot
        assert action == ("boil", "water")
    
        action = yield pot
        assert action == ("add", "pasta")
        pot.append(action[1])
    
        action = yield pot
        assert action == ("decant", "water")
        pot.remove("water")
    
        action = yield pot
        assert action == ("serve")
        pot = []
        yield pot

หากต้องการใช้งานก่อนอื่นให้สร้างwatched_attemptอินสแตนซ์:

>>> watched_attempt = recipe()                                                                         
>>> watched_attempt.next()                                                                                     
[]                                                                                                     

.next()จำเป็นต้องเรียกใช้เพื่อเริ่มการทำงานของตัวสร้าง

ค่าที่ส่งคืนแสดงให้เห็นว่าหม้อของเราว่างเปล่าในขณะนี้

ตอนนี้ให้ดำเนินการบางอย่างตามสิ่งที่สูตรคาดหวัง:

>>> watched_attempt.send(("add", "water"))                                                                     
['water']                                                                                              
>>> watched_attempt.send(("add", "salt"))                                                                      
['water', 'salt']                                                                                      
>>> watched_attempt.send(("boil", "water"))                                                                    
['water', 'salt']                                                                                      
>>> watched_attempt.send(("add", "pasta"))                                                                     
['water', 'salt', 'pasta']                                                                             
>>> watched_attempt.send(("decant", "water"))                                                                  
['salt', 'pasta']                                                                                      
>>> watched_attempt.send(("serve"))                                                                            
[] 

อย่างที่เราเห็นในที่สุดหม้อก็ว่างเปล่า

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

>>> watched_attempt = running.recipe()                                                                         
>>> watched_attempt.next()                                                                                     
[]                                                                                                     
>>> watched_attempt.send(("add", "water"))                                                                     
['water']                                                                                              
>>> watched_attempt.send(("add", "pasta")) 

---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-21-facdf014fe8e> in <module>()
----> 1 watched_attempt.send(("add", "pasta"))

/home/javl/sandbox/stack/send/running.py in recipe()
     29
     30     action = yield pot
---> 31     assert action == ("add", "salt")
     32     pot.append(action[1])
     33

AssertionError:

ขอให้สังเกตว่า:

  • มีลำดับเชิงเส้นของขั้นตอนที่คาดหวัง
  • ขั้นตอนอาจแตกต่างกัน (บางคนกำลังเอาออกบางคนกำลังเพิ่มลงในหม้อ)
  • เราจัดการเพื่อทำสิ่งนั้นโดยฟังก์ชั่น / ตัวสร้าง - ไม่จำเป็นต้องใช้คลาสที่ซับซ้อนหรือโครงสร้างที่คล้ายกัน

ผลรวมสะสม

เราอาจใช้เครื่องกำเนิดไฟฟ้าเพื่อติดตามการทำงานทั้งหมดของค่าที่ส่งไปยังมัน

เมื่อใดก็ตามที่เราเพิ่มตัวเลขจำนวนอินพุตและผลรวมทั้งหมดจะถูกส่งกลับ (ใช้ได้สำหรับช่วงเวลาที่อินพุตก่อนหน้าถูกส่งเข้ามา)

from collections import namedtuple

RunningTotal = namedtuple("RunningTotal", ["n", "total"])


def runningtotals(n=0, total=0):
    while True:
        delta = yield RunningTotal(n, total)
        if delta:
            n += 1
            total += delta


if __name__ == "__main__":
    nums = [9, 8, None, 3, 4, 2, 1]

    bookeeper = runningtotals()
    print bookeeper.next()
    for num in nums:
        print num, bookeeper.send(num)

ผลลัพธ์จะมีลักษณะดังนี้:

RunningTotal(n=0, total=0)
9 RunningTotal(n=1, total=9)
8 RunningTotal(n=2, total=17)
None RunningTotal(n=2, total=17)
3 RunningTotal(n=3, total=20)
4 RunningTotal(n=4, total=24)
2 RunningTotal(n=5, total=26)
1 RunningTotal(n=6, total=27)

3
ฉันรันตัวอย่างของคุณและใน python 3 ดูเหมือนว่า watched_attempt.next () จะต้องถูกแทนที่ด้วยถัดไป (watched_attempt)
thanos.a

15

send()วิธีการควบคุมสิ่งที่มีค่าทางด้านซ้ายของการแสดงออกของผลผลิตที่ได้จะเป็น

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

มาตรา 6.15 คำสั่งประเมินผล

Python ประเมินค่านิพจน์จากซ้ายไปขวา ขอให้สังเกตว่าในขณะที่การประเมินการมอบหมายด้านขวามือจะถูกประเมินก่อนทางด้านซ้าย

ดังนั้นการแสดงออกa = bทางด้านขวาจะถูกประเมินก่อน

ดังต่อไปนี้แสดงให้เห็นว่าa[p('left')] = p('right')ด้านขวาถูกประเมินก่อน

>>> def p(side):
...     print(side)
...     return 0
... 
>>> a[p('left')] = p('right')
right
left
>>> 
>>> 
>>> [p('left'), p('right')]
left
right
[0, 0]

ผลตอบแทนทำอะไร?, ให้ผลตอบแทน, ระงับการทำงานของฟังก์ชั่นและกลับไปที่ผู้โทร, และกลับสู่การทำงานในสถานที่เดิมที่ถูกทิ้งไว้ก่อนที่จะระงับ

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

yield ให้ค่า 2 ค่าหนึ่งค่าจากขวาไปอีกค่าหนึ่งไปทางซ้าย

คุณควบคุมค่าทางด้านซ้ายมือของการแสดงออกของผลผลิตได้อย่างไร ผ่าน.send()วิธีการ

6.2.9 นิพจน์ผลตอบแทน

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


13

sendวิธีการดำเนินการcoroutines

หากคุณไม่ได้พบกับ Coroutines พวกเขาจะต้องพาดหัวไปรอบ ๆ เพราะพวกเขาเปลี่ยนวิธีการไหลของโปรแกรม คุณสามารถอ่านบทแนะนำที่ดีสำหรับรายละเอียดเพิ่มเติม


6

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

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

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

  1. สมมติว่าgเป็นเครื่องกำเนิดไฟฟ้า g.sendผลักดันค่าไปทางซ้ายจนถึงปลายด้านขวาของท่อ
  2. การดำเนินการภายในบริบทของการgหยุดชั่วคราวทำให้ร่างกายของฟังก์ชันเครื่องกำเนิดสามารถทำงานได้
  3. ค่าที่ถูกผลักโดยg.sendถูกดึงไปทางซ้ายโดยyieldและรับที่ปลายด้านซ้ายของท่อ ในx = yield y, xได้รับมอบหมายให้คุ้มค่าดึง
  4. การประมวลผลดำเนินการต่อภายในร่างกายของฟังก์ชันของตัวสร้างจนกว่าจะถึงบรรทัดถัดไปที่มีyieldอยู่
  5. yieldg.sendดันขวาค่าผ่านทางปลายด้านซ้ายของท่อกลับไปที่ ในx = yield y, yถูกผลักขวาผ่านท่อ
  6. การประมวลผลภายในตัวเครื่องของฟังก์ชั่นเครื่องกำเนิดหยุดชั่วคราวทำให้ขอบเขตด้านนอกสามารถดำเนินการต่อจากจุดที่ค้างไว้
  7. g.send ดำเนินการต่อและดึงค่าและส่งกลับไปยังผู้ใช้
  8. เมื่อg.sendเรียกครั้งถัดไปให้กลับไปที่ขั้นตอนที่ 1

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

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

ถ้าเราต้องการที่จะทำลายการผลัก / ดึง / ท่อนี้ลงไปให้ไกลที่สุดเท่าที่จะทำได้เราจะพบกับ pseudocode ต่อไปนี้ซึ่งขับรถกลับบ้านจริงๆนอกเหนือจากขั้นตอนที่ 1-5 yieldและsendเป็นสองด้านของท่อเหรียญเดียวกัน:

  1. right_end.push(None) # the first half of g.send; sending None is what starts a generator
  2. right_end.pause()
  3. left_end.start()
  4. initial_value = left_end.pull()
  5. if initial_value is not None: raise TypeError("can't send non-None value to a just-started generator")
  6. left_end.do_stuff()
  7. left_end.push(y) # the first half of yield
  8. left_end.pause()
  9. right_end.resume()
  10. value1 = right_end.pull() # the second half of g.send
  11. right_end.do_stuff()
  12. right_end.push(value2) # the first half of g.send (again, but with a different value)
  13. right_end.pause()
  14. left_end.resume()
  15. x = left_end.pull() # the second half of yield
  16. 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:

  1. เราได้ย้ายการใช้ไปสู่ฟังก์ชั่นที่ซ้อนกัน
  2. เราได้สร้างไปป์สองทิศทางที่left_endจะสามารถเข้าถึงได้โดยฟังก์ชั่นที่ซ้อนกันและใครright_endจะถูกส่งคืนและเข้าถึงได้โดยขอบเขตด้านนอก - right_endคือสิ่งที่เรารู้ว่าเป็นวัตถุกำเนิด
  3. ภายในฟังก์ชั่นที่ซ้อนกันสิ่งแรกที่เราทำคือตรวจสอบนั่นleft_end.pull()คือNoneใช้ค่าที่ถูกผลักเข้าไปในกระบวนการ
  4. ภายในฟังก์ชั่นที่ซ้อนกันคำสั่งที่x = yield yได้ถูกแทนที่โดยสองสาย: และleft_end.push(y)x = left_end.pull()
  5. เราได้กำหนด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ถูกยกขึ้น


"อาร์กิวเมนต์ 1 ใน g = f (1) ได้รับการบันทึกตามปกติและกำหนดให้ y ในเนื้อหาของ f แต่ในขณะที่ True ยังไม่ได้เริ่ม" ทำไมจะไม่ล่ะ? ทำไมจะไม่หลามพยายามที่จะเรียกใช้รหัสนี้จนกว่าจะพบเช่นyield?
Josh

@ Josh เคอร์เซอร์จะไม่สูงจนสายแรกที่send; มันจะใช้เวลาหนึ่งเรียกร้องของsend(None)เพื่อเลื่อนเคอร์เซอร์ไปก่อนyieldคำสั่งและเพียงแล้วจะตามมาsendโทรส่งจริง "ของจริง" yieldมูลค่าให้กับ
BallpointBen

ขอบคุณ - มันน่าสนใจดังนั้นล่ามก็รู้ว่าฟังก์ชั่นf จะ ทำงานyieldในบางจุดและรอจนกว่าจะได้รับsendจากผู้โทร? ด้วยฟังก์ชั่นปกติ cal ล่ามก็จะเริ่มดำเนินการfทันทีใช่มั้ย ท้ายที่สุดไม่มีการรวบรวม AOT ของการเรียงลำดับใด ๆ ใน Python คุณแน่ใจหรือไม่ (ไม่ถามสิ่งที่คุณพูดฉันแค่สับสนกับสิ่งที่คุณเขียนที่นี่) ฉันจะอ่านเพิ่มเติมเกี่ยวกับวิธีที่ Python รู้ว่าต้องรอก่อนที่จะเริ่มการทำงานของส่วนที่เหลือได้อย่างไร
Josh

@ Josh ฉันสร้างแบบจำลองทางจิตนี้ขึ้นมาโดยสังเกตว่าตัวกำเนิดของเล่นต่างทำงานอย่างไรโดยไม่ต้องเข้าใจเรื่องภายในของ Python อย่างไรก็ตามความจริงที่ว่าค่าเริ่มต้นsend(None)ให้ค่าที่เหมาะสม (เช่น1) โดยไม่ส่งNoneไปยังเครื่องกำเนิดไฟฟ้าแสดงให้เห็นว่าการเรียกครั้งแรกไปsendเป็นกรณีพิเศษ มันเป็นอินเตอร์เฟสที่ยุ่งยากในการออกแบบ หากคุณให้ค่าแรกsendส่งค่าโดยพลการลำดับของค่าที่ให้ผลและค่าที่ส่งจะถูกปิดโดยหนึ่งเมื่อเทียบกับที่เป็นอยู่ในปัจจุบัน
BallpointBen

ขอบคุณ BallpointBen น่าสนใจมากฉันทิ้งคำถามไว้ที่นี่เพื่อดูว่าทำไมถึงเป็นเช่นนั้น
Josh

2

สิ่งเหล่านี้ทำให้ฉันสับสนเช่นกัน นี่คือตัวอย่างที่ฉันทำเมื่อพยายามตั้งค่าเครื่องกำเนิดไฟฟ้าที่ให้ผลตอบแทนและรับสัญญาณในลำดับการสลับ (ผลผลิต, ยอมรับ, ผลผลิต, ยอมรับ) ...

def echo_sound():

    thing_to_say = '<Sound of wind on cliffs>'
    while True:
        thing_to_say = (yield thing_to_say)
        thing_to_say = '...'.join([thing_to_say]+[thing_to_say[-6:]]*2)
        yield None  # This is the return value of send.

gen = echo_sound()

print 'You are lost in the wilderness, calling for help.'

print '------'
in_message = gen.next()
print 'You hear: "{}"'.format(in_message)
out_message = 'Hello!'
print 'You yell "{}"'.format(out_message)
gen.send(out_message)

print '------'
in_message = gen.next()
print 'You hear: "{}"'.format(in_message)
out_message = 'Is anybody out there?'
print 'You yell "{}"'.format(out_message)
gen.send(out_message)

print '------'
in_message = gen.next()
print 'You hear: "{}"'.format(in_message)
out_message = 'Help!'
print 'You yell "{}"'.format(out_message)
gen.send(out_message)

ผลลัพธ์คือ:

You are lost in the wilderness, calling for help.
------
You hear: "<Sound of wind on cliffs>"
You yell "Hello!"
------
You hear: "Hello!...Hello!...Hello!"
You yell "Is anybody out there?"
------
You hear: "Is anybody out there?...there?...there?"
You yell "Help!"
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.