Từ khóa năng suất trên mạng làm gì?


10194

Việc sử dụng yieldtừ khóa trong Python là gì và nó làm gì?

Ví dụ: tôi đang cố gắng hiểu mã này 1 :

def _get_child_candidates(self, distance, min_dist, max_dist):
    if self._leftchild and distance - max_dist < self._median:
        yield self._leftchild
    if self._rightchild and distance + max_dist >= self._median:
        yield self._rightchild  

Và đây là người gọi:

result, candidates = [], [self]
while candidates:
    node = candidates.pop()
    distance = node._get_dist(obj)
    if distance <= max_dist and distance >= min_dist:
        result.extend(node._values)
    candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))
return result

Điều gì xảy ra khi phương thức _get_child_candidatesđược gọi? Là một danh sách trả lại? Một yếu tố duy nhất? Có được gọi lại không? Khi nào các cuộc gọi tiếp theo sẽ dừng lại?


1. Đoạn mã này được viết bởi Jochen Schulz (jrschulz), người đã tạo ra một thư viện Python tuyệt vời cho các không gian số liệu. Đây là liên kết đến nguồn hoàn chỉnh: Module mspace .

Câu trả lời:


14645

Để hiểu những gì yieldlàm, bạn phải hiểu máy phát điện là gì . Và trước khi bạn có thể hiểu máy phát điện, bạn phải hiểu iterables .

Lặp lại

Khi bạn tạo một danh sách, bạn có thể đọc từng mục một. Đọc các mục của nó từng cái một được gọi là lặp:

>>> mylist = [1, 2, 3]
>>> for i in mylist:
...    print(i)
1
2
3

mylistlà một lần lặp . Khi bạn sử dụng khả năng hiểu danh sách, bạn tạo một danh sách và do đó, có thể lặp lại:

>>> mylist = [x*x for x in range(3)]
>>> for i in mylist:
...    print(i)
0
1
4

Mọi thứ bạn có thể sử dụng " for... in..." trên là một lần lặp; lists,, stringstập tin ...

Các iterables này rất tiện dụng vì bạn có thể đọc chúng nhiều như bạn muốn, nhưng bạn lưu trữ tất cả các giá trị trong bộ nhớ và đây không phải lúc nào cũng là điều bạn muốn khi bạn có nhiều giá trị.

Máy phát điện

Trình tạo là các trình vòng lặp, một loại lặp mà bạn chỉ có thể lặp lại một lần . Các trình tạo không lưu trữ tất cả các giá trị trong bộ nhớ, chúng tạo ra các giá trị một cách nhanh chóng :

>>> mygenerator = (x*x for x in range(3))
>>> for i in mygenerator:
...    print(i)
0
1
4

Nó chỉ giống nhau ngoại trừ bạn sử dụng ()thay vì []. NHƯNG, bạn không thể thực hiện for i in mygeneratorlần thứ hai vì máy phát điện chỉ có thể được sử dụng một lần: họ tính 0, sau đó quên nó và tính 1, và kết thúc tính toán 4, từng cái một.

Năng suất

yieldlà một từ khóa được sử dụng như thế return, ngoại trừ hàm sẽ trả về một trình tạo.

>>> def createGenerator():
...    mylist = range(3)
...    for i in mylist:
...        yield i*i
...
>>> mygenerator = createGenerator() # create a generator
>>> print(mygenerator) # mygenerator is an object!
<generator object createGenerator at 0xb7555c34>
>>> for i in mygenerator:
...     print(i)
0
1
4

Đây là một ví dụ vô dụng, nhưng thật tiện lợi khi bạn biết chức năng của mình sẽ trả về một tập hợp giá trị khổng lồ mà bạn sẽ chỉ cần đọc một lần.

Để thành thạoyield , bạn phải hiểu rằng khi bạn gọi hàm, mã bạn đã viết trong thân hàm không chạy. Hàm chỉ trả về đối tượng trình tạo, điều này hơi khó :-)

Sau đó, mã của bạn sẽ tiếp tục từ nơi nó dừng lại mỗi lần forsử dụng trình tạo.

Bây giờ là phần khó:

Lần đầu tiên forgọi đối tượng trình tạo được tạo từ hàm của bạn, nó sẽ chạy mã trong hàm của bạn từ đầu cho đến khi chạm yield, sau đó nó sẽ trả về giá trị đầu tiên của vòng lặp. Sau đó, mỗi cuộc gọi tiếp theo sẽ chạy một vòng lặp khác của vòng lặp mà bạn đã viết trong hàm và trả về giá trị tiếp theo. Điều này sẽ tiếp tục cho đến khi trình tạo được coi là trống, xảy ra khi chức năng chạy mà không nhấn yield. Điều đó có thể là do vòng lặp đã kết thúc hoặc do bạn không còn thỏa mãn "if/else".


Mã của bạn đã giải thích

Máy phát điện:

# Here you create the method of the node object that will return the generator
def _get_child_candidates(self, distance, min_dist, max_dist):

    # Here is the code that will be called each time you use the generator object:

    # If there is still a child of the node object on its left
    # AND if the distance is ok, return the next child
    if self._leftchild and distance - max_dist < self._median:
        yield self._leftchild

    # If there is still a child of the node object on its right
    # AND if the distance is ok, return the next child
    if self._rightchild and distance + max_dist >= self._median:
        yield self._rightchild

    # If the function arrives here, the generator will be considered empty
    # there is no more than two values: the left and the right children

Người gọi:

# Create an empty list and a list with the current object reference
result, candidates = list(), [self]

# Loop on candidates (they contain only one element at the beginning)
while candidates:

    # Get the last candidate and remove it from the list
    node = candidates.pop()

    # Get the distance between obj and the candidate
    distance = node._get_dist(obj)

    # If distance is ok, then you can fill the result
    if distance <= max_dist and distance >= min_dist:
        result.extend(node._values)

    # Add the children of the candidate in the candidate's list
    # so the loop will keep running until it will have looked
    # at all the children of the children of the children, etc. of the candidate
    candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))

return result

Mã này chứa một số phần thông minh:

  • Vòng lặp lặp trên một danh sách, nhưng danh sách mở rộng trong khi vòng lặp đang được lặp lại :-) Đó là một cách ngắn gọn để đi qua tất cả các dữ liệu lồng nhau này ngay cả khi nó hơi nguy hiểm vì bạn có thể kết thúc bằng một vòng lặp vô hạn. Trong trường hợp này, candidates.extend(node._get_child_candidates(distance, min_dist, max_dist))làm cạn kiệt tất cả các giá trị của trình tạo, nhưng whiletiếp tục tạo các đối tượng trình tạo mới sẽ tạo ra các giá trị khác với các giá trị trước đó vì nó không được áp dụng trên cùng một nút.

  • Các extend()phương pháp là một phương pháp đối tượng trong danh sách đó hy vọng một iterable và thêm giá trị của nó vào danh sách.

Thông thường chúng ta chuyển một danh sách cho nó:

>>> a = [1, 2]
>>> b = [3, 4]
>>> a.extend(b)
>>> print(a)
[1, 2, 3, 4]

Nhưng trong mã của bạn, nó có một trình tạo, điều này tốt bởi vì:

  1. Bạn không cần phải đọc các giá trị hai lần.
  2. Bạn có thể có rất nhiều trẻ em và bạn không muốn tất cả chúng được lưu trữ trong bộ nhớ.

Và nó hoạt động vì Python không quan tâm xem đối số của phương thức có phải là danh sách hay không. Python mong đợi các lần lặp để nó sẽ hoạt động với các chuỗi, danh sách, bộ dữ liệu và trình tạo! Đây được gọi là gõ vịt và là một trong những lý do tại sao Python rất tuyệt. Nhưng đây là một câu chuyện khác, cho một câu hỏi khác ...

Bạn có thể dừng ở đây hoặc đọc một chút để thấy cách sử dụng nâng cao của trình tạo:

Kiểm soát kiệt sức máy phát điện

>>> class Bank(): # Let's create a bank, building ATMs
...    crisis = False
...    def create_atm(self):
...        while not self.crisis:
...            yield "$100"
>>> hsbc = Bank() # When everything's ok the ATM gives you as much as you want
>>> corner_street_atm = hsbc.create_atm()
>>> print(corner_street_atm.next())
$100
>>> print(corner_street_atm.next())
$100
>>> print([corner_street_atm.next() for cash in range(5)])
['$100', '$100', '$100', '$100', '$100']
>>> hsbc.crisis = True # Crisis is coming, no more money!
>>> print(corner_street_atm.next())
<type 'exceptions.StopIteration'>
>>> wall_street_atm = hsbc.create_atm() # It's even true for new ATMs
>>> print(wall_street_atm.next())
<type 'exceptions.StopIteration'>
>>> hsbc.crisis = False # The trouble is, even post-crisis the ATM remains empty
>>> print(corner_street_atm.next())
<type 'exceptions.StopIteration'>
>>> brand_new_atm = hsbc.create_atm() # Build a new one to get back in business
>>> for cash in brand_new_atm:
...    print cash
$100
$100
$100
$100
$100
$100
$100
$100
$100
...

Lưu ý: Đối với Python 3, sử dụng print(corner_street_atm.__next__())hoặcprint(next(corner_street_atm))

Nó có thể hữu ích cho những thứ khác nhau như kiểm soát truy cập vào tài nguyên.

Itertools, người bạn tốt nhất của bạn

Mô-đun itertools chứa các chức năng đặc biệt để thao tác các lần lặp. Bao giờ muốn nhân đôi một máy phát điện? Xích hai máy phát điện? Giá trị nhóm trong danh sách lồng nhau với một lớp lót? Map / Zipmà không tạo ra một danh sách khác?

Sau đó chỉ là import itertools.

Một ví dụ? Chúng ta hãy xem các đơn đặt hàng có thể đến cho một cuộc đua bốn con ngựa:

>>> horses = [1, 2, 3, 4]
>>> races = itertools.permutations(horses)
>>> print(races)
<itertools.permutations object at 0xb754f1dc>
>>> print(list(itertools.permutations(horses)))
[(1, 2, 3, 4),
 (1, 2, 4, 3),
 (1, 3, 2, 4),
 (1, 3, 4, 2),
 (1, 4, 2, 3),
 (1, 4, 3, 2),
 (2, 1, 3, 4),
 (2, 1, 4, 3),
 (2, 3, 1, 4),
 (2, 3, 4, 1),
 (2, 4, 1, 3),
 (2, 4, 3, 1),
 (3, 1, 2, 4),
 (3, 1, 4, 2),
 (3, 2, 1, 4),
 (3, 2, 4, 1),
 (3, 4, 1, 2),
 (3, 4, 2, 1),
 (4, 1, 2, 3),
 (4, 1, 3, 2),
 (4, 2, 1, 3),
 (4, 2, 3, 1),
 (4, 3, 1, 2),
 (4, 3, 2, 1)]

Hiểu các cơ chế bên trong của phép lặp

Lặp lại là một quá trình ngụ ý lặp (thực hiện __iter__()phương thức) và lặp (thực hiện __next__()phương thức). Lặp lại là bất kỳ đối tượng bạn có thể nhận được một trình vòng lặp từ. Lặp đi lặp lại là các đối tượng cho phép bạn lặp trên các vòng lặp.

