2013-05-09 36 views
14

Tại sao trong ví dụ chức năng chấm dứt:Hiệu suất bắt ngoại lệ StopIteration như thế nào?

def func(iterable): 
    while True: 
     val = next(iterable) 
     yield val 

nhưng nếu tôi cất cánh chức năng tuyên bố sản lượng sẽ tăng StopIteration ngoại lệ?

EDIT: Xin lỗi vì đã gây hiểu nhầm cho các bạn. Tôi biết máy phát điện là gì và cách sử dụng chúng. Tất nhiên khi tôi nói chức năng chấm dứt tôi không có nghĩa là mong muốn đánh giá chức năng. Tôi chỉ ngụ ý rằng khi tôi sử dụng chức năng để sản xuất máy phát điện:

gen = func(iterable) 

trong trường hợp func nó hoạt động và trả về máy phát điện tương tự, nhưng trong trường hợp của Func2:

def func2(iterable): 
    while True: 
     val = next(iterable) 

nó làm tăng StopIteration thay của Không có gì là trả lại hoặc vòng lặp vô hạn.

Hãy để tôi cụ thể hơn. Có một chức năng tee trong itertools tương đương với:

def tee(iterable, n=2): 
    it = iter(iterable) 
    deques = [collections.deque() for i in range(n)] 
    def gen(mydeque): 
     while True: 
      if not mydeque:    # when the local deque is empty 
       newval = next(it)  # fetch a new value and 
       for d in deques:  # load it to all the deques 
        d.append(newval) 
      yield mydeque.popleft() 
    return tuple(gen(d) for d in deques) 

có, trên thực tế, một số kỳ diệu, bởi vì hàm lồng nhau gen có vòng lặp vô hạn mà không báo cáo nghỉ ngơi. Chức năng gen chấm dứt do StopIteration ngoại lệ khi không có mục nào trong . Nhưng nó chấm dứt chính xác (không nêu ra ngoại lệ), tức là chỉ dừng vòng lặp. Vì vậy, câu hỏi là: ở đâu là StopIteration được xử lý?

+0

Bạn gọi điện thoại này như thế nào? –

Trả lời

24

Để trả lời câu hỏi của bạn về nơi StopIteration bị phát hiện trong máy phát điện gen được tạo bên trong itertools.tee: không. Đó là tùy thuộc vào người tiêu dùng của kết quả tee để bắt ngoại lệ khi họ lặp lại.

Trước hết, điều quan trọng cần lưu ý là chức năng máy phát (là bất kỳ hàm nào có câu lệnh yield trong đó, ở bất kỳ đâu) về cơ bản khác với chức năng bình thường. Thay vì chạy mã của hàm khi nó được gọi, thay vào đó, bạn sẽ chỉ nhận được một đối tượng generator khi bạn gọi hàm. Chỉ khi bạn lặp qua máy phát thì bạn sẽ chạy mã.

Chức năng máy phát sẽ không bao giờ kết thúc lặp mà không tăng StopIteration (trừ khi nó tăng một số ngoại lệ khác thay thế). StopIteration là tín hiệu từ máy phát điện mà nó được thực hiện, và nó không phải là tùy chọn. Nếu bạn đạt được tuyên bố return hoặc kết thúc mã của hàm của trình tạo không cần nâng cấp bất cứ điều gì, Python sẽ tăng StopIteration cho bạn!

Điều này khác với các chức năng thông thường, trả về None nếu chúng kết thúc mà không trả lại bất kỳ điều gì khác. Nó liên kết với các cách khác nhau mà máy phát điện hoạt động, như tôi đã mô tả ở trên.

Dưới đây là một chức năng máy phát điện ví dụ mà sẽ làm cho nó dễ dàng để xem làm thế nào StopIteration được nêu ra:

def simple_generator(): 
    yield "foo" 
    yield "bar" 
    # StopIteration will be raised here automatically 

Đây là những gì xảy ra khi bạn tiêu thụ nó:

>>> g = simple_generator() 
>>> next(g) 
'foo' 
>>> next(g) 
'bar' 
>>> next(g) 
Traceback (most recent call last): 
    File "<pyshell#6>", line 1, in <module> 
    next(g) 
StopIteration 

