máy phát điện python có thể gửi mục đích chức năng?


163

Ai đó có thể cho tôi một ví dụ về lý do tại sao hàm "gửi" được liên kết với hàm trình tạo Python tồn tại không? Tôi hoàn toàn hiểu chức năng năng suất. Tuy nhiên, chức năng gửi là khó hiểu với tôi. Các tài liệu về phương pháp này là phức tạp:

generator.send(value)

Tiếp tục thực hiện và gửi lại một giá trị vào hàm tạo. Đối số giá trị trở thành kết quả của biểu thức năng suất hiện tại. Phương thức send () trả về giá trị tiếp theo do trình tạo mang lại hoặc tăng StopIteration nếu trình tạo thoát mà không mang lại giá trị khác.

Điều đó nghĩa là gì? Tôi nghĩ giá trị là đầu vào cho chức năng? Cụm từ "Phương thức send () trả về giá trị tiếp theo do trình tạo mang lại" dường như cũng là mục đích chính xác của hàm sản lượng; năng suất trả về giá trị tiếp theo do máy phát ...

Ai đó có thể cho tôi một ví dụ về một máy phát điện sử dụng gửi mà hoàn thành một cái gì đó không thể?



3
Đã thêm một ví dụ thực tế khác (đọc từ FTP) khi các cuộc gọi lại được chuyển thành trình tạo được sử dụng từ bên trong
Jan Vlcinsky

2
Điều đáng nói là "Khi send()được gọi để khởi động trình tạo, nó phải được gọi với Nonetư cách là đối số, bởi vì không có biểu thức năng suất nào có thể nhận giá trị.", Trích dẫn từ tài liệu chính thức và trích dẫn trong câu hỏi là còn thiếu.
Rick

Câu trả lời:


146

Nó được sử dụng để gửi các giá trị vào một trình tạo vừa mang lại. Dưới đây là một ví dụ giải thích nhân tạo (không hữu ích):

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

Bạn không thể làm điều này chỉ với yield.

Về lý do tại sao nó hữu ích, một trong những trường hợp sử dụng tốt nhất tôi từng thấy là Twisted @defer.inlineCallbacks. Về cơ bản, nó cho phép bạn viết một hàm như thế này:

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

Điều gì xảy ra là takesTwoSeconds()trả về a Deferred, là giá trị hứa hẹn một giá trị sẽ được tính sau. Twisted có thể chạy tính toán trong một chủ đề khác. Khi tính toán được thực hiện, nó chuyển nó vào phần hoãn lại và sau đó giá trị sẽ được gửi trở lại doStuff()hàm. Do đó, doStuff()cuối cùng có thể trông giống hoặc ít hơn một chức năng thủ tục thông thường, ngoại trừ nó có thể thực hiện tất cả các loại tính toán và gọi lại, v.v. Sự thay thế trước chức năng này sẽ là làm một việc như:

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

Nó phức tạp hơn nhiều và khó sử dụng.


2
Bạn có thể giải thích mục đích của việc này là gì? Tại sao điều này không thể được tạo lại với double_inputs (startnumber) và năng suất?
Tommy

@ Mẹ: oh vì các giá trị bạn nhận được không liên quan gì đến giá trị trước đó. hãy để tôi thay đổi ví dụ
Claudiu

Tại sao bạn lại sử dụng cái này sau một chức năng đơn giản làm tăng gấp đôi đầu vào của nó ??
Tommy

4
@ Mẹ: Bạn sẽ không. Ví dụ đầu tiên chỉ là để giải thích những gì nó làm. Ví dụ thứ hai là cho một trường hợp sử dụng thực sự hữu ích.
Claudiu

1
@ Mẹ: Tôi sẽ nói nếu bạn thực sự muốn biết hãy xem bài thuyết trình này và thực hiện tất cả. Một câu trả lời ngắn sẽ không đủ vì sau đó bạn sẽ chỉ nói "Nhưng tôi không thể làm như thế này sao?" v.v.
Claudiu

96

Chức năng này là để viết 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

in

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

Xem cách kiểm soát được truyền qua lại? Đó là những xác chết. Chúng có thể được sử dụng cho tất cả các loại điều thú vị như asynch IO và tương tự.

Hãy nghĩ về nó như thế này, với một máy phát điện và không gửi, đó là con đường một chiều

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

Nhưng với gửi, nó trở thành một con đường hai chiều

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

Điều này mở ra cánh cửa cho người dùng tùy chỉnh hành vi của máy phát điện khi đang bay và máy phát đáp ứng với người dùng.