Có nhiều hơn về nó trong bài viết này về cách forcác vòng lặp hoạt động .


355
yieldkhông phải là huyền diệu câu trả lời này cho thấy. Khi bạn gọi một hàm có chứa một yieldcâu lệnh ở bất cứ đâu, bạn sẽ nhận được một đối tượng trình tạo, nhưng không có mã nào chạy. Sau đó, mỗi lần bạn trích xuất một đối tượng từ trình tạo, Python thực thi mã trong hàm cho đến khi nó đến một yieldcâu lệnh, sau đó tạm dừng và phân phối đối tượng. Khi bạn trích xuất một đối tượng khác, Python tiếp tục ngay sau yieldvà tiếp tục cho đến khi nó đến một đối tượng khác yield(thường là cùng một đối tượng, nhưng một lần lặp lại sau). Điều này tiếp tục cho đến khi chức năng chạy hết, tại thời điểm đó, trình tạo được coi là hết.
Matthias Fripp

30
"Các iterables này rất tiện dụng ... nhưng bạn lưu trữ tất cả các giá trị trong bộ nhớ và đây không phải lúc nào cũng là điều bạn muốn", là sai hoặc khó hiểu. Một iterable trả về một iterator khi gọi iter () trên iterable và một iterator không phải luôn lưu trữ các giá trị của nó trong bộ nhớ, tùy thuộc vào việc thực hiện phương thức iter , nó cũng có thể tạo ra các giá trị theo trình tự theo yêu cầu.
picmate

Sẽ thật tốt khi thêm vào câu trả lời tuyệt vời này tại sao Nó chỉ giống nhau ngoại trừ bạn đã sử dụng ()thay vì[] , cụ thể ()là gì (có thể có sự nhầm lẫn với một tuple).
WoJ

Tôi có thể sai, nhưng một trình tạo không phải là một trình vòng lặp, "trình tạo được gọi là" là một trình vòng lặp.
aderchox

2006

Phím tắt để hiểu yield

Khi bạn thấy một hàm có các yieldcâu lệnh, hãy áp dụng mẹo đơn giản này để hiểu điều gì sẽ xảy ra:

  1. Chèn một dòng result = []ở đầu hàm.
  2. Thay thế yield exprbằng result.append(expr).
  3. Chèn một dòng return resultở dưới cùng của hàm.
  4. Yay - không có thêm yieldtuyên bố! Đọc và tìm ra mã.
  5. So sánh hàm với định nghĩa ban đầu.

Thủ thuật này có thể cho bạn ý tưởng về logic đằng sau hàm, nhưng những gì thực sự xảy ra với yieldnó khác biệt đáng kể so với những gì xảy ra trong cách tiếp cận dựa trên danh sách. Trong nhiều trường hợp, cách tiếp cận năng suất sẽ hiệu quả hơn rất nhiều về bộ nhớ và cũng nhanh hơn. Trong các trường hợp khác, thủ thuật này sẽ khiến bạn bị mắc kẹt trong một vòng lặp vô hạn, mặc dù chức năng ban đầu chỉ hoạt động tốt. Đọc để tìm hiểu thêm ...

Đừng nhầm lẫn Iterables, Iterators và Generators của bạn

Đầu tiên, giao thức lặp - khi bạn viết

for x in mylist:
    ...loop body...

Python thực hiện hai bước sau:

  1. Nhận một trình vòng lặp cho mylist:

    Gọi iter(mylist)-> điều này trả về một đối tượng với một next()phương thức (hoặc __next__()trong Python 3).

    [Đây là bước mà hầu hết mọi người quên nói với bạn về]

  2. Sử dụng iterator để lặp qua các mục:

    Tiếp tục gọi next()phương thức trên iterator được trả về từ bước 1. Giá trị trả về từ next()được gán cho xvà thân vòng lặp được thực thi. Nếu một ngoại lệ StopIterationđược đưa ra từ bên trong next(), điều đó có nghĩa là không có thêm giá trị nào trong iterator và vòng lặp được thoát.

Sự thật là Python thực hiện hai bước trên bất cứ lúc nào nó muốn lặp lại nội dung của một đối tượng - vì vậy nó có thể là một vòng lặp for, nhưng nó cũng có thể là mã như otherlist.extend(mylist)(trong đó otherlistlà danh sách Python).

Đây mylistlà một iterable vì nó thực hiện giao thức iterator. Trong một lớp do người dùng định nghĩa, bạn có thể thực hiện __iter__()phương thức để tạo các thể hiện của lớp lặp lại. Phương pháp này sẽ trả về một iterator . Một iterator là một đối tượng với một next()phương thức. Có thể thực hiện cả hai __iter__()next()trên cùng một lớp, và có __iter__()trả lại self. Điều này sẽ làm việc cho các trường hợp đơn giản, nhưng không phải khi bạn muốn hai vòng lặp lặp trên cùng một đối tượng cùng một lúc.

Vì vậy, đó là giao thức lặp, nhiều đối tượng thực hiện giao thức này:

  1. Tích hợp danh sách, từ điển, bộ dữ liệu, bộ, tập tin.
  2. Các lớp do người dùng định nghĩa thực hiện __iter__().
  3. Máy phát điện.

Lưu ý rằng một forvòng lặp không biết nó đang xử lý loại đối tượng nào - nó chỉ tuân theo giao thức iterator và rất vui khi nhận được vật phẩm sau vật phẩm khi nó gọi next(). Danh sách tích hợp trả lại từng mục một, từ điển trả lại từng khóa một, các tệp trả về từng dòng một, v.v. Và các trình tạo trả lại ... đó là nơi yieldxuất hiện:

def f123():
    yield 1
    yield 2
    yield 3

for item in f123():
    print item

Thay vì các yieldcâu lệnh, nếu bạn chỉ có ba returncâu lệnh f123()đầu tiên sẽ được thực thi và hàm sẽ thoát. Nhưng f123()không có chức năng bình thường. Khi f123()được gọi, nó không trả về bất kỳ giá trị nào trong các báo cáo lợi tức! Nó trả về một đối tượng máy phát điện. Ngoài ra, chức năng không thực sự thoát - nó đi vào trạng thái treo. Khi forvòng lặp cố gắng lặp qua đối tượng trình tạo, hàm sẽ tiếp tục từ trạng thái treo của nó ở dòng tiếp theo sau khi yieldnó được trả về trước đó, thực thi dòng mã tiếp theo, trong trường hợp này, một yieldcâu lệnh và trả về đó là dòng tiếp theo mục. Điều này xảy ra cho đến khi hàm thoát, tại đó trình tạo tăng StopIterationvà vòng lặp thoát.

Vì vậy, đối tượng trình tạo giống như một bộ chuyển đổi - ở một đầu, nó thể hiện giao thức iterator, bằng cách hiển thị __iter__()next()các phương thức để giữ cho forvòng lặp hạnh phúc. Tuy nhiên, ở đầu kia, nó chạy hàm vừa đủ để lấy giá trị tiếp theo ra khỏi nó và đưa nó trở lại chế độ treo.

Tại sao nên sử dụng Máy phát điện?

Thông thường, bạn có thể viết mã không sử dụng trình tạo nhưng thực hiện cùng logic. Một lựa chọn là sử dụng danh sách 'lừa' tạm thời mà tôi đã đề cập trước đây. Điều đó sẽ không hoạt động trong mọi trường hợp, ví dụ nếu bạn có các vòng lặp vô hạn hoặc nó có thể sử dụng bộ nhớ không hiệu quả khi bạn có một danh sách thực sự dài. Cách tiếp cận khác là triển khai một lớp lặp lặp mới SomethingIter giữ trạng thái thành viên thể hiện và thực hiện bước logic tiếp theo trong phương thức của nó next()(hoặc __next__()trong Python 3). Tùy thuộc vào logic, mã bên trong next()phương thức có thể trông rất phức tạp và dễ bị lỗi. Ở đây máy phát điện cung cấp một giải pháp sạch sẽ và dễ dàng.


20
"Khi bạn thấy một hàm có câu lệnh lợi nhuận, hãy áp dụng mẹo đơn giản này để hiểu điều gì sẽ xảy ra" Không phải điều này hoàn toàn bỏ qua thực tế là bạn có thể sendvào một máy phát điện, một phần rất lớn trong điểm của máy phát điện?
DanielSank

10
"Nó có thể là một vòng lặp for, nhưng nó cũng có thể là mã như otherlist.extend(mylist)" -> Điều này không chính xác. extend()sửa đổi danh sách tại chỗ và không trả về một lần lặp. Cố gắng lặp lại otherlist.extend(mylist)sẽ thất bại với một TypeErrorextend()hoàn toàn quay trở lại Nonevà bạn không thể lặp lại None.
Pedro

4
@pedro Bạn đã hiểu nhầm câu đó. Nó có nghĩa là python thực hiện hai bước được đề cập trên mylist(không bật otherlist) khi thực thi otherlist.extend(mylist).
hôm nay ngày

555

Nghĩ theo cách này:

Một iterator chỉ là một thuật ngữ âm thanh ưa thích cho một đối tượng có một next()phương thức. Vì vậy, một hàm suất-ed kết thúc giống như thế này:

Phiên bản gốc:

def some_function():
    for i in xrange(4):
        yield i

for i in some_function():
    print i

Về cơ bản, đây là những gì trình thông dịch Python thực hiện với đoạn mã trên:

class it:
    def __init__(self):
        # Start at -1 so that we get 0 when we add 1 below.
        self.count = -1

    # The __iter__ method will be called once by the 'for' loop.
    # The rest of the magic happens on the object returned by this method.
    # In this case it is the object itself.
    def __iter__(self):
        return self

    # The next method will be called repeatedly by the 'for' loop
    # until it raises StopIteration.
    def next(self):
        self.count += 1
        if self.count < 4:
            return self.count
        else:
            # A StopIteration exception is raised
            # to signal that the iterator is done.
            # This is caught implicitly by the 'for' loop.
            raise StopIteration

def some_func():
    return it()

for i in some_func():
    print i

Để hiểu rõ hơn về những gì xảy ra đằng sau hậu trường, forvòng lặp có thể được viết lại thành này:

iterator = some_func()
try:
    while 1:
        print iterator.next()
except StopIteration:
    pass

Điều đó có ý nghĩa hơn hay chỉ làm bạn bối rối hơn? :)

Tôi nên lưu ý rằng đây một sự đơn giản hóa cho mục đích minh họa. :)


1
__getitem__có thể được định nghĩa thay vì __iter__. Ví dụ : class it: pass; it.__getitem__ = lambda self, i: i*10 if i < 10 else [][0]; for i in it(): print(i), Nó sẽ in: 0, 10, 20, ..., 90
jfs

17
Tôi đã thử ví dụ này trong Python 3.6 và nếu tôi tạo iterator = some_function(), biến iteratorkhông có hàm được gọi next()nữa mà chỉ có một __next__()hàm. Tôi nghĩ tôi sẽ đề cập đến nó.
Peter