Calling simple_generator luôn trả về một đối tượng generator ngay lập tức (không chạy bất kỳ mã nào trong hàm). Mỗi cuộc gọi của next trên đối tượng máy phát sẽ chạy mã cho đến câu lệnh yield tiếp theo và trả về giá trị được tạo. Nếu không có thêm để nhận được, StopIteration được nâng lên.

Bây giờ, thông thường bạn không thấy StopIteration ngoại lệ. Lý do cho điều này là bạn thường tiêu thụ máy phát điện bên trong vòng for. Một tuyên bố for sẽ tự động gọi next hơn và hơn cho đến khi StopIteration được nâng lên. Nó sẽ bắt và ngăn chặn các ngoại lệ StopIteration cho bạn, vì vậy bạn không cần phải lộn xộn xung quanh với try/except khối để đối phó với nó.

Một vòng lặp for như for item in iterable: do_suff(item) là gần như chính xác tương đương với while vòng lặp này (sự khác biệt duy nhất là một thực for không cần một biến tạm thời để giữ iterator):

iterator = iter(iterable) 
try: 
    while True: 
     item = next(iterator) 
     do_stuff(item) 
except StopIteration: 
    pass 
finally: 
    del iterator 

Chức năng gen máy phát điện bạn cho thấy ở trên cùng là một ngoại lệ. Nó sử dụng ngoại lệ StopIteration được tạo bởi trình vòng lặp, nó tiêu thụ như là tín hiệu riêng của nó mà nó được thực hiện đang được lặp lại. Đó là, thay vì bắt các StopIteration và sau đó phá vỡ ra khỏi vòng lặp, nó chỉ đơn giản là cho phép ngoại lệ đi uncaught (có lẽ là bị bắt bởi một số mã cấp cao hơn).

Không liên quan đến câu hỏi chính, có một điều khác tôi muốn chỉ ra. Trong mã của bạn, bạn đang gọi số next trên một biến có tên là iterable. Nếu bạn lấy tên đó làm tài liệu cho loại đối tượng bạn sẽ nhận được, điều này không nhất thiết phải an toàn.

next là một phần của iterator giao thức, không phải là iterable (hoặc container) giao thức. Nó có thể làm việc cho một số loại vòng lặp (chẳng hạn như các tệp và trình tạo, vì các kiểu đó là các trình vòng lặp riêng của chúng), nhưng nó sẽ thất bại đối với các vòng lặp khác, chẳng hạn như các bộ và danh sách. Cách tiếp cận đúng hơn là gọi iter trên giá trị iterable của bạn, sau đó gọi next trên trình lặp mà bạn nhận được. (Hoặc chỉ cần sử dụng for vòng, mà gọi cả iternext cho bạn vào những thời điểm thích hợp!)

Edit: tôi chỉ tìm thấy câu trả lời của riêng tôi trong một tìm kiếm Google cho một câu hỏi liên quan, và tôi nghĩ rằng tôi muốn cập nhật đến thời điểm câu trả lời ở trên sẽ không hoàn toàn đúng trong các phiên bản Python trong tương lai. PEP 479 làm cho nó trở thành lỗi khi cho phép StopIteration bong bóng lên không bị bắt do một chức năng của máy phát điện. Nếu điều đó xảy ra, Python sẽ biến nó thành một ngoại lệ RuntimeError thay thế.

Điều này có nghĩa là mã như ví dụ trong itertools sử dụng StopIteration để thoát khỏi chức năng của trình tạo sẽ cần được sửa đổi. Thông thường, bạn sẽ cần phải nắm bắt ngoại lệ với try/except và sau đó thực hiện return.

Vì đây là sự thay đổi không tương thích ngược, nó đang được phân đoạn dần dần. Trong Python 3.5, tất cả mã sẽ hoạt động như trước theo mặc định, nhưng bạn có thể nhận được hành vi mới với from __future__ import generator_stop. Trong Python 3.6, mã sẽ vẫn hoạt động, nhưng nó sẽ đưa ra một cảnh báo. Trong Python 3.7, hành vi mới sẽ áp dụng mọi lúc.

+0

Vì vậy, StopIteration được tiêu thụ theo định nghĩa hàm (hoặc cấu trúc máy phát tương đương)? Tôi chỉ muốn tìm ra nếu chúng ta sử dụng bên ngoài bên ngoài của cơ quan chức năng nó sẽ tăng ngoại lệ, nhưng nếu chúng ta sử dụng chức năng bên trong nó sẽ chấm dứt bình thường. –

+1