3
nhưng một hàm tạo có thể lấy tham số. Làm thế nào để "Gửi" vượt ra ngoài việc gửi một tham số cho trình tạo?
Tommy

13
@ Mẹ bởi vì bạn không thể thay đổi các tham số thành một trình tạo khi nó chạy. Bạn cung cấp cho nó các tham số, nó chạy, thực hiện. Với gửi, bạn cung cấp cho nó các tham số, nó chạy một chút, bạn gửi cho nó một giá trị và nó làm điều gì đó khác đi, lặp lại
Daniel Gratzer

2
@Tommy Điều này sẽ khởi động lại trình tạo, điều này sẽ khiến bạn phải làm lại rất nhiều công việc
Daniel Gratzer

5
Bạn có thể vui lòng giải thích mục đích của việc gửi Không trước mọi thứ?
Shubham Aggarwal

2
@ShubhamAggarwal Nó được thực hiện để 'khởi động' trình tạo. Nó chỉ là một cái gì đó cần phải được thực hiện. Nó có ý nghĩa khi bạn nghĩ về nó kể từ lần đầu tiên bạn gọi send()trình tạo chưa đạt được từ khóa yield.
Michael

50

Điều này có thể giúp một ai đó. Đây là một trình tạo không bị ảnh hưởng bởi chức năng gửi. Nó nhận tham số số khi khởi tạo và không bị ảnh hưởng bởi gửi:

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

Bây giờ đây là cách bạn sẽ thực hiện cùng loại hàm bằng cách sử dụng gửi, vì vậy trên mỗi lần lặp, bạn có thể thay đổi giá trị của số:

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

Đây là những gì trông giống như, như bạn có thể thấy việc gửi một giá trị mới cho số thay đổi kết quả:

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

Bạn cũng có thể đặt cái này trong một vòng lặp for như vậy:

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

Để được trợ giúp thêm hãy xem hướng dẫn tuyệt vời này .


12
So sánh này giữa một hàm không bị ảnh hưởng bởi send () với hàm thực sự có ích. Cảm ơn!
Manas Bajaj

Làm thế nào đây có thể là một ví dụ minh họa về mục đích của send? Một đơn giản lambda x: x * 2làm điều tương tự theo cách ít phức tạp hơn nhiều.
dùng209974

Nó có sử dụng gửi không? Đi và thêm câu trả lời của bạn.
radtek

17

Một số trường hợp sử dụng để sử dụng máy phát điện và send()

Máy phát điện send()cho phép:

  • ghi nhớ trạng thái nội bộ của việc thực hiện
    • chúng ta đang ở bước nào
    • tình trạng hiện tại của dữ liệu của chúng tôi là gì
  • trả về chuỗi giá trị
  • trình tự nhận đầu vào

Dưới đây là một số trường hợp sử dụng:

Đã xem cố gắng làm theo một công thức

Hãy để chúng tôi có một công thức, dự kiến ​​bộ đầu vào được xác định trước theo một số thứ tự.

Chúng ta có thể:

  • tạo một watched_attemptví dụ từ công thức
  • hãy để nó nhận được một số đầu vào
  • với mỗi thông tin trả về đầu vào về những gì hiện có trong nồi
  • với mỗi kiểm tra đầu vào, đầu vào đó là đầu vào dự kiến ​​(và thất bại nếu không)

    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

Để sử dụng nó, trước tiên hãy tạo watched_attemptví dụ:

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

Cuộc gọi đến .next()là cần thiết để bắt đầu thực hiện trình tạo.

Trả về giá trị cho thấy, nồi của chúng tôi hiện đang trống.

Bây giờ thực hiện một vài hành động theo những gì công thức mong đợi:

>>> 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"))                                                                            
[] 

Như chúng ta thấy, cái nồi cuối cùng trống rỗng.

Trong trường hợp, một người sẽ không tuân theo công thức, nó sẽ thất bại (kết quả mong muốn của việc theo dõi nỗ lực nấu món gì đó - chỉ cần học chúng tôi đã không chú ý đầy đủ khi được hướng dẫn.

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

Thông báo rằng:

  • có chuỗi tuyến tính của các bước dự kiến
  • các bước có thể khác nhau (một số đang loại bỏ, một số đang thêm vào nồi)
  • chúng tôi quản lý để thực hiện tất cả điều đó bằng một hàm / trình tạo - không cần sử dụng lớp phức tạp hoặc các cấu trúc tương tự.

Tổng số chạy

Chúng tôi có thể sử dụng trình tạo để theo dõi tổng số giá trị được gửi tới nó.

Bất cứ khi nào chúng tôi thêm một số, số lượng đầu vào và tổng tiền được trả lại (hợp lệ cho thời điểm đầu vào trước đó được gửi vào nó).

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)