Trường hợp forthực hiện vòng lặp mà bạn đã viết gọi __iter__phương thức iterator, ví dụ khởi tạo của it?
SystematicDisintegration

455

Các yieldtừ khóa được giảm xuống còn hai sự kiện rất đơn giản:

  1. Nếu trình biên dịch phát hiện yieldtừ khóa ở bất cứ đâu trong hàm, hàm đó không còn trả về qua returncâu lệnh. Thay vào đó , nó ngay lập tức trả về một đối tượng "danh sách đang chờ xử lý" lười biếng được gọi là trình tạo
  2. Một máy phát điện có thể lặp lại. Một lặp đi lặp lại là gì? Đó là bất cứ thứ gì như một listhoặc sethoặc rangehoặc chế độ xem chính tả, với giao thức tích hợp để truy cập từng phần tử theo một thứ tự nhất định .

Tóm lại: trình tạo là một danh sách lười biếng, đang chờ xử lý tăng dần và các yieldcâu lệnh cho phép bạn sử dụng ký hiệu hàm để lập trình các giá trị danh sách mà trình tạo sẽ tăng dần.

generator = myYieldingFunction(...)
x = list(generator)

   generator
       v
[x[0], ..., ???]

         generator
             v
[x[0], x[1], ..., ???]

               generator
                   v
[x[0], x[1], x[2], ..., ???]

                       StopIteration exception
[x[0], x[1], x[2]]     done

list==[x[0], x[1], x[2]]

Thí dụ

Hãy xác định một hàm makeRangegiống như của Python range. Gọi makeRange(n)TRẢ LẠI MỘT MÁY PHÁT ĐIỆN:

def makeRange(n):
    # return 0,1,2,...,n-1
    i = 0
    while i < n:
        yield i
        i += 1

>>> makeRange(5)
<generator object makeRange at 0x19e4aa0>

Để buộc trình tạo trả về ngay các giá trị đang chờ xử lý, bạn có thể chuyển nó vào list()(giống như bạn có thể lặp lại bất kỳ):

>>> list(makeRange(5))
[0, 1, 2, 3, 4]

So sánh ví dụ với "chỉ trả về một danh sách"

Ví dụ trên có thể được coi là chỉ tạo ra một danh sách mà bạn nối vào và trả về:

# list-version                   #  # generator-version
def makeRange(n):                #  def makeRange(n):
    """return [0,1,2,...,n-1]""" #~     """return 0,1,2,...,n-1"""
    TO_RETURN = []               #>
    i = 0                        #      i = 0
    while i < n:                 #      while i < n:
        TO_RETURN += [i]         #~         yield i
        i += 1                   #          i += 1  ## indented
    return TO_RETURN             #>

>>> makeRange(5)
[0, 1, 2, 3, 4]

Có một sự khác biệt lớn, mặc dù; xem phần cuối cùng


Làm thế nào bạn có thể sử dụng máy phát điện

Một iterable là phần cuối cùng của việc hiểu danh sách và tất cả các trình tạo đều có thể lặp lại được, vì vậy chúng thường được sử dụng như vậy:

#                   _ITERABLE_
>>> [x+10 for x in makeRange(5)]
[10, 11, 12, 13, 14]

Để có được cảm giác tốt hơn cho máy phát điện, bạn có thể chơi xung quanh với itertoolsmô-đun (hãy chắc chắn sử dụng chain.from_iterablethay vì chainkhi được bảo hành). Ví dụ: bạn thậm chí có thể sử dụng các trình tạo để thực hiện các danh sách lười biếng dài vô tận như thế nào itertools.count(). Bạn có thể tự thực hiện def enumerate(iterable): zip(count(), iterable)hoặc thay thế bằng yieldtừ khóa trong vòng lặp while.

Xin lưu ý: máy phát điện thực sự có thể được sử dụng cho nhiều thứ khác, chẳng hạn như triển khai coroutines hoặc lập trình không xác định hoặc những thứ thanh lịch khác. Tuy nhiên, quan điểm "danh sách lười biếng" tôi trình bày ở đây là cách sử dụng phổ biến nhất mà bạn sẽ tìm thấy.


Đằng sau hậu trường

Đây là cách "Giao thức lặp Python" hoạt động. Đó là, những gì đang xảy ra khi bạn làm list(makeRange(5)). Đây là những gì tôi mô tả trước đây là một "danh sách gia tăng, lười biếng".

>>> x=iter(range(5))
>>> next(x)
0
>>> next(x)
1
>>> next(x)
2
>>> next(x)
3
>>> next(x)
4
>>> next(x)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Hàm tích hợp next()chỉ gọi .next()hàm đối tượng , là một phần của "giao thức lặp" và được tìm thấy trên tất cả các trình vòng lặp. Bạn có thể sử dụng next()chức năng thủ công (và các phần khác của giao thức lặp) để thực hiện những thứ ưa thích, thường là chi phí dễ đọc, vì vậy hãy cố gắng tránh làm điều đó ...


Minutiae

Thông thường, hầu hết mọi người sẽ không quan tâm đến những khác biệt sau đây và có lẽ muốn dừng đọc ở đây.

Trong Python-speak, iterable là bất kỳ đối tượng nào "hiểu khái niệm vòng lặp for" như danh sách [1,2,3]iterator là một thể hiện cụ thể của vòng lặp for được yêu cầu [1,2,3].__iter__(). Một trình tạo giống hệt như bất kỳ trình lặp nào, ngoại trừ cách nó được viết (với cú pháp hàm).

Khi bạn yêu cầu một trình vòng lặp từ danh sách, nó sẽ tạo ra một trình vòng lặp mới. Tuy nhiên, khi bạn yêu cầu một trình vòng lặp từ một trình vòng lặp (điều mà bạn hiếm khi làm), nó chỉ cung cấp cho bạn một bản sao của chính nó.

Do đó, trong trường hợp không chắc là bạn không làm được điều gì đó như thế này ...

> x = myRange(5)
> list(x)
[0, 1, 2, 3, 4]
> list(x)
[]

... Sau đó hãy nhớ rằng một trình tạo là một trình vòng lặp ; đó là sử dụng một lần Nếu bạn muốn sử dụng lại, bạn nên gọi myRange(...)lại. Nếu bạn cần sử dụng kết quả hai lần, hãy chuyển đổi kết quả thành một danh sách và lưu trữ nó trong một biến x = list(myRange(5)). Những người thực sự cần sao chép một trình tạo (ví dụ, những người đang thực hiện siêu dữ liệu hack siêu tốc) có thể sử dụng itertools.teenếu thực sự cần thiết, vì đề xuất tiêu chuẩn Python PEP có thể sao chép đã bị hoãn lại.


377

Không những gì yieldtừ khóa làm bằng Python?

Trả lời Đề cương / Tóm tắt

  • Một hàm với yield, khi được gọi, trả về một Trình tạo .
  • Các trình tạo là các trình vòng lặp vì chúng thực hiện giao thức lặp , vì vậy bạn có thể lặp qua chúng.
  • Một máy phát điện cũng có thể được gửi thông tin , làm cho nó về mặt khái niệm là một coroutine .
  • Trong Python 3, bạn có thể ủy nhiệm từ trình tạo này sang trình tạo khác theo cả hai hướng với yield from.
  • (Phụ lục phê bình một vài câu trả lời, bao gồm câu trả lời trên cùng và thảo luận về việc sử dụng returntrong một trình tạo.)

Máy phát điện:

yieldchỉ hợp pháp bên trong định nghĩa hàm và việc bao gồm yieldtrong định nghĩa hàm làm cho nó trả về một trình tạo.

Ý tưởng cho máy phát điện đến từ các ngôn ngữ khác (xem chú thích 1) với các cách triển khai khác nhau. Trong Trình tạo của Python, việc thực thi mã được đóng băng tại điểm sản lượng. Khi trình tạo được gọi (các phương thức được thảo luận bên dưới) thực thi lại và sau đó đóng băng ở sản lượng tiếp theo.

yieldcung cấp một cách dễ dàng để thực hiện giao thức iterator , được xác định bởi hai phương thức sau: __iter__next(Python 2) hoặc __next__(Python 3). Cả hai phương thức này làm cho một đối tượng trở thành một trình vòng lặp mà bạn có thể kiểm tra kiểu với IteratorLớp cơ sở trừu tượng từ collectionsmô-đun.

>>> def func():
...     yield 'I am'
...     yield 'a generator!'
... 
>>> type(func)                 # A function with yield is still a function
<type 'function'>
>>> gen = func()
>>> type(gen)                  # but it returns a generator
<type 'generator'>
>>> hasattr(gen, '__iter__')   # that's an iterable
True
>>> hasattr(gen, 'next')       # and with .next (.__next__ in Python 3)
True                           # implements the iterator protocol.

Kiểu máy phát là một kiểu con của trình vòng lặp:

>>> import collections, types
>>> issubclass(types.GeneratorType, collections.Iterator)
True

Và nếu cần, chúng ta có thể gõ kiểm tra như thế này:

>>> isinstance(gen, types.GeneratorType)
True
>>> isinstance(gen, collections.Iterator)
True

Một tính năng của một Iterator là đã hết , bạn không thể sử dụng lại hoặc đặt lại nó:

>>> list(gen)
['I am', 'a generator!']
>>> list(gen)
[]

Bạn sẽ phải tạo một cái khác nếu bạn muốn sử dụng lại chức năng của nó (xem chú thích 2):

>>> list(func())
['I am', 'a generator!']

Người ta có thể mang lại dữ liệu theo chương trình, ví dụ:

def func(an_iterable):
    for item in an_iterable:
        yield item

Trình tạo đơn giản ở trên cũng tương đương với bên dưới - kể từ Python 3.3 (và không có sẵn trong Python 2), bạn có thể sử dụng yield from:

def func(an_iterable):
    yield from an_iterable

Tuy nhiên, yield fromcũng cho phép ủy quyền cho các nhà phát triển phụ, điều này sẽ được giải thích trong phần sau về phái đoàn hợp tác với các tiểu đoàn.

Quân đoàn:

yield tạo thành một biểu thức cho phép dữ liệu được gửi vào trình tạo (xem chú thích 3)

Dưới đây là một ví dụ, lưu ý đến receivedbiến, nó sẽ trỏ đến dữ liệu được gửi đến trình tạo:

def bank_account(deposited, interest_rate):
    while True:
        calculated_interest = interest_rate * deposited 
        received = yield calculated_interest
        if received:
            deposited += received


>>> my_account = bank_account(1000, .05)

Đầu tiên, chúng ta phải xếp hàng trình tạo với hàm dựng sẵn , next. Nó sẽ gọi phương thức nexthoặc __next__phương thức thích hợp , tùy thuộc vào phiên bản Python bạn đang sử dụng:

>>> first_year_interest = next(my_account)
>>> first_year_interest
50.0

Và bây giờ chúng ta có thể gửi dữ liệu vào máy phát điện. ( Gửi Nonecũng giống như gọinext .):

>>> next_year_interest = my_account.send(first_year_interest + 1000)
>>> next_year_interest
102.5

Phái đoàn hợp tác với Tiểu đoàn với yield from

