Trong thực tế, những cách sử dụng chính cho năng suất mới của cú pháp từ cú pháp trong 3.3.3 là gì?


407

Tôi đang có một thời gian khó khăn trong bộ não của tôi xung quanh PEP 380 .

  1. Các tình huống mà "sản lượng từ" là hữu ích là gì?
  2. Trường hợp sử dụng cổ điển là gì?
  3. Tại sao nó được so sánh với các chủ đề vi mô?

[cập nhật]

Bây giờ tôi hiểu nguyên nhân của những khó khăn của tôi. Tôi đã sử dụng máy phát điện, nhưng chưa bao giờ thực sự sử dụng coroutines (được giới thiệu bởi PEP-342 ). Mặc dù có một số điểm tương đồng, máy phát điện và coroutines về cơ bản là hai khái niệm khác nhau. Hiểu về coroutines (không chỉ máy phát điện) là chìa khóa để hiểu cú pháp mới.

IMHO coroutines là tính năng Python tối nghĩa nhất , hầu hết các cuốn sách làm cho nó trông vô dụng và không thú vị.

Cảm ơn câu trả lời tuyệt vời, nhưng cảm ơn đặc biệt đến agf và bình luận của anh ấy liên kết với bài thuyết trình của David Beazley . David đá.



7
Video giới thiệu dabeaz.com/coroutines của David Beazley : youtube.com/watch?v=Z_OAlIhXziw
jcugat

Câu trả lời:


571

Trước tiên hãy lấy một thứ ra khỏi đường đi. Lời giải thích yield from gtương đương với việc for v in g: yield v thậm chí không bắt đầu thực hiện công lý cho yield fromtất cả những gì về. Bởi vì, hãy đối mặt với nó, nếu tất cả đều yield frommở rộng forvòng lặp, thì nó không đảm bảo thêm yield fromvào ngôn ngữ và ngăn chặn toàn bộ các tính năng mới được triển khai trong Python 2.x.

Có gì yield fromkhông là nó thiết lập một kết nối hai chiều trong suốt giữa người gọi và sub-máy phát điện :

  • Kết nối là "trong suốt" theo nghĩa là nó cũng sẽ truyền bá mọi thứ một cách chính xác, không chỉ các yếu tố được tạo ra (ví dụ: ngoại lệ được lan truyền).

  • Kết nối là "hai chiều" theo nghĩa là dữ liệu có thể được gửi từđến một máy phát.

( Nếu chúng ta đang nói về TCP, yield from gcó thể có nghĩa là "bây giờ tạm thời ngắt kết nối ổ cắm của máy khách của tôi và kết nối lại với ổ cắm máy chủ khác này". )

BTW, nếu bạn không chắc chắn việc gửi dữ liệu đến máy phát thậm chí có nghĩa là gì, bạn cần bỏ mọi thứ và đọc về coroutines trước khi chúng rất hữu ích (tương phản chúng với chương trình con ), nhưng không may là ít được biết đến trong Python. Khóa học tò mò của Dave Beazley về Coroutines là một khởi đầu tuyệt vời. Đọc các slide 24-33 để có một đoạn mồi nhanh.

Đọc dữ liệu từ máy phát điện sử dụng năng suất từ

def reader():
    """A generator that fakes a read from a file, socket, etc."""
    for i in range(4):
        yield '<< %s' % i

def reader_wrapper(g):
    # Manually iterate over data produced by reader
    for v in g:
        yield v

wrap = reader_wrapper(reader())
for i in wrap:
    print(i)

# Result
<< 0
<< 1
<< 2
<< 3

Thay vì lặp lại bằng tay reader(), chúng ta có thể chỉ cần yield fromnó.

def reader_wrapper(g):
    yield from g

Điều đó hoạt động, và chúng tôi đã loại bỏ một dòng mã. Và có lẽ ý định rõ ràng hơn một chút (hoặc không). Nhưng không có gì thay đổi cuộc sống.

Gửi dữ liệu đến máy phát điện (coroutine) bằng cách sử dụng năng suất từ ​​- Phần 1

Bây giờ hãy làm điều gì đó thú vị hơn. Hãy tạo một coroutine được gọi là writerchấp nhận dữ liệu được gửi đến nó và ghi vào ổ cắm, fd, v.v.

def writer():
    """A coroutine that writes data *sent* to it to fd, socket, etc."""
    while True:
        w = (yield)
        print('>> ', w)