Đầu ra sẽ như sau:

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
Tôi chạy ví dụ của bạn và trong python 3 có vẻ như đã xem thay thế theo dõi_atteem.next () bằng tiếp theo (theo dõi_atteem).
thanos.a

15

Các send()điều khiển phương pháp gì giá trị bên trái của biểu thức năng suất sẽ.

Để hiểu mức năng suất khác nhau và giá trị của nó như thế nào, trước tiên hãy nhanh chóng làm mới mã python theo thứ tự.

Mục 6.15 Đánh giá

Python đánh giá các biểu thức từ trái sang phải. Lưu ý rằng trong khi đánh giá một bài tập, phía bên phải được đánh giá trước phía bên trái.

Vì vậy, một biểu thức a = b phía bên tay phải được đánh giá đầu tiên.

Như sau đây chứng tỏ rằng a[p('left')] = p('right')phía bên tay phải được đánh giá đầu tiên.

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

Năng suất làm gì?, Sản lượng, đình chỉ thực hiện chức năng và trả về cho người gọi và tiếp tục thực hiện tại cùng một nơi mà nó đã dừng trước khi tạm dừng.

Trường hợp chính xác là thực hiện bị đình chỉ? Bạn có thể đã đoán nó rồi ... việc thực thi bị đình chỉ giữa bên phải và bên trái của biểu thức sản lượng. Vì vậy, new_val = yield old_valviệc thực thi được tạm dừng tại =dấu hiệu và giá trị bên phải (trước khi tạm dừng và cũng là giá trị được trả về cho người gọi) có thể là một giá trị khác sau đó giá trị bên trái (là giá trị được gán sau khi tiếp tục chấp hành).

yield mang lại 2 giá trị, một bên phải và một giá trị bên trái.

Làm thế nào để bạn kiểm soát giá trị ở phía bên trái của biểu thức năng suất? thông qua .send()phương pháp.

6.2.9. Biểu thức năng suất

Giá trị của biểu thức năng suất sau khi nối lại phụ thuộc vào phương thức tiếp tục thực hiện. Nếu __next__()được sử dụng (thường thông qua a cho hoặc next()dựng sẵn) thì kết quả là Không có. Mặt khác, nếu send()được sử dụng, thì kết quả sẽ là giá trị được truyền vào phương thức đó.


13

Các sendphương pháp cụ coroutines .

Nếu bạn chưa gặp Coroutines, họ rất khó khăn để quấn đầu bạn vì họ thay đổi cách chương trình chảy. Bạn có thể đọc một hướng dẫn tốt để biết thêm chi tiết.


6

Từ "nhường" có hai nghĩa: tạo ra một thứ gì đó (ví dụ, để sản xuất ngô) và tạm dừng để cho ai đó / điều khác tiếp tục (ví dụ: ô tô nhường đường cho người đi bộ). Cả hai định nghĩa đều áp dụng cho yieldtừ khóa của Python ; Điều làm cho các chức năng của trình tạo trở nên đặc biệt là không giống như trong các hàm thông thường, các giá trị có thể được "trả lại" cho người gọi trong khi chỉ tạm dừng, không kết thúc, một hàm tạo.

Dễ nhất là tưởng tượng một máy phát là một đầu của ống hai chiều có đầu "bên trái" và đầu "bên phải"; đường ống này là phương tiện mà các giá trị được gửi giữa chính bộ tạo và thân của hàm tạo. Mỗi đầu của ống có hai thao tácpush gửi một giá trị và các khối cho đến khi đầu kia của ống kéo giá trị đó và không trả về gì; vàpull, chặn cho đến khi đầu kia của ống đẩy một giá trị và trả về giá trị được đẩy. Trong thời gian chạy, thực thi nảy qua lại giữa các bối cảnh ở hai bên của đường ống - mỗi bên chạy cho đến khi nó gửi một giá trị sang phía bên kia, tại đó nó dừng lại, cho phép phía bên kia chạy và chờ một giá trị trong trở lại, tại thời điểm đó, phía bên kia dừng lại và nó tiếp tục. Nói cách khác, mỗi đầu của ống chạy từ thời điểm nó nhận được một giá trị đến thời điểm nó gửi một giá trị.