Bây giờ, hãy nhớ lại rằng yield fromcó sẵn trong Python 3. Điều này cho phép chúng ta ủy thác các coroutines cho một chương trình con:

def money_manager(expected_rate):
    under_management = yield     # must receive deposited value
    while True:
        try:
            additional_investment = yield expected_rate * under_management 
            if additional_investment:
                under_management += additional_investment
        except GeneratorExit:
            '''TODO: write function to send unclaimed funds to state'''
        finally:
            '''TODO: write function to mail tax info to client'''


def investment_account(deposited, manager):
    '''very simple model of an investment account that delegates to a manager'''
    next(manager) # must queue up manager
    manager.send(deposited)
    while True:
        try:
            yield from manager
        except GeneratorExit:
            return manager.close()

Và bây giờ chúng ta có thể ủy quyền chức năng cho một trình tạo phụ và nó có thể được sử dụng bởi một trình tạo như trên:

>>> my_manager = money_manager(.06)
>>> my_account = investment_account(1000, my_manager)
>>> first_year_return = next(my_account)
>>> first_year_return
60.0
>>> next_year_return = my_account.send(first_year_return + 1000)
>>> next_year_return
123.6

Bạn có thể đọc thêm về ngữ nghĩa chính xác yield fromtrong PEP 380.

Phương pháp khác: đóng và ném

Các closephương pháp làm tăng GeneratorExittại thời điểm thực hiện chức năng đã bị đóng băng. Điều này cũng sẽ được gọi bởi __del__vì vậy bạn có thể đặt bất kỳ mã dọn dẹp nào mà bạn xử lý GeneratorExit:

>>> my_account.close()

Bạn cũng có thể ném một ngoại lệ có thể được xử lý trong trình tạo hoặc truyền lại cho người dùng:

>>> import sys
>>> try:
...     raise ValueError
... except:
...     my_manager.throw(*sys.exc_info())
... 
Traceback (most recent call last):
  File "<stdin>", line 4, in <module>
  File "<stdin>", line 2, in <module>
ValueError

Phần kết luận

Tôi tin rằng tôi đã bao gồm tất cả các khía cạnh của câu hỏi sau đây:

Không những gì yieldtừ khóa làm bằng Python?

Nó chỉ ra rằng yieldrất nhiều. Tôi chắc chắn rằng tôi có thể thêm các ví dụ kỹ lưỡng hơn nữa vào điều này. Nếu bạn muốn nhiều hơn hoặc có một số lời chỉ trích mang tính xây dựng, hãy cho tôi biết bằng cách bình luận bên dưới.


Ruột thừa:

Phê bình về câu trả lời hàng đầu / được chấp nhận **

  • Nó bị nhầm lẫn về những gì làm cho một lần lặp , chỉ sử dụng một danh sách làm ví dụ. Xem các tài liệu tham khảo của tôi ở trên, nhưng tóm lại: một iterable có một __iter__phương thức trả về một iterator . Trình lặp cung cấp một phương thức .next(Python 2 hoặc .__next__(Python 3), được gọi ngầm bằng forcác vòng lặp cho đến khi nó tăng lên StopIteration, và một khi nó thực hiện, nó sẽ tiếp tục làm như vậy.
  • Sau đó, nó sử dụng một biểu thức máy phát để mô tả máy phát điện là gì. Vì một trình tạo chỉ đơn giản là một cách thuận tiện để tạo ra một trình vòng lặp , nên nó chỉ gây nhầm lẫn vấn đề và chúng ta vẫn chưa đến yieldphần.
  • Trong Kiểm soát sự cạn kiệt của máy phát điện , anh ta gọi .nextphương thức đó, khi thay vào đó anh ta nên sử dụng hàm dựng sẵn , next. Nó sẽ là một lớp gián tiếp thích hợp, bởi vì mã của anh ta không hoạt động trong Python 3.
  • Vòng lặp? Điều này không liên quan đến những gì yieldhiện tại.
  • Không có thảo luận về các phương thức yieldcung cấp cùng với chức năng mới yield fromtrong Python 3. Câu trả lời hàng đầu / được chấp nhận là một câu trả lời rất không đầy đủ.

Phê bình câu trả lời gợi ý yieldtrong một biểu thức hoặc hiểu.

Ngữ pháp hiện cho phép bất kỳ biểu thức trong một sự hiểu biết danh sách.

expr_stmt: testlist_star_expr (annassign | augassign (yield_expr|testlist) |
                     ('=' (yield_expr|testlist_star_expr))*)
...
yield_expr: 'yield' [yield_arg]
yield_arg: 'from' test | testlist

Vì năng suất là một biểu thức, nó được một số người thú vị sử dụng để hiểu nó trong biểu thức hiểu hoặc trình tạo - mặc dù trích dẫn không có trường hợp sử dụng đặc biệt tốt.

Các nhà phát triển cốt lõi của CPython đang thảo luận về việc từ chối trợ cấp của nó . Đây là một bài viết có liên quan từ danh sách gửi thư:

Vào ngày 30 tháng 1 năm 2017 lúc 19:05, Brett Cannon đã viết:

Trên Sun, ngày 29 tháng 1 năm 2017 lúc 16:39 Craig Coleues đã viết:

Tôi ổn với một trong hai cách tiếp cận. Bỏ mặc mọi thứ theo cách của họ trong Python 3 là không tốt, IMHO.

Phiếu bầu của tôi là SyntaxError vì bạn không nhận được những gì bạn mong đợi từ cú pháp.

Tôi đồng ý rằng đó là một nơi hợp lý để chúng tôi kết thúc, vì bất kỳ mã nào dựa trên hành vi hiện tại thực sự quá thông minh để có thể duy trì.

Về việc đến đó, chúng tôi có thể muốn:

  • Cú pháp hoặc Khấu haoWarning trong 3.7
  • Cảnh báo Py3k trong 2.7.x
  • Cú phápError trong 3.8

Chúc mừng, Nick.

- Nick Coghlan | ncoghlan tại gmail.com | thành phố ven sông Brisbane, là thủ phủ của Qeensland, miền đông nước Úc

Hơn nữa, có một vấn đề nổi bật (10544) dường như đang chỉ ra hướng này không bao giờ là một ý tưởng hay (PyPy, một triển khai Python được viết bằng Python, đã đưa ra cảnh báo cú pháp.)

Điểm mấu chốt, cho đến khi các nhà phát triển của CPython nói với chúng tôi khác: Đừng đưa yieldvào biểu thức hoặc sự hiểu biết của trình tạo.

Các returntuyên bố trong một máy phát điện

Trong Python 2 :

Trong hàm tạo, returncâu lệnh không được phép bao gồm một expression_list. Trong bối cảnh đó, trần returnchỉ ra rằng máy phát điện đã hoàn thành và sẽ gây ra sự StopIterationgia tăng.

An expression_listvề cơ bản là bất kỳ số lượng biểu thức nào được phân tách bằng dấu phẩy - về cơ bản, trong Python 2, bạn có thể dừng trình tạo return, nhưng bạn không thể trả về giá trị.

Trong Python 3 :

Trong một chức năng của trình tạo, returncâu lệnh chỉ ra rằng trình tạo được hoàn thành và sẽ gây ra sự StopIterationtăng lên. Giá trị được trả về (nếu có) được sử dụng làm đối số để xây dựng StopIterationvà trở thành StopIteration.valuethuộc tính.

Chú thích

  1. Các ngôn ngữ CLU, Sather và Icon đã được tham chiếu trong đề xuất giới thiệu khái niệm về trình tạo cho Python. Ý tưởng chung là một chức năng có thể duy trì trạng thái bên trong và mang lại các điểm dữ liệu trung gian theo yêu cầu của người dùng. Điều này hứa hẹn sẽ có hiệu suất vượt trội so với các phương pháp khác, bao gồm phân luồng Python , thậm chí không khả dụng trên một số hệ thống.

  2. Điều này có nghĩa là, ví dụ, xrangecác đối tượng ( rangetrong Python 3) không phải Iteratorlà s, mặc dù chúng có thể lặp lại được, bởi vì chúng có thể được sử dụng lại. Giống như danh sách, các __iter__phương thức của chúng trả về các đối tượng lặp.

  3. yieldban đầu được giới thiệu như một tuyên bố, có nghĩa là nó chỉ có thể xuất hiện ở đầu một dòng trong một khối mã. Bây giờ yieldtạo ra một biểu thức năng suất. https://docs.python.org/2/reference/simple_stmts.html#grammar-token-yield_stmt Thay đổi này được đề xuất để cho phép người dùng gửi dữ liệu vào trình tạo như người ta có thể nhận được. Để gửi dữ liệu, người ta phải có thể gán nó cho một cái gì đó và vì thế, một tuyên bố sẽ không hoạt động.


328

yieldcũng giống như return- nó trả về bất cứ điều gì bạn nói với nó (như một trình tạo). Sự khác biệt là lần sau khi bạn gọi trình tạo, việc thực thi bắt đầu từ lệnh gọi cuối cùng đến yieldcâu lệnh. Không giống như trả về, khung ngăn xếp không được dọn sạch khi xuất hiện sản lượng, tuy nhiên điều khiển được chuyển trở lại cho người gọi, do đó trạng thái của nó sẽ tiếp tục lại khi chức năng được gọi tiếp theo.

Trong trường hợp mã của bạn, hàm get_child_candidateshoạt động như một trình vòng lặp để khi bạn mở rộng danh sách của mình, nó sẽ thêm một phần tử tại một danh sách mới vào danh sách mới.

list.extendgọi một vòng lặp cho đến khi nó cạn kiệt. Trong trường hợp mẫu mã bạn đã đăng, sẽ rõ ràng hơn nhiều nếu chỉ trả lại một tuple và thêm nó vào danh sách.


107
Điều này là gần, nhưng không chính xác. Mỗi khi bạn gọi một hàm có câu lệnh lợi suất trong nó, nó sẽ trả về một đối tượng trình tạo hoàn toàn mới. Chỉ khi bạn gọi phương thức .next () của trình tạo đó mới thực hiện lại sau lần mang lại cuối cùng.
Kurosch

239

Có một điều nữa cần đề cập: một chức năng mang lại không thực sự phải chấm dứt. Tôi đã viết mã như thế này:

def fib():
    last, cur = 0, 1
    while True: 
        yield cur
        last, cur = cur, last + cur

Sau đó, tôi có thể sử dụng nó trong mã khác như thế này:

for f in fib():
    if some_condition: break
    coolfuncs(f);

Nó thực sự giúp đơn giản hóa một số vấn đề, và làm cho một số thứ dễ dàng hơn để làm việc với.


233

Đối với những người thích một ví dụ làm việc tối thiểu, hãy thiền về phiên Python tương tác này:

>>> def f():
...   yield 1
...   yield 2
...   yield 3
... 
>>> g = f()
>>> for i in g:
...   print(i)
... 
1
2
3
>>> for i in g:
...   print(i)
... 
>>> # Note that this time nothing was printed

209

TL; DR

Thay vì điều này:

def square_list(n):
    the_list = []                         # Replace
    for x in range(n):
        y = x * x
        the_list.append(y)                # these
    return the_list                       # lines

làm cái này:

def square_yield(n):
    for x in range(n):
        y = x * x
        yield y                           # with this one.

Bất cứ khi nào bạn thấy mình xây dựng một danh sách từ đầu, yieldthay vào đó , từng mảnh.

Đây là khoảnh khắc "aha" đầu tiên của tôi với năng suất.


yieldlà một cách nói đường

xây dựng một loạt các công cụ

Hành vi tương tự:

>>> for square in square_list(4):
...     print(square)
...
0
1
4
9
>>> for square in square_yield(4):
...     print(square)
...
0
1
4
9

Hành vi khác nhau:

Năng suất là một lần vượt qua : bạn chỉ có thể lặp lại một lần. Khi một hàm có năng suất trong nó, chúng ta gọi nó là hàm tạo . Và một iterator là những gì nó trả về. Những điều khoản được tiết lộ. Chúng tôi mất đi sự tiện lợi của một container, nhưng có được sức mạnh của một chuỗi được tính toán khi cần thiết và dài tùy ý.

Năng suất là lười biếng , nó đặt ra tính toán. Một hàm có năng suất trong nó hoàn toàn không thực thi khi bạn gọi nó. Nó trả về một đối tượng lặp mà nhớ nơi nó rời đi. Mỗi lần bạn gọi next()vào trình vòng lặp (điều này xảy ra trong một vòng lặp for) thực hiện chuyển tiếp tới sản lượng tiếp theo. returntăng StopIteration và kết thúc chuỗi (đây là kết thúc tự nhiên của vòng lặp for).

Năng suất rất linh hoạt . Dữ liệu không phải được lưu trữ cùng nhau, dữ liệu có thể được cung cấp cùng một lúc. Nó có thể là vô hạn.

>>> def squares_all_of_them():
...     x = 0
...     while True:
...         yield x * x
...         x += 1
...
>>> squares = squares_all_of_them()
>>> for _ in range(4):
...     print(next(squares))
...
0
1
4
9

Nếu bạn cần nhiều lượt đi và chuỗi không quá dài, chỉ cần gọi list()nó:

>>> list(square_yield(4))
[0, 1, 4, 9]

Lựa chọn tuyệt vời của từ yieldcả hai ý nghĩa áp dụng:

năng suất - sản xuất hoặc cung cấp (như trong nông nghiệp)

... cung cấp dữ liệu tiếp theo trong chuỗi.

nhường - nhường đường hoặc từ bỏ (như trong quyền lực chính trị)

... từ bỏ thực thi CPU cho đến khi trình vòng lặp tiến lên.


194

Yield cung cấp cho bạn một máy phát điện.

def get_odd_numbers(i):
    return range(1, i, 2)
def yield_odd_numbers(i):
    for x in range(1, i, 2):
       yield x
foo = get_odd_numbers(10)
bar = yield_odd_numbers(10)
foo
[1, 3, 5, 7, 9]
bar
<generator object yield_odd_numbers at 0x1029c6f50>
bar.next()
1
bar.next()
3
bar.next()
5

Như bạn có thể thấy, trong trường hợp đầu tiên foogiữ toàn bộ danh sách trong bộ nhớ cùng một lúc. Nó không phải là một vấn đề lớn đối với một danh sách có 5 yếu tố, nhưng nếu bạn muốn một danh sách 5 triệu thì sao? Đây không chỉ là một bộ nhớ khổng lồ, mà còn tốn rất nhiều thời gian để xây dựng tại thời điểm mà hàm được gọi.

Trong trường hợp thứ hai, barchỉ cần cung cấp cho bạn một máy phát điện. Trình tạo là một iterable - có nghĩa là bạn có thể sử dụng nó trong một forvòng lặp, v.v., nhưng mỗi giá trị chỉ có thể được truy cập một lần. Tất cả các giá trị cũng không được lưu trữ trong bộ nhớ cùng một lúc; đối tượng trình tạo "ghi nhớ" vị trí của nó trong vòng lặp lần cuối bạn gọi nó - theo cách này, nếu bạn đang sử dụng một số lần lặp để đếm tới 50 tỷ, bạn không phải đếm tới 50 tỷ cùng một lúc và lưu trữ 50 tỷ số để đếm qua.

Một lần nữa, đây là một ví dụ khá giả tạo, bạn có thể sẽ sử dụng itertools nếu bạn thực sự muốn đếm đến 50 tỷ. :)

