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 yield
từ 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 yield
từ khóa, trong khi đầu bên phải là trình tạo và có thể truy cập qua send
chứ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 yield
và send
thự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 send
ngượ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 yield
và send
chia 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:
- Giả sử
g
là máy phát điện. g.send
đẩy một giá trị sang trái qua đầu phải của ống.
- Thực thi trong bối cảnh
g
tạm dừng, cho phép cơ thể của chức năng tạo chạy.
- Giá trị được đẩy bởi
g.send
được kéo sang trái yield
và nhận ở đầu bên trái của đường ống. Trong x = yield y
, x
được gán cho giá trị kéo.
- 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 .
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.
- 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.
g.send
nối lại và kéo giá trị và trả lại cho người dùng.
- 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 yield
câ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 yield
tuyê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ư return
từ khóa dịch, yield
có 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ó return
từ khóa.) Khi yield
gặ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à send
chỉ 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 yield
và send
là hai mặt của cùng một ống đồng xu :
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
Biến đổi chính là chúng ta đã chia x = yield y
và value1 = g.send(value2)
mỗi phần thành hai câu: left_end.push(y)
và x = left_end.pull()
; và value1 = right_end.pull()
và right_end.push(value2)
. Có hai trường hợp đặc biệt của yield
từ khóa: x = yield
và yield y
. Đây là đường cú pháp, tương ứng, cho x = yield None
và _ = 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 send
hoạ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 f
desugars đạ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
:
- Chúng tôi đã chuyển việc thực hiện thành một hàm lồng nhau.
- Chúng ta đã tạo một ống hai chiều mà hàm
left_end
sẽ được truy cập bởi hàm lồng nhau và chúng right_end
sẽ được trả về và truy cập bởi phạm vi bên ngoài - right_end
là những gì chúng ta biết là đối tượng trình tạo.
- 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()
là None
, tiêu thụ một giá trị đẩy trong quá trình này.
- 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)
và x = left_end.pull()
.
- Chúng tôi đã xác định
send
hàm cho right_end
, đó là đối tác của hai dòng chúng tôi đã thay thế x = yield y
câ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_end
và 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 yield
câ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, x
khô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.