Đường ống đối xứng về mặt chức năng, nhưng - theo quy ước tôi xác định trong câu trả lời này - đầu bên trái chỉ có sẵn bên trong thân hàm của trình tạo và có thể truy cập thông qua yieldtừ khóa, trong khi đầu bên phải trình tạo và có thể truy cập qua sendchức năng của máy phát điện . Là các giao diện đơn lẻ với các đầu tương ứng của ống yieldsendthực hiện nhiệm vụ kép: cả hai đều đẩy và kéo các giá trị đến / từ đầu ống của chúng, yieldđẩy sang phải và kéo sang trái trong khi sendngược lại. Nhiệm vụ kép này là mấu chốt của sự nhầm lẫn xung quanh ngữ nghĩa của các tuyên bố như thế nào x = yield y. Chia yieldsendchia thành hai bước đẩy / kéo rõ ràng sẽ làm cho ngữ nghĩa của chúng rõ ràng hơn nhiều:

  1. Giả sử glà máy phát điện. g.sendđẩy một giá trị sang trái qua đầu phải của ống.
  2. Thực thi trong bối cảnh gtạm dừng, cho phép cơ thể của chức năng tạo chạy.
  3. Giá trị được đẩy bởi g.sendđược kéo sang trái yieldvà nhận ở đầu bên trái của đường ống. Trong x = yield y, xđược gán cho giá trị kéo.
  4. Việc thực thi tiếp tục trong cơ thể của hàm tạo cho đến khi yieldđạt được dòng tiếp theo .
  5. yieldđẩy một giá trị sang phải qua đầu bên trái của ống, sao lưu lên g.send. Trong x = yield y, yđược đẩy thẳng qua đường ống.
  6. Việc thực thi trong cơ thể của hàm tạo sẽ tạm dừng, cho phép phạm vi bên ngoài tiếp tục ở nơi nó dừng lại.
  7. g.send nối lại và kéo giá trị và trả lại cho người dùng.
  8. Khi g.sendđược gọi tiếp theo, quay lại Bước 1.

Trong khi theo chu kỳ, quy trình này có một sự khởi đầu: khi nào g.send(None)- đó là từ next(g)viết tắt của - được gọi đầu tiên (việc chuyển một cái gì đó khác với cuộc gọi Noneđầu tiên là bất hợp pháp send). Và nó có thể kết thúc: khi không còn yieldcâu lệnh nào đạt được trong cơ thể của hàm tạo.

Bạn có thấy điều gì làm cho yieldtuyên bố (hay chính xác hơn là máy phát điện) trở nên đặc biệt không? Không giống như returntừ khóa dịch, yieldcó thể truyền các giá trị cho người gọi và nhận tất cả các giá trị từ người gọi mà không cần kết thúc chức năng mà nó sống! (Tất nhiên, nếu bạn muốn chấm dứt một chức năng - hoặc một trình tạo - cũng rất hữu ích khi có returntừ khóa.) Khi yieldgặp một câu lệnh, hàm tạo chỉ dừng lại, sau đó chọn sao lưu bên phải ở bên trái tắt khi được gửi một giá trị khác. Và sendchỉ là giao diện để giao tiếp với bên trong của hàm tạo từ bên ngoài nó.

Nếu chúng ta thực sự muốn phá vỡ sự tương tự đẩy / kéo / ống này hết mức có thể, chúng ta sẽ kết thúc với mã giả sau đây thực sự lái xe về nhà, ngoài các bước 1-5 yieldsendlà hai mặt của cùng một ống đồng xu :

  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

Biến đổi chính là chúng ta đã chia x = yield yvalue1 = g.send(value2)mỗi phần thành hai câu: left_end.push(y)x = left_end.pull(); và value1 = right_end.pull()right_end.push(value2). Có hai trường hợp đặc biệt của yieldtừ khóa: x = yieldyield y. Đây là đường cú pháp, tương ứng, cho x = yield None_ = yield y # discarding value.

Để biết chi tiết cụ thể về thứ tự chính xác trong đó các giá trị được gửi qua đường ống, xem bên dưới.


Những gì sau đây là một mô hình cụ thể khá dài của ở trên. Đầu tiên, cần lưu ý rằng đối với bất kỳ máy phát điện nào g, next(g)chính xác là tương đương với g.send(None). Với suy nghĩ này, chúng ta chỉ có thể tập trung vào cách thức sendhoạt động và chỉ nói về việc thúc đẩy máy phát điện send.

Giả sử chúng ta có

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

Bây giờ, định nghĩa của fdesugars đại khái cho hàm thông thường (không tạo) sau đây:

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