@BranAlgue Không, định nghĩa hàm sẽ không tiêu thụ ngoại lệ. Cũng giống như bất kỳ ngoại lệ nào khác, một 'StopIteration' sẽ đi lên ngăn xếp cuộc gọi cho đến khi nó bị chặn bởi một khối' try'/'catch' rõ ràng, hoặc bởi một ngầm định bên trong một vòng lặp' for'. Tôi nghĩ rằng điều bạn đang thiếu là 'StopIteration' không phải là một vấn đề trong một chức năng máy phát điện. Bạn dự kiến ​​sẽ tăng một khi bạn không còn gì để sinh lời. Bạn có thể làm điều đó một cách rõ ràng với 'raise StopIteration()', hoặc ngầm định bằng cách đi đến phần cuối của hàm - hoặc bạn có thể cho phép 'StopIteration' được tạo ra bởi một lời gọi tới' next' không bị bắt. – Blckknght

+0

Tôi hiểu điều đó. Tôi không hiểu tại sao 'StopIteration' không phải là một vấn đề bên trong chức năng máy phát điện. Yêu cầu rằng trình phát có ngầm xử lý ngoại lệ đúng không? –

2

Nếu không có yield, bạn lặp lại toàn bộ iterable mà không dừng lại để làm bất kỳ điều gì với val. Vòng lặp while không bắt được ngoại lệ StopIteration. Tương đương for vòng lặp sẽ là:

def func(iterable): 
    for val in iterable: 
     pass 

mà bắt StopIteration và chỉ đơn giản là thoát khỏi vòng lặp và do đó trở về từ hàm.

Bạn rõ ràng có thể bắt ngoại lệ:

def func(iterable): 
    while True: 
     try: 
      val = next(iterable) 
     except StopIteration: 
      break 
5

Khi một hàm chứa yield, gọi đó là không thực sự thực hiện bất cứ điều gì, nó chỉ tạo ra một đối tượng máy phát điện. Chỉ lặp lại đối tượng này sẽ thực thi mã. Vì vậy, tôi đoán là bạn chỉ đơn thuần gọi hàm, có nghĩa là hàm không tăng StopIterationnó không bao giờ được thực thi.

Với chức năng của mình, và một iterable:

def func(iterable): 
    while True: 
     val = next(iterable) 
     yield val 

iterable = iter([1, 2, 3]) 

Đây là cách sai lầm khi gọi nó là:

func(iterable) 

Đây là đúng cách:

for item in func(iterable): 
    # do something with item 

Bạn cũng có thể lưu máy phát điện trong một biến và gọi next() trên đó (hoặc lặp lại nó trong một số cách khác):

gen = func(iterable) 
print(next(gen)) # prints 1 
print(next(gen)) # prints 2 
print(next(gen)) # prints 3 
print(next(gen)) # StopIteration 

By the way, một cách tốt hơn để viết chức năng của bạn như sau:

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

Hoặc bằng Python 3.3 và sau này:

def func(iterable): 
    yield from iter(iterable) 

Tất nhiên, máy phát điện thực sự hiếm khi quá tầm thường. :-)

+0

Cẩn thận với ví dụ cuối cùng của bạn ở đó. 'list' không có phương thức' next' hoặc '__next__', ví dụ của bạn sẽ không hoạt động. Việc sửa chữa là dễ dàng mặc dù. 'gen = func (iter ([1,2,3]))' – mgilson

+0

Đúng, chỉ nhận ra rằng, cố định. :-) – kindall

0

yield không bắt được StopIteration. Những gì yield làm cho chức năng của bạn là nó gây ra nó để trở thành một chức năng máy phát điện chứ không phải là một chức năng thường xuyên. Do đó, đối tượng được trả về từ hàm gọi là một đối tượng có thể lặp lại (tính toán giá trị tiếp theo khi bạn yêu cầu nó với hàm next (được gọi ngầm bởi vòng lặp for)). Nếu bạn để lại câu lệnh yield trong số đó, thì python sẽ thực hiện toàn bộ vòng lặp while ngay lập tức, kết thúc quá trình cạn kiệt (nếu nó là hữu hạn) và tăng StopIteration ngay khi bạn gọi nó.

xem xét:

x = func(x for x in []) 
next(x) #raises StopIteration 

Một vòng lặp for bắt ngoại lệ - Đó là cách nó biết khi nào phải ngừng gọi next trên iterable bạn đưa nó.