Đây là trường hợp sử dụng đơn giản nhất của máy phát điện. Như bạn đã nói, nó có thể được sử dụng để viết các hoán vị hiệu quả, sử dụng năng suất để đẩy mọi thứ lên qua ngăn xếp cuộc gọi thay vì sử dụng một số biến số ngăn xếp. Máy phát điện cũng có thể được sử dụng để truyền tải cây chuyên dụng và tất cả các cách khác.


Chỉ cần một lưu ý - trong Python 3, rangecũng trả về một trình tạo thay vì danh sách, vì vậy bạn cũng sẽ thấy một ý tưởng tương tự, ngoại trừ việc __repr__/ __str__bị ghi đè để hiển thị kết quả đẹp hơn, trong trường hợp này range(1, 10, 2).
Đó là NÓI.

189

Nó đang trả lại một máy phát điện. Tôi không đặc biệt quen thuộc với Python, nhưng tôi tin rằng đó là loại tương tự như các khối lặp của C # nếu bạn quen thuộc với chúng.

Ý tưởng chính là trình biên dịch / trình thông dịch / bất cứ điều gì có một số mánh khóe để liên quan đến người gọi, họ có thể tiếp tục gọi next () và nó sẽ tiếp tục trả về các giá trị - như thể phương thức trình tạo bị tạm dừng . Bây giờ rõ ràng bạn không thể thực sự "tạm dừng" một phương thức, vì vậy trình biên dịch xây dựng một máy trạng thái để bạn nhớ vị trí hiện tại của bạn và các biến cục bộ, v.v. Điều này dễ hơn nhiều so với việc tự viết một trình vòng lặp.


167

Có một loại câu trả lời mà tôi không cảm thấy đã được đưa ra, trong số rất nhiều câu trả lời tuyệt vời mô tả cách sử dụng máy phát điện. Đây là câu trả lời lý thuyết ngôn ngữ lập trình:

Câu yieldlệnh trong Python trả về một trình tạo. Trình tạo trong Python là một hàm trả về các phần tiếp theo (và cụ thể là một loại coroutine, nhưng phần tiếp theo thể hiện cơ chế tổng quát hơn để hiểu những gì đang diễn ra).

Sự tiếp nối trong lý thuyết ngôn ngữ lập trình là một loại tính toán cơ bản hơn nhiều, nhưng chúng không thường được sử dụng, bởi vì chúng cực kỳ khó lý luận và cũng rất khó thực hiện. Nhưng ý tưởng về sự tiếp nối là gì, rất đơn giản: đó là trạng thái của một tính toán chưa kết thúc. Ở trạng thái này, các giá trị hiện tại của các biến, các hoạt động chưa được thực hiện, v.v., được lưu. Sau đó, tại một số điểm sau đó trong chương trình, việc tiếp tục có thể được gọi, sao cho các biến của chương trình được đặt lại về trạng thái đó và các hoạt động đã được lưu được thực hiện.

Tiếp tục, trong hình thức tổng quát hơn này, có thể được thực hiện theo hai cách. Theo call/cccách này, ngăn xếp của chương trình được lưu theo nghĩa đen và sau đó khi tiếp tục được gọi, ngăn xếp được khôi phục.

Trong kiểu truyền tiếp tục (CPS), các phần tiếp theo chỉ là các hàm bình thường (chỉ trong các ngôn ngữ có các hàm là lớp đầu tiên) mà lập trình viên quản lý rõ ràng và chuyển qua các chương trình con. Trong kiểu này, trạng thái chương trình được biểu diễn bằng các bao đóng (và các biến xảy ra được mã hóa trong chúng) chứ không phải là các biến nằm ở đâu đó trên ngăn xếp. Các hàm quản lý luồng điều khiển chấp nhận tiếp tục làm đối số (trong một số biến thể của CPS, các hàm có thể chấp nhận nhiều liên tục) và điều khiển luồng điều khiển bằng cách gọi chúng bằng cách gọi chúng và quay lại sau đó. Một ví dụ rất đơn giản về kiểu chuyền tiếp tục như sau:

def save_file(filename):
  def write_file_continuation():
    write_stuff_to_file(filename)

  check_if_file_exists_and_user_wants_to_overwrite(write_file_continuation)

Trong ví dụ này (rất đơn giản), lập trình viên lưu hoạt động thực sự ghi tệp vào một phần tiếp theo (có thể có khả năng là một hoạt động rất phức tạp với nhiều chi tiết để viết ra), và sau đó chuyển qua phần tiếp theo đó (như là lần đầu tiên- đóng lớp) cho một toán tử khác thực hiện thêm một số xử lý, và sau đó gọi nó nếu cần thiết. (Tôi sử dụng mẫu thiết kế này rất nhiều trong lập trình GUI thực tế, vì nó giúp tôi tiết kiệm các dòng mã hoặc quan trọng hơn là quản lý luồng điều khiển sau khi kích hoạt sự kiện GUI.)

Phần còn lại của bài đăng này, sẽ không mất tính tổng quát, khái niệm các phần tiếp theo là CPS, bởi vì nó là một địa ngục dễ hiểu và dễ đọc hơn rất nhiều.


Bây giờ hãy nói về máy phát điện trong Python. Máy phát điện là một kiểu con cụ thể của sự tiếp tục. Trong khi các phần tiếp theo nói chung có thể lưu trạng thái tính toán (nghĩa là ngăn xếp cuộc gọi của chương trình), các trình tạo chỉ có thể lưu trạng thái lặp qua trình lặp . Mặc dù, định nghĩa này hơi sai lệch đối với một số trường hợp sử dụng máy phát điện. Ví dụ:

def f():
  while True:
    yield 4

Đây rõ ràng là một lần lặp hợp lý có hành vi được xác định rõ - mỗi lần trình tạo lặp lại trên nó, nó sẽ trả về 4 (và cứ như vậy mãi mãi). Nhưng nó có lẽ không phải là kiểu lặp của nguyên mẫu mà tôi nghĩ đến khi nghĩ về các trình vòng lặp (nghĩa là for x in collection: do_something(x)). Ví dụ này minh họa sức mạnh của máy phát điện: nếu bất cứ thứ gì là một trình vòng lặp, thì một trình tạo có thể lưu trạng thái lặp của nó.

Để nhắc lại: Tiếp tục có thể lưu trạng thái của ngăn xếp chương trình và trình tạo có thể lưu trạng thái lặp. Điều này có nghĩa là việc tiếp tục mạnh hơn rất nhiều so với máy phát điện, nhưng cũng có thể máy phát điện dễ dàng hơn rất nhiều. Trình thiết kế ngôn ngữ dễ thực hiện hơn và lập trình viên dễ sử dụng hơn (nếu bạn có thời gian để ghi, hãy thử đọc và hiểu trang này về phần tiếp theo và gọi / cc ).

Nhưng bạn có thể dễ dàng thực hiện (và khái niệm hóa) các trình tạo như một trường hợp đơn giản, cụ thể của kiểu truyền tiếp tục:

Bất cứ khi nào yieldđược gọi, nó báo cho hàm trả về tiếp tục. Khi chức năng được gọi lại, nó bắt đầu từ bất cứ nơi nào nó rời đi. Vì vậy, trong mã giả mã (nghĩa là không phải mã giả, nhưng không mã), nextphương thức của trình tạo về cơ bản như sau:

class Generator():
  def __init__(self,iterable,generatorfun):
    self.next_continuation = lambda:generatorfun(iterable)

  def next(self):
    value, next_continuation = self.next_continuation()
    self.next_continuation = next_continuation
    return value

trong đó yieldtừ khóa thực sự là cú pháp đường cho hàm tạo thực, về cơ bản là:

def generatorfun(iterable):
  if len(iterable) == 0:
    raise StopIteration
  else:
    return (iterable[0], lambda:generatorfun(iterable[1:]))

Hãy nhớ rằng đây chỉ là mã giả và việc triển khai thực tế các trình tạo trong Python phức tạp hơn. Nhưng như một bài tập để hiểu những gì đang diễn ra, hãy thử sử dụng kiểu truyền tiếp tục để thực hiện các đối tượng trình tạo mà không sử dụng yieldtừ khóa.


152

Đây là một ví dụ trong ngôn ngữ đơn giản. Tôi sẽ cung cấp một sự tương ứng giữa các khái niệm con người cấp cao với các khái niệm Python cấp thấp.

Tôi muốn thực hiện theo một chuỗi các số, nhưng tôi không muốn làm phiền bản thân mình với việc tạo ra chuỗi đó, tôi chỉ muốn tập trung vào thao tác tôi muốn làm. Vì vậy, tôi làm như sau:

  • Tôi gọi cho bạn và nói với bạn rằng tôi muốn một chuỗi các số được tạo ra theo một cách cụ thể, và tôi cho bạn biết thuật toán là gì.
    Bước này tương ứng với defviệc nhập hàm tạo, tức là hàm chứa a yield.
  • Một lúc sau, tôi nói với bạn, "OK, hãy sẵn sàng cho tôi biết chuỗi số".
    Bước này tương ứng với việc gọi hàm tạo tạo trả về một đối tượng trình tạo. Lưu ý rằng bạn chưa cho tôi biết bất kỳ số nào; bạn chỉ cần lấy giấy và bút chì của bạn.
  • Tôi hỏi bạn, "cho tôi biết số tiếp theo" và bạn cho tôi biết số đầu tiên; Sau đó, bạn đợi tôi hỏi bạn số tiếp theo. Đó là công việc của bạn để nhớ bạn đã ở đâu, những con số bạn đã nói và số tiếp theo là gì. Tôi không quan tâm đến các chi tiết.
    Bước này tương ứng với việc gọi .next()đối tượng tạo.
  • Lặp lại bước trước, cho đến khi
  • cuối cùng, bạn có thể kết thúc Bạn không cho tôi biết một số; bạn chỉ cần hét lên, "giữ ngựa của bạn! Tôi đã hoàn tất! Không còn số nào nữa!"
    Bước này tương ứng với đối tượng trình tạo kết thúc công việc của nó và đưa ra một StopIterationngoại lệ Chức năng trình tạo không cần phải tăng ngoại lệ. Nó được nâng lên tự động khi chức năng kết thúc hoặc sự cố a return.

Đây là những gì một trình tạo làm (một hàm chứa a yield); nó bắt đầu thực thi, tạm dừng bất cứ khi nào nó thực hiện yieldvà khi được yêu cầu một .next()giá trị, nó tiếp tục từ điểm cuối cùng. Nó phù hợp hoàn hảo bởi thiết kế với giao thức iterator của Python, mô tả cách yêu cầu tuần tự các giá trị.

Người dùng nổi tiếng nhất của giao thức iterator là forlệnh trong Python. Vì vậy, bất cứ khi nào bạn làm một:

for item in sequence:

không thành vấn đề nếu sequencelà danh sách, chuỗi, từ điển hoặc đối tượng trình tạo như mô tả ở trên; kết quả là như nhau: bạn đọc từng mục một.

Lưu ý rằng việc nhập defmột hàm chứa yieldtừ khóa không phải là cách duy nhất để tạo trình tạo; đó chỉ là cách dễ nhất để tạo một cái.

Để biết thông tin chính xác hơn, hãy đọc về các loại trình vòng lặp , báo cáo sản lượng và trình tạo trong tài liệu Python.


130

Trong khi rất nhiều câu trả lời cho thấy lý do tại sao bạn sử dụng một yieldđể tạo một trình tạo, có nhiều cách sử dụng hơn cho yield. Thật dễ dàng để tạo ra một coroutine, cho phép truyền thông tin giữa hai khối mã. Tôi sẽ không lặp lại bất kỳ ví dụ hay nào đã được đưa ra về việc sử dụng yieldđể tạo một trình tạo.

Để giúp hiểu những gì a yieldlàm trong mã sau đây, bạn có thể sử dụng ngón tay của mình để theo dõi chu kỳ thông qua bất kỳ mã nào có a yield. Mỗi khi ngón tay của bạn chạm vào yield, bạn phải đợi một nexthoặc một sendđể được nhập. Khi a nextđược gọi, bạn theo dõi mã cho đến khi bạn nhấn yieldmã Mã ở bên phải mã yieldđược đánh giá và trả lại cho người gọi Lọ thì bạn chờ. Khi nextđược gọi lại, bạn thực hiện một vòng lặp khác thông qua mã. Tuy nhiên, bạn sẽ lưu ý rằng trong một coroutine, yieldcũng có thể được sử dụng với một tên lửa sendsẽ gửi một giá trị từ người gọi vào chức năng cho năng suất. Nếu a sendđược đưa ra, thìyieldnhận giá trị được gửi và nhổ nó ra phía bên trái, sau đó dấu vết thông qua mã tiến triển cho đến khi bạn nhấn yieldlại (trả lại giá trị ở cuối, như thể nextđược gọi).

Ví dụ:

>>> def coroutine():
...     i = -1
...     while True:
...         i += 1
...         val = (yield i)
...         print("Received %s" % val)
...
>>> sequence = coroutine()
>>> sequence.next()
0
>>> sequence.next()
Received None
1
>>> sequence.send('hello')
Received hello
2
>>> sequence.close()

Dễ thương! Một tấm bạt lò xo (theo nghĩa Lisp). Không thường xuyên nhìn thấy những người!
00prometheus

129

Có một cách yieldsử dụng và ý nghĩa khác (kể từ Python 3.3):

yield from <expr>

Từ PEP 380 - Cú pháp để ủy quyền cho một Subgenerator :

Một cú pháp được đề xuất cho một trình tạo để ủy quyền một phần hoạt động của nó cho một trình tạo khác. Điều này cho phép một phần mã chứa 'suất' được đưa ra và đặt vào một trình tạo khác. Ngoài ra, bộ phát phụ được phép trả về với một giá trị và giá trị này được cung cấp cho bộ tạo ủy nhiệm.

Cú pháp mới cũng mở ra một số cơ hội để tối ưu hóa khi một trình tạo lại mang lại giá trị do người khác tạo ra.

Hơn nữa, điều này sẽ giới thiệu (kể từ Python 3.5):

async def new_coroutine(data):
   ...
   await blocking_action()

để tránh coroutines bị nhầm lẫn với một máy phát thông thường (ngày nay yieldđược sử dụng trong cả hai).


117

Tất cả các câu trả lời tuyệt vời, tuy nhiên một chút khó khăn cho người mới.

Tôi giả sử bạn đã học được returntuyên bố.

Như một sự tương tự, returnyieldlà anh em sinh đôi. returncó nghĩa là 'trở lại và dừng lại' trong khi 'suất` có nghĩa là' trở lại, nhưng tiếp tục '

  1. Cố gắng để có được một num_list với return.
def num_list(n):
    for i in range(n):
        return i

Chạy nó:

In [5]: num_list(3)
Out[5]: 0

Hãy xem, bạn chỉ nhận được một số duy nhất chứ không phải là một danh sách của họ. returnkhông bao giờ cho phép bạn thắng thế một cách hạnh phúc, chỉ cần thực hiện một lần và bỏ.

  1. Có đến yield

Thay thế returnbằng yield:

In [10]: def num_list(n):
    ...:     for i in range(n):
    ...:         yield i
    ...:

In [11]: num_list(3)
Out[11]: <generator object num_list at 0x10327c990>

In [12]: list(num_list(3))
Out[12]: [0, 1, 2]

Bây giờ, bạn thắng để có được tất cả các số.

So sánh với returnlần chạy một lần và dừng, yieldchạy lần bạn đã lên kế hoạch. Bạn có thể giải thích returnnhư return one of them, và yieldnhư return all of them. Điều này được gọi là iterable.

  1. Một bước nữa chúng ta có thể viết lại yieldcâu lệnh vớireturn
In [15]: def num_list(n):
    ...:     result = []
    ...:     for i in range(n):
    ...:         result.append(i)
    ...:     return result

In [16]: num_list(3)
Out[16]: [0, 1, 2]

Đó là cốt lõi về yield.

Sự khác biệt giữa returnđầu ra danh sách và đầu ra đối tượng yieldlà:

Bạn sẽ luôn nhận được [0, 1, 2] từ một đối tượng danh sách nhưng chỉ có thể truy xuất chúng từ ' yieldđầu ra của đối tượng ' một lần. Vì vậy, nó có một generatorđối tượng tên mới như được hiển thị trong Out[11]: <generator object num_list at 0x10327c990>.

Tóm lại, như một phép ẩn dụ để mò mẫm nó:

  • returnyieldlà anh em sinh đôi
  • listgeneratorlà anh em sinh đôi

Điều này có thể hiểu được, nhưng một điểm khác biệt chính là bạn có thể có nhiều sản lượng trong một hàm / phương thức. Sự tương tự hoàn toàn bị phá vỡ tại thời điểm đó. Yield ghi nhớ vị trí của nó trong một hàm, vì vậy lần sau khi bạn gọi next (), hàm của bạn sẽ tiếp tục tiếp theo yield. Điều này là quan trọng, tôi nghĩ, và nên được thể hiện.
Mike S

104

Dưới đây là một số ví dụ về Python về cách thực hiện các trình tạo như thể Python không cung cấp đường cú pháp cho chúng:

Là một trình tạo Python:

from itertools import islice

def fib_gen():
    a, b = 1, 1
    while True:
        yield a
        a, b = b, a + b

assert [1, 1, 2, 3, 5] == list(islice(fib_gen(), 5))

Sử dụng đóng cửa từ vựng thay vì máy phát điện

def ftake(fnext, last):
    return [fnext() for _ in xrange(last)]

def fib_gen2():
    #funky scope due to python2.x workaround
    #for python 3.x use nonlocal
    def _():
        _.a, _.b = _.b, _.a + _.b
        return _.a
    _.a, _.b = 0, 1
    return _

assert [1,1,2,3,5] == ftake(fib_gen2(), 5)