Bây giờ câu hỏi là, hàm bao bọc xử lý việc gửi dữ liệu tới người viết như thế nào, sao cho bất kỳ dữ liệu nào được gửi đến trình bao bọc được gửi trong suốt đến writer()?

def writer_wrapper(coro):
    # TBD
    pass

w = writer()
wrap = writer_wrapper(w)
wrap.send(None)  # "prime" the coroutine
for i in range(4):
    wrap.send(i)

# Expected result
>>  0
>>  1
>>  2
>>  3

Trình bao bọc cần chấp nhận dữ liệu được gửi đến nó (rõ ràng) và cũng nên xử lý StopIterationkhi vòng lặp for hết. Rõ ràng chỉ làm for x in coro: yield xsẽ không làm. Đây là một phiên bản hoạt động.

def writer_wrapper(coro):
    coro.send(None)  # prime the coro
    while True:
        try:
            x = (yield)  # Capture the value that's sent
            coro.send(x)  # and pass it to the writer
        except StopIteration:
            pass

Hoặc, chúng ta có thể làm điều này.

def writer_wrapper(coro):
    yield from coro

Điều đó tiết kiệm 6 dòng mã, làm cho nó dễ đọc hơn nhiều và nó chỉ hoạt động. Ma thuật!

Gửi dữ liệu đến năng suất của trình tạo từ - Phần 2 - Xử lý ngoại lệ

Hãy làm cho nó phức tạp hơn. Nếu nhà văn của chúng ta cần xử lý các trường hợp ngoại lệ thì sao? Giả sử writertay cầm a SpamExceptionvà nó in ***nếu gặp phải.

class SpamException(Exception):
    pass

def writer():
    while True:
        try:
            w = (yield)
        except SpamException:
            print('***')
        else:
            print('>> ', w)

Nếu chúng ta không thay đổi writer_wrapperthì sao? Nó có hoạt động không? Hãy thử

# writer_wrapper same as above

w = writer()
wrap = writer_wrapper(w)
wrap.send(None)  # "prime" the coroutine
for i in [0, 1, 2, 'spam', 4]:
    if i == 'spam':
        wrap.throw(SpamException)
    else:
        wrap.send(i)

# Expected Result
>>  0
>>  1
>>  2
***
>>  4

# Actual Result
>>  0
>>  1
>>  2
Traceback (most recent call last):
  ... redacted ...
  File ... in writer_wrapper
    x = (yield)
__main__.SpamException

Ừm, nó không hoạt động vì x = (yield)chỉ làm tăng ngoại lệ và mọi thứ đều dừng lại. Hãy làm cho nó hoạt động, nhưng xử lý thủ công các ngoại lệ và gửi chúng hoặc ném chúng vào trình tạo phụ ( writer)

def writer_wrapper(coro):
    """Works. Manually catches exceptions and throws them"""
    coro.send(None)  # prime the coro
    while True:
        try:
            try:
                x = (yield)
            except Exception as e:   # This catches the SpamException
                coro.throw(e)
            else:
                coro.send(x)
        except StopIteration:
            pass

Những công việc này.

# Result
>>  0
>>  1
>>  2
***
>>  4

Nhưng điều này cũng vậy!

def writer_wrapper(coro):
    yield from coro

Các yield fromxử lý minh bạch gửi các giá trị hoặc ném các giá trị vào các tiểu phát điện.

Điều này vẫn không bao gồm tất cả các trường hợp góc mặc dù. Điều gì xảy ra nếu máy phát bên ngoài bị đóng? Còn trường hợp khi trình tạo phụ trả về một giá trị (có, trong Python 3.3+, các trình tạo có thể trả về giá trị), thì giá trị trả về nên được lan truyền như thế nào? Điều đó yield fromminh bạch xử lý tất cả các trường hợp góc là thực sự ấn tượng . yield fromchỉ làm việc kỳ diệu và xử lý tất cả những trường hợp.