Sau đây đã xảy ra trong sự chuyển đổi này của f:

  1. Chúng tôi đã chuyển việc thực hiện thành một hàm lồng nhau.
  2. Chúng ta đã tạo một ống hai chiều mà hàm left_endsẽ được truy cập bởi hàm lồng nhau và chúng right_endsẽ được trả về và truy cập bởi phạm vi bên ngoài - right_endlà những gì chúng ta biết là đối tượng trình tạo.
  3. Trong hàm lồng nhau, điều đầu tiên chúng tôi làm là kiểm tra mà left_end.pull()None, tiêu thụ một giá trị đẩy trong quá trình này.
  4. Trong hàm lồng nhau, câu lệnh x = yield yđã được thay thế bằng hai dòng: left_end.push(y)x = left_end.pull().
  5. Chúng tôi đã xác định sendhàm cho right_end, đó là đối tác của hai dòng chúng tôi đã thay thế x = yield ycâu lệnh trong bước trước.

Trong thế giới giả tưởng này, nơi các chức năng có thể tiếp tục sau khi trở về, gđược gán right_endvà sau đó impl()được gọi. Vì vậy, trong ví dụ của chúng tôi ở trên, chúng tôi đã theo dõi từng dòng thực thi, những gì sẽ xảy ra đại khái như sau:

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

Bản đồ này chính xác đến mã giả 16 bước ở trên.

Có một số chi tiết khác, như cách truyền lỗi và điều gì xảy ra khi bạn đến cuối máy phát (đường ống đã đóng), nhưng điều này sẽ làm rõ cách thức hoạt động của luồng điều khiển cơ bản khi sendđược sử dụng.

Sử dụng các quy tắc giải mã tương tự này, chúng ta hãy xem xét hai trường hợp đặc biệt:

def f1(x):
    while True:
        x = yield x

def f2():  # No parameter
    while True:
        x = yield x

Đối với hầu hết các phần họ giải thích theo cùng một cách f, sự khác biệt duy nhất là cách các yieldcâu lệnh được chuyển đổi:

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

Đầu tiên, giá trị được truyền vào f1được đẩy (mang lại) ban đầu, và sau đó tất cả các giá trị được kéo (gửi) được đẩy trở lại (mang lại) ngay. Trong lần thứ hai, xkhông có giá trị (chưa) khi nó đến lần đầu tiên push, vì vậy an UnboundLocalErrorđược nâng lên.


"Đối số 1 trong g = f (1) đã được nắm bắt bình thường và được gán cho y trong cơ thể của f, nhưng trong khi True chưa bắt đầu." Tại sao không? Tại sao Python không thử chạy mã này cho đến khi nó gặp phải, vd yield?
Josh

@Josh Con trỏ không được nâng cao cho đến lần gọi đầu tiên send; phải mất một cuộc gọi send(None)để di chuyển con trỏ đến yieldcâu lệnh đầu tiên và chỉ sau đó các sendcuộc gọi tiếp theo mới thực sự gửi giá trị "thực" đến yield.
BallpointBen

Cảm ơn - Điều đó thật thú vị, vì vậy người phiên dịch biết rằng chức năng f này sẽ yield đến một lúc nào đó, và do đó đợi cho đến khi nó nhận được sendtừ người gọi? Với một chức năng bình thường, trình thông dịch sẽ bắt đầu thực thi fngay lập tức, phải không? Rốt cuộc, không có phần tổng hợp AOT nào trong Python. Bạn có chắc là như vậy không? (không hỏi bạn đang nói gì, tôi thực sự chỉ bối rối với những gì bạn viết ở đây). Tôi có thể đọc thêm về cách Python biết rằng nó cần phải đợi trước khi bắt đầu thực thi phần còn lại của hàm?
Josh

@Josh Tôi đã xây dựng mô hình tinh thần này chỉ bằng cách quan sát cách các máy phát đồ chơi khác nhau hoạt động, mà không có bất kỳ hiểu biết nào về nội bộ của Python. Tuy nhiên, thực tế là ban đầu send(None)mang lại giá trị thích hợp (ví dụ 1:) mà không gửi Nonevào trình tạo cho thấy rằng cuộc gọi đầu tiên đến sendlà một trường hợp đặc biệt. Đó là một giao diện phức tạp để thiết kế; nếu bạn để người đầu tiên sendgửi một giá trị tùy ý, thì thứ tự của các giá trị được mang lại và các giá trị được gửi sẽ bị tắt đi so với giá trị hiện tại.
BallpointBen

Cảm ơn BallpointBen. Rất thú vị, tôi đã để lại một câu hỏi ở đây để xem tại sao lại như vậy.
Josh

2

Những điều này làm tôi bối rối quá. Dưới đây là một ví dụ tôi đã thực hiện khi cố gắng thiết lập một trình tạo mang lại và chấp nhận tín hiệu theo thứ tự xen kẽ (nhường, chấp nhận, nhường, chấp nhận) ...

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)

Đầu ra là:

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!"
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.