Sử dụng các bao đóng đối tượng thay vì các trình tạo (vì ClosuresAndObjectsAreEquivalent )

class fib_gen3:
    def __init__(self):
        self.a, self.b = 1, 1

    def __call__(self):
        r = self.a
        self.a, self.b = self.b, self.a + self.b
        return r

assert [1,1,2,3,5] == ftake(fib_gen3(), 5)

97

Tôi sẽ đăng "đọc trang 19 của 'Python: Tài liệu tham khảo thiết yếu' của Beazley để mô tả nhanh về máy phát điện", nhưng rất nhiều người khác đã đăng những mô tả hay.

Ngoài ra, lưu ý rằng yieldcó thể được sử dụng trong coroutines như là sử dụng kép của chúng trong các chức năng của trình tạo. Mặc dù nó không được sử dụng giống như đoạn mã của bạn, nhưng (yield)có thể được sử dụng như một biểu thức trong một hàm. Khi một người gọi gửi một giá trị cho phương thức bằng send()phương thức đó, thì coroutine sẽ thực thi cho đến khi (yield)gặp câu lệnh tiếp theo .

Máy phát điện và coroutines là một cách hay để thiết lập các ứng dụng loại luồng dữ liệu. Tôi nghĩ rằng nó sẽ có giá trị khi biết về việc sử dụng khác của yieldcâu lệnh trong các hàm.


97

Từ quan điểm lập trình, các trình vòng lặp được triển khai dưới dạng thunks .

Để thực hiện các trình vòng lặp, trình tạo và nhóm luồng để thực thi đồng thời, v.v. như thunks (còn được gọi là hàm ẩn danh), người ta sử dụng các tin nhắn được gửi đến một đối tượng đóng, trong đó có một bộ điều phối và bộ điều phối trả lời cho "tin nhắn".

http://en.wikipedia.org/wiki/Message_passing

" next " là một tin nhắn được gửi đến một bao đóng, được tạo bởi lệnh gọi " iter ".

Có rất nhiều cách để thực hiện tính toán này. Tôi đã sử dụng đột biến, nhưng thật dễ dàng để làm điều đó mà không bị đột biến, bằng cách trả lại giá trị hiện tại và năng suất tiếp theo.

Đây là một minh chứng sử dụng cấu trúc của R6RS, nhưng ngữ nghĩa hoàn toàn giống với Python. Đó là cùng một mô hình tính toán và chỉ cần thay đổi cú pháp để viết lại nó trong Python.

Welcome to Racket v6.5.0.3.