Cá nhân tôi cảm thấy yield fromlà một lựa chọn từ khóa kém bởi vì nó không làm cho bản chất hai chiều rõ ràng. Có những từ khóa khác được đề xuất (thích delegatenhưng bị từ chối vì việc thêm một từ khóa mới vào ngôn ngữ khó hơn nhiều so với việc kết hợp các từ khóa hiện có.

Nói tóm lại, nó là tốt nhất để nghĩ về yield fromnhư một transparent two way channelgiữa người gọi và sub-máy phát điện.

Người giới thiệu:

  1. PEP 380 - Cú pháp ủy quyền cho trình tạo phụ (Ewing) [v3.3, 2009/02/13]
  2. PEP 342 - Coroutines thông qua Máy phát điện nâng cao (GvR, Eby) [v2,5, 2005-05-10]

3
@PraveenGollakota, trong phần thứ hai của câu hỏi của bạn, Gửi dữ liệu đến một trình tạo (coroutine) bằng cách sử dụng năng suất từ ​​- Phần 1 , nếu bạn có nhiều hơn coroutines để chuyển tiếp vật phẩm nhận được thì sao? Giống như một kịch bản phát sóng hoặc thuê bao trong đó bạn cung cấp nhiều coroutines cho trình bao bọc trong ví dụ của bạn và các mục nên được gửi đến tất cả hoặc tập hợp con của chúng?
Kevin Ghaboosi

3
@PraveenGollakota, Kudos cho câu trả lời tuyệt vời. Các ví dụ nhỏ cho phép tôi thử mọi thứ thay thế. Liên kết đến khóa học Dave Beazley là một phần thưởng!
BiGYaN

1
thực hiện except StopIteration: passbên trong while True:vòng lặp không phải là một đại diện chính xác của yield from coro- đó không phải là một vòng lặp vô hạn và sau đó corolà hết (nghĩa là tăng StopIteration), writer_wrappersẽ thực hiện câu lệnh tiếp theo. Sau khi báo cáo kết quả cuối cùng nó sẽ tự auto-tăng StopIterationnhư bất kỳ máy phát điện kiệt sức ...
Aprillion

1
... Vì vậy, nếu được writerchứa for _ in range(4)thay vì while True, sau khi in, >> 3nó cũng sẽ tự động nâng lên StopIterationvà điều này sẽ được tự động xử lý yield fromvà sau đó writer_wrappersẽ tự động nâng nó lên StopIterationvà vì wrap.send(i)nó không nằm trong trykhối, nên nó sẽ thực sự được nâng lên vào thời điểm này ( tức là truy nguyên sẽ chỉ báo cáo dòng với wrap.send(i), không phải bất cứ thứ gì từ bên trong máy phát điện)
Aprilillion

3
Khi đọc " thậm chí không bắt đầu thực hiện công lý ", tôi biết rằng tôi đã đi đến câu trả lời đúng. Cảm ơn bạn đã giải thích tuyệt vời!
Hot.PxL

89

Các tình huống mà "sản lượng từ" là hữu ích là gì?

Mọi tình huống mà bạn có một vòng lặp như thế này:

for x in subgenerator:
  yield x

Như PEP mô tả, đây là một nỗ lực khá ngây thơ trong việc sử dụng trình phát phụ, nó thiếu một số khía cạnh, đặc biệt là việc xử lý đúng các .throw()/ .send()/ .close()cơ chế được giới thiệu bởi PEP 342 . Để làm điều này đúng, mã khá phức tạp là cần thiết.

Trường hợp sử dụng cổ điển là gì?

Xem xét rằng bạn muốn trích xuất thông tin từ cấu trúc dữ liệu đệ quy. Giả sử chúng ta muốn lấy tất cả các nút lá trong cây:

def traverse_tree(node):
  if not node.children:
    yield node
  for child in node.children:
    yield from traverse_tree(child)

Điều quan trọng hơn nữa là thực tế là cho đến khi yield from, không có phương pháp đơn giản nào để tái cấu trúc mã trình tạo. Giả sử bạn có một trình tạo (vô tri) như thế này:

def get_list_values(lst):
  for item in lst:
    yield int(item)
  for item in lst:
    yield str(item)
  for item in lst:
    yield float(item)

Bây giờ bạn quyết định đưa các vòng lặp này vào các máy phát riêng biệt. Không yield from, điều này thật xấu xí, đến mức bạn sẽ suy nghĩ kỹ xem bạn có thực sự muốn làm điều đó không. Với yield from, thật tuyệt khi nhìn vào:

def get_list_values(lst):
  for sub in [get_list_values_as_int, 
              get_list_values_as_str, 
              get_list_values_as_float]:
    yield from sub(lst)

Tại sao nó được so sánh với các chủ đề vi mô?

Tôi nghĩ điều mà phần này trong PEP đang nói đến là mọi trình tạo đều có bối cảnh thực thi riêng biệt. Cùng với thực tế là việc thực thi được chuyển đổi giữa trình tạo-trình lặp và trình gọi bằng cách sử dụng yield__next__()tương ứng, điều này tương tự như các luồng, trong đó hệ điều hành chuyển đổi luồng thực thi theo thời gian, cùng với bối cảnh thực hiện (ngăn xếp, thanh ghi, ...).

Hiệu quả của việc này cũng tương đương nhau: Cả trình tạo-trình lặp và trình gọi của tiến trình trong trạng thái thực thi của chúng cùng một lúc, các thực thi của chúng được xen kẽ. Ví dụ: nếu trình tạo thực hiện một số loại tính toán và người gọi sẽ in ra kết quả, bạn sẽ thấy kết quả ngay khi chúng có sẵn. Đây là một hình thức đồng thời.

Sự tương tự đó không phải là bất cứ điều gì cụ thể yield from, mặc dù - đó là một thuộc tính chung của các trình tạo trong Python.


Tái cấu trúc máy phát điện là đau ngày hôm nay.
Josh Lee

1
Tôi có xu hướng sử dụng itertools rất nhiều để tái cấu trúc các trình tạo (thứ như itertools.chain), đó không phải là vấn đề lớn. Tôi thích năng suất từ, nhưng tôi vẫn không thấy nó mang tính cách mạng như thế nào. Có lẽ là vậy, vì Guido hoàn toàn điên rồ về điều đó, nhưng tôi phải bỏ lỡ bức tranh lớn. Tôi đoán thật tuyệt khi gửi () vì điều này rất khó để cấu trúc lại, nhưng tôi không sử dụng nó khá thường xuyên.
e-satis

Tôi cho rằng đó get_list_values_as_xxxlà những máy phát đơn giản với một dòng duy nhất for x in input_param: yield int(x)và hai dòng còn lại tương ứng với strfloat
madtyn

@NiklasB. lại "trích xuất thông tin từ cấu trúc dữ liệu đệ quy." Tôi chỉ nhận được vào Py cho dữ liệu. Bạn có thể đâm vào Q này không?
alancalvitti

33

Bất cứ nơi nào bạn gọi một trình tạo từ bên trong một trình tạo, bạn cần một "máy bơm" để xác định lại yieldcác giá trị : for v in inner_generator: yield v. Như PEP chỉ ra rằng có những phức tạp tinh vi đến mức mà hầu hết mọi người bỏ qua. Điều khiển luồng không cục bộ như throw()là một ví dụ được đưa ra trong PEP. Cú pháp mới yield from inner_generatorđược sử dụng bất cứ nơi nào bạn đã viết forvòng lặp rõ ràng trước đó. Tuy nhiên, đây không chỉ là cú pháp cú pháp: Nó xử lý tất cả các trường hợp góc bị forvòng lặp bỏ qua . Trở thành "đường" khuyến khích mọi người sử dụng nó và do đó có được những hành vi đúng đắn.

Thông điệp này trong chuỗi thảo luận nói về những phức tạp này:

Với các tính năng trình tạo bổ sung được giới thiệu bởi PEP 342, đó không còn là trường hợp nữa: như được mô tả trong PEP của Greg, phép lặp đơn giản không hỗ trợ send () và throw () chính xác. Thể dục dụng cụ cần thiết để hỗ trợ send () và throw () thực sự không phức tạp khi bạn phá vỡ chúng, nhưng chúng cũng không tầm thường.

Tôi không thể nói đến một so sánh với các chủ đề vi mô, ngoài việc quan sát rằng các máy phát điện là một loại paralellism. Bạn có thể coi trình tạo treo là một luồng gửi các giá trị thông qua yieldmột luồng tiêu dùng. Việc triển khai thực tế có thể không giống như thế này (và việc triển khai thực tế rõ ràng là rất đáng quan tâm đối với các nhà phát triển Python) nhưng điều này không liên quan đến người dùng.

yield fromCú pháp mới không thêm bất kỳ khả năng bổ sung nào cho ngôn ngữ về luồng, nó chỉ giúp sử dụng chính xác các tính năng hiện có dễ dàng hơn. Hay chính xác hơn, nó giúp người tiêu dùng mới làm quen với một máy phát bên trong phức tạp được viết bởi một chuyên gia để đi qua máy phát điện đó mà không phá vỡ bất kỳ tính năng phức tạp nào của nó.


23

Một ví dụ ngắn sẽ giúp bạn hiểu một trong yield fromcác trường hợp sử dụng: nhận giá trị từ một trình tạo khác

def flatten(sequence):
    """flatten a multi level list or something
    >>> list(flatten([1, [2], 3]))
    [1, 2, 3]
    >>> list(flatten([1, [2], [3, [4]]]))
    [1, 2, 3, 4]
    """
    for element in sequence:
        if hasattr(element, '__iter__'):
            yield from flatten(element)
        else:
            yield element

print(list(flatten([1, [2], [3, [4]]])))

2
Chỉ muốn đề xuất rằng bản in ở cuối sẽ trông đẹp hơn một chút mà không cần chuyển đổi thành danh sách -print(*flatten([1, [2], [3, [4]]]))
yoniLavi

6

yield from về cơ bản chuỗi lặp theo cách hiệu quả:

# chain from itertools:
def chain(*iters):
    for it in iters:
        for item in it:
            yield item

# with the new keyword
def chain(*iters):
    for it in iters:
        yield from it

Như bạn có thể thấy nó loại bỏ một vòng lặp Python thuần túy. Đó là khá nhiều tất cả những gì nó làm, nhưng chuỗi lặp là một mô hình khá phổ biến trong Python.

Chủ đề về cơ bản là một tính năng cho phép bạn nhảy ra khỏi các chức năng tại các điểm hoàn toàn ngẫu nhiên và quay trở lại trạng thái của một chức năng khác. Người giám sát luồng thực hiện việc này rất thường xuyên, vì vậy chương trình dường như chạy tất cả các chức năng này cùng một lúc. Vấn đề là các điểm là ngẫu nhiên, vì vậy bạn cần sử dụng khóa để ngăn người giám sát dừng chức năng tại một điểm có vấn đề.

Các trình tạo khá giống với các luồng theo nghĩa này: Chúng cho phép bạn chỉ định các điểm cụ thể (bất cứ khi nào chúng yield) nơi bạn có thể nhảy vào và ra. Khi được sử dụng theo cách này, máy phát điện được gọi là coroutines.

Đọc hướng dẫn tuyệt vời này về coroutines trong Python để biết thêm chi tiết


10
Câu trả lời này là sai lệch bởi vì nó phù hợp với tính năng nổi bật của "năng suất từ", như đã đề cập ở trên: hỗ trợ send () và throw ().
Justin W

2
@Justin W: Tôi đoán bất cứ điều gì bạn đọc trước đây thực sự là sai lệch, bởi vì bạn đã không nhận được điểm throw()/send()/close()yieldcác tính năng yield fromrõ ràng phải thực hiện đúng vì nó được cho là đơn giản hóa mã. Những chuyện nhỏ nhặt như vậy không liên quan gì đến việc sử dụng.
Jochen Ritzel

5
Bạn đang tranh luận câu trả lời của Ben Jackson ở trên? Tôi đọc câu trả lời của bạn là về cơ bản là đường cú pháp theo sự chuyển đổi mã mà bạn cung cấp. Câu trả lời của Ben Jackson đặc biệt bác bỏ tuyên bố đó.
Justin W

@JochenRitzel Bạn không bao giờ cần phải viết chainchức năng của riêng mình vì itertools.chainđã tồn tại. Sử dụng yield from itertools.chain(*iters).
Acumenus

4

Trong sử dụng được áp dụng cho coroutine IO không đồng bộ , yield fromcó hành vi tương tự như awaittrong chức năng coroutine . Cả hai đều được sử dụng để đình chỉ việc thực hiện coroutine.

Đối với Asyncio, nếu không cần hỗ trợ phiên bản Python cũ hơn (tức là> 3.5), async def/ awaitlà cú pháp được đề xuất để xác định coroutine. Do đó yield fromkhông còn cần thiết trong một coroutine.

Nhưng nói chung bên ngoài asyncio, yield from <sub-generator>vẫn còn một số cách sử dụng khác trong việc lặp lại trình tạo phụ như đã đề cập trong câu trả lời trước đó.


1

Mã này xác định một hàm fixed_sum_digitstrả về một trình tạo liệt kê tất cả các số có sáu chữ số sao cho tổng các chữ số là 20.

def iter_fun(sum, deepness, myString, Total):
    if deepness == 0:
        if sum == Total:
            yield myString
    else:  
        for i in range(min(10, Total - sum + 1)):
            yield from iter_fun(sum + i,deepness - 1,myString + str(i),Total)

def fixed_sum_digits(digits, Tot):
    return iter_fun(0,digits,"",Tot) 

Cố gắng viết nó mà không có yield from. Nếu bạn tìm thấy một cách hiệu quả để làm điều đó cho tôi biết.

Tôi nghĩ rằng đối với các trường hợp như thế này: thăm cây, yield fromlàm cho mã đơn giản và gọn gàng hơn.


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.