Hàm trình tạo trống trong Python


97

Trong python, người ta có thể dễ dàng xác định một hàm trình lặp, bằng cách đặt từ khóa lợi nhuận vào nội dung của hàm, chẳng hạn như:

def gen():
    for i in range(100):
        yield i

Làm cách nào để xác định hàm trình tạo không mang lại giá trị (tạo giá trị 0), mã sau không hoạt động, vì python không thể biết rằng nó được cho là một trình tạo chứ không phải một hàm bình thường:

def empty():
    pass

Tôi có thể làm một cái gì đó như

def empty():
    if False:
        yield None

Nhưng điều đó sẽ rất xấu. Có cách nào hay để nhận ra một hàm vòng lặp trống không?

Câu trả lời:


132

Bạn có thể sử dụng returnmột lần trong máy phát điện; nó ngừng lặp lại mà không mang lại bất cứ điều gì và do đó cung cấp một giải pháp thay thế rõ ràng để cho phép hàm chạy ra khỏi phạm vi. Vì vậy, hãy sử dụng yieldđể biến hàm thành một trình tạo, nhưng đặt trước nó returnđể kết thúc trình tạo trước khi mang lại bất kỳ thứ gì.

>>> def f():
...     return
...     yield
... 
>>> list(f())
[]

Tôi không chắc nó tốt hơn nhiều so với những gì bạn có - nó chỉ thay thế một ifcâu lệnh no-op bằng một câu no-op yield. Nhưng nó mang tính thành ngữ hơn. Lưu ý rằng chỉ sử dụng yieldkhông hoạt động.

>>> def f():
...     yield
... 
>>> list(f())
[None]

Tại sao không chỉ sử dụng iter(())?

Câu hỏi này hỏi cụ thể về một chức năng tạo trống . Vì lý do đó, tôi coi đây là câu hỏi về tính nhất quán bên trong của cú pháp Python, chứ không phải là câu hỏi về cách tốt nhất để tạo một trình lặp trống nói chung.

Nếu câu hỏi thực sự là về cách tốt nhất để tạo một trình lặp trống, thì bạn có thể đồng ý với Zectbumo về việc sử dụng iter(())thay thế. Tuy nhiên, điều quan trọng là phải quan sát rằng iter(())nó không trả về một hàm! Nó trực tiếp trả về một giá trị có thể lặp lại trống. Giả sử bạn đang làm việc với một API mong đợi một hàm có thể gọi trả về một giá trị có thể lặp lại. Bạn sẽ phải làm điều gì đó như sau:

def empty():
    return iter(())

(Tín dụng sẽ thuộc về Unutbu vì đã đưa ra phiên bản chính xác đầu tiên của câu trả lời này.)

Bây giờ, bạn có thể thấy điều trên rõ ràng hơn, nhưng tôi có thể tưởng tượng những tình huống mà nó sẽ kém rõ ràng hơn. Hãy xem xét ví dụ này về một danh sách dài các định nghĩa hàm của trình tạo (tiếp theo):

def zeros():
    while True:
        yield 0

def ones():
    while True:
        yield 1

...

Ở cuối danh sách dài đó, tôi muốn xem một cái gì đó có một yieldtrong đó, như thế này:

def empty():
    return
    yield

hoặc, trong Python 3.3 trở lên (theo đề xuất của DSM ), điều này:

def empty():
    yield from ()

Sự hiện diện của yieldtừ khóa làm rõ ràng rằng đây chỉ là một hàm trình tạo khác, giống hệt như tất cả các hàm khác. Cần thêm một chút thời gian để thấy rằng iter(())phiên bản đang làm điều tương tự.

Đó là một sự khác biệt nhỏ, nhưng tôi thành thật nghĩ rằng các yieldhàm dựa trên cơ sở này dễ đọc và dễ bảo trì hơn.


Điều này thực sự tốt hơn so với là if False: yieldnhưng vẫn kinda khó hiểu đối với những người không biết mô hình này
Konstantin Weitz

1
Ew, cái gì đó sau khi trở về? Tôi mong đợi một cái gì đó như thế nào itertools.empty().
Grault

1
@Jesdisciple, tốt, returncó nghĩa là một cái gì đó khác nhau bên trong máy phát điện. Nó giống hơn break.
gửi

Tôi thích giải pháp này vì nó (tương đối) ngắn gọn và nó không làm thêm bất kỳ công việc nào như so sánh với False.
Pi Marillion,

Câu trả lời của Unutbu không phải là "hàm trình tạo đúng" như bạn đã đề cập vì nó trả về một trình lặp.
Zectbumo

72
iter(())

Bạn không cần máy phát điện. Nào mọi người!


3
Tôi chắc chắn thích câu trả lời này nhất. Nó nhanh chóng, dễ viết, nhanh chóng trong thực thi và hấp dẫn hơn đối với tôi hơn iter([])là thực tế đơn giản ()là một hằng số trong khi []có thể khởi tạo một đối tượng danh sách mới trong bộ nhớ mỗi khi nó được gọi.
Mumbleskates

Đối với tôi, đây có vẻ là giải pháp tốt nhất.
Matthias CM Troffaes

2
Làm việc trở lại thông qua chuỗi này, tôi cảm thấy bắt buộc phải chỉ ra rằng nếu bạn muốn có một trình thay thế thực sự cho hàm trình tạo, bạn phải viết một cái gì đó như empty = lambda: iter(())hoặc def empty(): return iter(()).
gửi

Nếu bạn phải có một máy phát điện sau đó bạn cũng có thể sử dụng (_ _ cho trong ()) như những người khác đã gợi ý
Zectbumo