-> (define gen
     (lambda (l)
       (define yield
         (lambda ()
           (if (null? l)
               'END
               (let ((v (car l)))
                 (set! l (cdr l))
                 v))))
       (lambda(m)
         (case m
           ('yield (yield))
           ('init  (lambda (data)
                     (set! l data)
                     'OK))))))
-> (define stream (gen '(1 2 3)))
-> (stream 'yield)
1
-> (stream 'yield)
2
-> (stream 'yield)
3
-> (stream 'yield)
'END
-> ((stream 'init) '(a b))
'OK
-> (stream 'yield)
'a
-> (stream 'yield)
'b
-> (stream 'yield)
'END
-> (stream 'yield)
'END
->

84

Đây là một ví dụ đơn giản:

def isPrimeNumber(n):
    print "isPrimeNumber({}) call".format(n)
    if n==1:
        return False
    for x in range(2,n):
        if n % x == 0:
            return False
    return True

def primes (n=1):
    while(True):
        print "loop step ---------------- {}".format(n)
        if isPrimeNumber(n): yield n
        n += 1

for n in primes():
    if n> 10:break
    print "wiriting result {}".format(n)

Đầu ra:

loop step ---------------- 1
isPrimeNumber(1) call
loop step ---------------- 2
isPrimeNumber(2) call
loop step ---------------- 3
isPrimeNumber(3) call
wiriting result 3
loop step ---------------- 4
isPrimeNumber(4) call
loop step ---------------- 5
isPrimeNumber(5) call
wiriting result 5
loop step ---------------- 6
isPrimeNumber(6) call
loop step ---------------- 7
isPrimeNumber(7) call
wiriting result 7
loop step ---------------- 8
isPrimeNumber(8) call
loop step ---------------- 9
isPrimeNumber(9) call
loop step ---------------- 10
isPrimeNumber(10) call
loop step ---------------- 11
isPrimeNumber(11) call

Tôi không phải là nhà phát triển Python, nhưng theo tôi, tôi yieldgiữ vị trí của luồng chương trình và vòng lặp tiếp theo bắt đầu từ vị trí "suất". Có vẻ như nó đang đợi ở vị trí đó, và ngay trước đó, trả lại một giá trị bên ngoài, và lần sau tiếp tục hoạt động.

Nó có vẻ là một khả năng thú vị và tốt đẹp: D


Bạn nói đúng. Nhưng ảnh hưởng của dòng chảy là gì để thấy hành vi của "năng suất"? Tôi có thể thay đổi thuật toán trong tên của toán học. Nó sẽ giúp để có được đánh giá khác nhau về "năng suất"?
Engin OZTURK

68

Đây là một hình ảnh tinh thần của những gì yieldlàm.

Tôi thích nghĩ về một chủ đề như có một ngăn xếp (ngay cả khi nó không được thực hiện theo cách đó).

Khi một hàm bình thường được gọi, nó đặt các biến cục bộ của nó trên ngăn xếp, thực hiện một số tính toán, sau đó xóa ngăn xếp và trả về. Các giá trị của các biến cục bộ của nó không bao giờ được nhìn thấy nữa.

Với một yieldhàm, khi mã của nó bắt đầu chạy (tức là sau khi hàm được gọi, trả về một đối tượng trình tạo, next()sau đó phương thức được gọi), nó tương tự đặt các biến cục bộ của nó vào ngăn xếp và tính toán một lúc. Nhưng sau đó, khi nó chạm vào yieldcâu lệnh, trước khi xóa một phần của ngăn xếp và quay trở lại, nó sẽ chụp nhanh các biến cục bộ của nó và lưu trữ chúng trong đối tượng trình tạo. Nó cũng ghi lại vị trí hiện tại trong mã của nó (tức là yieldcâu lệnh cụ thể ).

Vì vậy, đây là một loại chức năng đóng băng mà máy phát điện đang treo.

Khi next()được gọi sau đó, nó lấy đồ đạc của hàm lên ngăn xếp và hoạt hình lại. Hàm tiếp tục tính toán từ nơi nó rời đi, không biết thực tế là nó vừa trải qua một thời gian vĩnh cửu trong kho lạnh.

So sánh các ví dụ sau:

def normalFunction():
    return
    if False:
        pass

def yielderFunction():
    return
    if False:
        yield 12

Khi chúng ta gọi hàm thứ hai, nó hoạt động rất khác với hàm thứ nhất. Các yieldtuyên bố có thể không thể truy cập, nhưng nếu đó là bất cứ nơi nào có mặt, nó thay đổi bản chất của những gì chúng ta đang làm việc với.

>>> yielderFunction()
<generator object yielderFunction at 0x07742D28>

Gọi yielderFunction()không chạy mã của nó, nhưng làm cho một trình tạo ra khỏi mã. (Có lẽ nên đặt tên những thứ như vậy với yieldertiền tố để dễ đọc.)

>>> gen = yielderFunction()
>>> dir(gen)
['__class__',
 ...
 '__iter__',    #Returns gen itself, to make it work uniformly with containers
 ...            #when given to a for loop. (Containers return an iterator instead.)
 'close',
 'gi_code',
 'gi_frame',
 'gi_running',
 'next',        #The method that runs the function's body.
 'send',
 'throw']

Các lĩnh vực gi_codegi_framelà nơi lưu trữ trạng thái đông lạnh. Khám phá chúng với dir(..), chúng tôi có thể xác nhận rằng mô hình tinh thần của chúng tôi ở trên là đáng tin cậy.


59

Giống như mọi câu trả lời gợi ý, yieldđược sử dụng để tạo một trình tạo trình tự. Nó được sử dụng để tạo ra một số chuỗi động. Ví dụ: trong khi đọc một dòng tệp theo dòng trên mạng, bạn có thể sử dụng yieldchức năng như sau:

def getNextLines():
   while con.isOpen():
       yield con.read()

Bạn có thể sử dụng nó trong mã của bạn như sau:

for line in getNextLines():
    doSomeThing(line)

Kiểm soát thực thi Chuyển gotcha

Điều khiển thực thi sẽ được chuyển từ getNextLines () sang forvòng lặp khi năng suất được thực thi. Do đó, mỗi khi getNextLines () được gọi, việc thực thi bắt đầu từ điểm tạm dừng lần trước.

Vì vậy, trong ngắn hạn, một chức năng với mã sau đây

def simpleYield():
    yield "first time"
    yield "second time"
    yield "third time"
    yield "Now some useful value {}".format(12)

for i in simpleYield():
    print i

sẽ in

"first time"
"second time"
"third time"
"Now some useful value 12"

59

Một ví dụ dễ hiểu để hiểu nó là gì: yield

def f123():
    for _ in range(4):
        yield 1
        yield 2


for i in f123():
    print (i)

Đầu ra là:

1 2 1 2 1 2 1 2

5
Bạn có chắc chắn về đầu ra đó? sẽ không được in trên một dòng nếu bạn chạy câu lệnh in đó bằng cách sử dụng print(i, end=' ')?
Mặt

@ user9074332, Bạn nói đúng, nhưng nó được viết trên một dòng để tạo điều kiện cho sự hiểu biết
Gavriel Cohen

57

(My dưới đây câu trả lời chỉ nói từ quan điểm của việc sử dụng máy phát điện Python, không phải là thực hiện cơ bản của cơ chế máy phát điện , trong đó bao gồm một số thủ thuật của ngăn xếp và thao tác heap.)

Khi yieldđược sử dụng thay vì returntrong hàm python, hàm đó được biến thành một thứ đặc biệt gọi là generator function. Hàm đó sẽ trả về một đối tượng generatorkiểu. Các yieldtừ khóa là một lá cờ để thông báo cho trình biên dịch python để điều trị chức năng như vậy đặc biệt. Các hàm thông thường sẽ chấm dứt khi một số giá trị được trả về từ nó. Nhưng với sự trợ giúp của trình biên dịch, hàm tạo có thể được coi là có thể phục hồi . Đó là, bối cảnh thực hiện sẽ được khôi phục và việc thực hiện sẽ tiếp tục từ lần chạy cuối cùng. Cho đến khi bạn gọi return return một cách rõ ràng, điều này sẽ đưa ra một StopIterationngoại lệ (cũng là một phần của giao thức iterator) hoặc đến cuối hàm. Tôi thấy rất nhiều tài liệu tham khảo về generatornhưng điều này mộttừ functional programming perspectivelà dễ tiêu hóa nhất.

(Bây giờ tôi muốn nói về lý do đằng sau generatoriteratordựa trên sự hiểu biết của riêng tôi. Tôi hy vọng điều này có thể giúp bạn nắm bắt được động lực thiết yếu của trình lặp và trình tạo.

Theo tôi hiểu, khi chúng tôi muốn xử lý một loạt dữ liệu, trước tiên chúng tôi thường lưu trữ dữ liệu ở đâu đó và sau đó xử lý từng dữ liệu một. Nhưng cách tiếp cận ngây thơ này là có vấn đề. Nếu khối lượng dữ liệu rất lớn, sẽ rất tốn kém để lưu trữ toàn bộ chúng trước đó. Vì vậy, thay vì lưu trữ datatrực tiếp, tại sao không lưu trữ một số loại metadatagián tiếp, tức làthe logic how the data is computed .

Có 2 cách tiếp cận để bọc siêu dữ liệu đó.

  1. Cách tiếp cận OO, chúng tôi gói siêu dữ liệu as a class. Đây là cái gọi là iteratorngười thực hiện giao thức iterator (tức là __next__()__iter__()các phương thức). Đây cũng là mẫu thiết kế lặp thường thấy .
  2. Cách tiếp cận chức năng, chúng tôi bọc siêu dữ liệu as a function. Đây là cái gọi là generator function. Nhưng dưới mui xe, iterator generator objectvẫn trả về IS-Avì nó cũng thực hiện giao thức iterator.

Dù bằng cách nào, một trình vòng lặp được tạo, tức là một số đối tượng có thể cung cấp cho bạn dữ liệu bạn muốn. Cách tiếp cận OO có thể hơi phức tạp. Dù sao, cái nào để sử dụng là tùy thuộc vào bạn.


54

Tóm lại, yieldcâu lệnh biến đổi chức năng của bạn thành một nhà máy sản xuất một đối tượng đặc biệt gọi là đối tượng generatorbao quanh cơ thể của chức năng ban đầu của bạn. Khi generatorđược lặp lại, nó sẽ thực thi chức năng của bạn cho đến khi nó đạt đến lần tiếp theo yieldsau đó tạm dừng thực thi và đánh giá giá trị được truyền vào yield. Nó lặp lại quá trình này trên mỗi lần lặp cho đến khi đường dẫn thực thi thoát khỏi hàm. Ví dụ,

def simple_generator():
    yield 'one'
    yield 'two'
    yield 'three'

for i in simple_generator():
    print i

đầu ra đơn giản

one
two
three

Sức mạnh đến từ việc sử dụng trình tạo với một vòng lặp tính toán một chuỗi, trình tạo thực hiện việc dừng vòng lặp mỗi lần để 'sinh ra' kết quả tính toán tiếp theo, theo cách này, nó sẽ tính toán một danh sách khi đang bay, lợi ích là bộ nhớ lưu cho các tính toán đặc biệt lớn

Giả sử bạn muốn tạo một rangehàm riêng tạo ra một dãy số có thể lặp lại, bạn có thể làm như vậy,

def myRangeNaive(i):
    n = 0
    range = []
    while n < i:
        range.append(n)
        n = n + 1
    return range

và sử dụng nó như thế này;

for i in myRangeNaive(10):
    print i

Nhưng điều này không hiệu quả vì

  • Bạn tạo một mảng mà bạn chỉ sử dụng một lần (điều này làm lãng phí bộ nhớ)
  • Mã này thực sự lặp lại trên mảng đó hai lần! :

May mắn thay, Guido và nhóm của ông đã đủ hào phóng để phát triển máy phát điện để chúng tôi có thể làm điều này;

def myRangeSmart(i):
    n = 0
    while n < i:
       yield n
       n = n + 1
    return

for i in myRangeSmart(10):
    print i

Bây giờ, sau mỗi lần lặp, một hàm trên trình tạo được gọi là next()thực thi hàm cho đến khi nó đạt đến câu lệnh 'suất' trong đó nó dừng lại và 'mang lại' giá trị hoặc đến cuối hàm. Trong trường hợp này trong cuộc gọi đầu tiên, next()thực hiện đến câu lệnh lãi suất và năng suất 'n', trong cuộc gọi tiếp theo, nó sẽ thực hiện câu lệnh tăng, nhảy trở lại 'while', đánh giá nó và nếu đúng, nó sẽ dừng và mang lại 'n' một lần nữa, nó sẽ tiếp tục theo cách đó cho đến khi điều kiện while trả về false và trình tạo nhảy đến cuối hàm.


53

Năng suất là một đối tượng

A returntrong một hàm sẽ trả về một giá trị duy nhất.

Nếu bạn muốn một hàm trả về một tập hợp giá trị khổng lồ , hãy sử dụng yield.

Quan trọng hơn, yieldlà một rào cản .

giống như rào cản trong ngôn ngữ CUDA, nó sẽ không chuyển điều khiển cho đến khi hoàn thành.

Đó là, nó sẽ chạy mã trong chức năng của bạn từ đầu cho đến khi nó chạm yield. Sau đó, nó sẽ trả về giá trị đầu tiên của vòng lặp.

Sau đó, mọi cuộc gọi khác sẽ chạy vòng lặp bạn đã viết trong hàm thêm một lần nữa, trả về giá trị tiếp theo cho đến khi không có giá trị nào trả về.


52

Nhiều người sử dụng returnhơn là yield, nhưng trong một số trường hợp yieldcó thể hiệu quả hơn và dễ dàng hơn để làm việc với.

Đây là một ví dụ yieldchắc chắn là tốt nhất cho:

trả về (trong chức năng)

import random

def return_dates():
    dates = [] # With 'return' you need to create a list then return it
    for i in range(5):
        date = random.choice(["1st", "2nd", "3rd", "4th", "5th", "6th", "7th", "8th", "9th", "10th"])
        dates.append(date)
    return dates

năng suất (trong chức năng)

def yield_dates():
    for i in range(5):
        date = random.choice(["1st", "2nd", "3rd", "4th", "5th", "6th", "7th", "8th", "9th", "10th"])
        yield date # 'yield' makes a generator automatically which works
                   # in a similar way. This is much more efficient.

Chức năng gọi

dates_list = return_dates()
print(dates_list)
for i in dates_list:
    print(i)

dates_generator = yield_dates()
print(dates_generator)
for i in dates_generator:
    print(i)

Cả hai chức năng đều làm cùng một việc, nhưng yieldsử dụng ba dòng thay vì năm và có một biến ít hơn để lo lắng.

Đây là kết quả từ mã:

Đầu ra

Như bạn có thể thấy cả hai chức năng làm cùng một điều. Sự khác biệt duy nhất là return_dates()đưa ra một danh sách và yield_dates()đưa ra một trình tạo.

Một ví dụ thực tế sẽ giống như đọc từng dòng tệp hoặc nếu bạn chỉ muốn tạo một trình tạo.


43

yieldgiống như một phần tử trả về cho một hàm. Sự khác biệt là, yieldphần tử biến một hàm thành một trình tạo. Một trình tạo hoạt động giống như một hàm cho đến khi một cái gì đó được 'mang lại'. Trình tạo dừng lại cho đến khi nó được gọi tiếp theo và tiếp tục từ chính xác điểm giống như khi nó bắt đầu. Bạn có thể nhận được một chuỗi tất cả các giá trị 'mang lại' trong một, bằng cách gọi list(generator()).


41

Các yieldtừ khóa chỉ đơn giản là thu thập kết quả trả về. Nghĩ yieldnhưreturn +=


36

Đây là một yieldcách tiếp cận dựa trên đơn giản , để tính toán chuỗi Wikipedia, giải thích:

def fib(limit=50):
    a, b = 0, 1
    for i in range(limit):
       yield b
       a, b = b, a+b

Khi bạn nhập cái này vào REPL của bạn và sau đó thử và gọi nó, bạn sẽ nhận được một kết quả bí ẩn:

>>> fib()
<generator object fib at 0x7fa38394e3b8>

Điều này là do sự hiện diện của yieldtín hiệu tới Python mà bạn muốn tạo một trình tạo , nghĩa là một đối tượng tạo ra các giá trị theo yêu cầu.

Vì vậy, làm thế nào để bạn tạo ra các giá trị này? Điều này có thể được thực hiện trực tiếp bằng cách sử dụng hàm tích hợp nexthoặc gián tiếp bằng cách cung cấp cho nó một cấu trúc tiêu thụ các giá trị.

Sử dụng next()hàm tích hợp, bạn trực tiếp gọi .next/ __next__, buộc trình tạo tạo ra một giá trị:

>>> g = fib()
>>> next(g)
1
>>> next(g)
1
>>> next(g)
2
>>> next(g)
3
>>> next(g)
5

Một cách gián tiếp, nếu bạn cung cấp fibcho một forvòng lặp, trình listkhởi tạo, trình tuplekhởi tạo hoặc bất kỳ thứ gì khác mong đợi một đối tượng tạo / tạo giá trị, bạn sẽ "tiêu thụ" trình tạo cho đến khi không có thêm giá trị nào được tạo ra (và nó trả về) :

results = []
for i in fib(30):       # consumes fib
    results.append(i) 
# can also be accomplished with
results = list(fib(30)) # consumes fib

Tương tự, với trình tuplekhởi tạo:

>>> tuple(fib(5))       # consumes fib
(1, 1, 2, 3, 5)

Một máy phát điện khác với một chức năng theo nghĩa là nó lười biếng. Nó thực hiện điều này bằng cách duy trì trạng thái cục bộ và cho phép bạn tiếp tục bất cứ khi nào bạn cần.

Khi bạn lần đầu tiên gọi fibbằng cách gọi nó:

f = fib()

Python biên dịch hàm, gặp yieldtừ khóa và chỉ cần trả về một đối tượng trình tạo lại cho bạn. Có vẻ không hữu ích lắm.

Sau đó, khi bạn yêu cầu nó tạo ra giá trị đầu tiên, trực tiếp hoặc gián tiếp, nó sẽ thực thi tất cả các câu lệnh mà nó tìm thấy, cho đến khi gặp a yield, nó sẽ trả về giá trị bạn cung cấp yieldvà tạm dừng. Để biết ví dụ minh họa rõ hơn cho điều này, hãy sử dụng một số printcuộc gọi (thay thế bằng print "text"nếu trên Python 2):

def yielder(value):
    """ This is an infinite generator. Only use next on it """ 
    while 1:
        print("I'm going to generate the value for you")
        print("Then I'll pause for a while")
        yield value
        print("Let's go through it again.")

Bây giờ, nhập REPL:

>>> gen = yielder("Hello, yield!")

bây giờ bạn có một đối tượng trình tạo đang chờ lệnh để nó tạo giá trị. Sử dụng nextvà xem những gì được in:

>>> next(gen) # runs until it finds a yield
I'm going to generate the value for you
Then I'll pause for a while
'Hello, yield!'

Các kết quả không được trích dẫn là những gì được in. Kết quả được trích dẫn là những gì được trả lại từ yield. Gọi nextlại ngay bây giờ:

>>> next(gen) # continues from yield and runs again
Let's go through it again.
I'm going to generate the value for you
Then I'll pause for a while
'Hello, yield!'

Máy phát điện nhớ nó đã bị tạm dừng yield valuevà tiếp tục từ đó. Thông báo tiếp theo được in và tìm kiếm cho yieldcâu lệnh tạm dừng tại đó được thực hiện lại (do whilevòng lặp).

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.