1
@Zectbumo, đó vẫn chưa phải là chức năng của trình tạo . Nó chỉ là một máy phát điện. Một hàm trình tạo trả về một trình tạo mới mỗi khi nó được gọi.
gửi

50

Python 3.3 (bởi vì tôi đang thích yield fromvà bởi vì @senderle đã đánh cắp suy nghĩ đầu tiên của tôi):

>>> def f():
...     yield from ()
... 
>>> list(f())
[]

Nhưng tôi phải thừa nhận rằng, tôi đang gặp khó khăn trong việc tìm ra một trường hợp sử dụng cho cái này iter([])hoặc (x)range(0)sẽ không hoạt động tốt như nhau.


Tôi thực sự thích cú pháp này. sản lượng từ là tuyệt vời!
Konstantin Weitz

2
Tôi nghĩ điều này dễ đọc hơn đối với một người mới làm quen hơn là return; yieldhoặc if False: yield None.
abarnert

1
Đây là giải pháp thanh lịch nhất
Maksym Ganenko

"Nhưng tôi phải thừa nhận rằng, tôi đang gặp khó khăn trong việc tìm ra một trường hợp sử dụng cho cái này iter([])hoặc (x)range(0)sẽ không hoạt động tốt như nhau." -> Không chắc chắn (x)range(0)là gì , nhưng một ca sử dụng có thể là một phương thức có nghĩa là được ghi đè với trình tạo hoàn chỉnh trong một số lớp kế thừa. Vì mục đích nhất quán, bạn muốn ngay cả cái cơ sở, mà những cái khác kế thừa, trả về một trình tạo giống như những người ghi đè nó.
Vedran Šego

18

Một lựa chọn khác là:

(_ for _ in ())

3
Tôi cũng thích điều này. Tôi có thể đề xuất một bộ trống thay vì một danh sách trống không? Bằng cách đó bạn sẽ có được gấp liên tục .
gửi

1
Cảm ơn. Tôi không biết về điều đó.
Ben Reynwar

4

Nó phải là một chức năng máy phát điện? Nếu không, làm thế nào về

def f():
    return iter([])

3

Cách "tiêu chuẩn" để tạo một trình lặp trống có vẻ là nó ([]). Tôi đã đề xuất đặt [] đối số mặc định cho iter (); điều này đã bị từ chối với các lập luận xác đáng, xem http://bugs.python.org/issue25215 - Jurjen


1

Giống như @senderle đã nói, hãy sử dụng cái này:

def empty():
    return
    yield

Tôi viết câu trả lời này chủ yếu để chia sẻ một lý do khác cho nó.

Một lý do để chọn giải pháp này hơn các giải pháp khác là nó tối ưu về mặt thông dịch viên.

>>> import dis
>>> def empty_yield_from():
...     yield from ()
... 
>>> def empty_iter():
...     return iter(())
... 
>>> def empty_return():
...     return
...     yield
...
>>> def noop():
...     pass
...
>>> dis.dis(empty_yield_from)
  2           0 LOAD_CONST               1 (())
              2 GET_YIELD_FROM_ITER
              4 LOAD_CONST               0 (None)
              6 YIELD_FROM
              8 POP_TOP
             10 LOAD_CONST               0 (None)
             12 RETURN_VALUE
>>> dis.dis(empty_iter)
  2           0 LOAD_GLOBAL              0 (iter)
              2 LOAD_CONST               1 (())
              4 CALL_FUNCTION            1
              6 RETURN_VALUE
>>> dis.dis(empty_return)
  2           0 LOAD_CONST               0 (None)
              2 RETURN_VALUE
>>> dis.dis(noop)
  2           0 LOAD_CONST               0 (None)
              2 RETURN_VALUE

Như chúng ta có thể thấy, hàm empty_returncó mã bytecode giống hệt như một hàm rỗng thông thường; phần còn lại thực hiện một số thao tác khác mà không thay đổi hành vi. Sự khác biệt duy nhất giữa empty_returnnooplà trước đây có cờ trình tạo được đặt:

>>> dis.show_code(noop)
Name:              noop
Filename:          <stdin>
Argument count:    0
Positional-only arguments: 0
Kw-only arguments: 0
Number of locals:  0
Stack size:        1
Flags:             OPTIMIZED, NEWLOCALS, NOFREE
Constants:
   0: None
>>> dis.show_code(empty_return)
Name:              empty_return
Filename:          <stdin>
Argument count:    0
Positional-only arguments: 0
Kw-only arguments: 0
Number of locals:  0
Stack size:        1
Flags:             OPTIMIZED, NEWLOCALS, GENERATOR, NOFREE
Constants:
   0: None

Tất nhiên, sức mạnh của đối số này phụ thuộc rất nhiều vào việc triển khai cụ thể của Python đang được sử dụng; một trình thông dịch thay thế đủ thông minh có thể nhận thấy rằng các hoạt động khác không hữu ích và tối ưu hóa chúng. Tuy nhiên, ngay cả khi những tối ưu hóa như vậy có mặt, chúng yêu cầu trình thông dịch dành thời gian để thực hiện chúng và để bảo vệ khỏi các giả định tối ưu hóa bị phá vỡ, chẳng hạn như mã iterđịnh danh ở phạm vi toàn cầu bị phục hồi sang một thứ khác (mặc dù điều đó rất có thể chỉ ra lỗi nếu nó thực tế đã xảy ra). Trong trường hợp empty_returnchỉ đơn giản là không có gì để tối ưu hóa, vì vậy ngay cả những CPython tương đối ngây thơ cũng sẽ không lãng phí thời gian cho bất kỳ hoạt động giả mạo nào.


0
generator = (item for item in [])
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.