năng suất trong khả năng hiểu danh sách và biểu thức trình tạo


76

Hành vi sau đây có vẻ khá phản trực giác với tôi (Python 3.4):

>>> [(yield i) for i in range(3)]
<generator object <listcomp> at 0x0245C148>
>>> list([(yield i) for i in range(3)])
[0, 1, 2]
>>> list((yield i) for i in range(3))
[0, None, 1, None, 2, None]

Các giá trị trung gian của dòng cuối cùng thực sự không phải luôn luôn None, chúng là bất cứ giá trị nào chúng ta sendđưa vào trình tạo, tương đương (tôi đoán) với trình tạo sau:

def f():
   for i in range(3):
      yield (yield i)

Tôi thấy buồn cười là ba dòng đó có tác dụng gì. Các tham chiếu nói rằng yieldchỉ được cho phép trong một định nghĩa hàm (mặc dù tôi chưa đọc nó sai và / hoặc có thể chỉ đơn giản là đã được sao chép từ các phiên bản cũ). Hai dòng đầu tiên tạo ra một SyntaxErrortrong Python 2.7, nhưng dòng thứ ba thì không.

Ngoài ra, nó có vẻ kỳ quặc

  • rằng khả năng hiểu danh sách trả về trình tạo chứ không phải danh sách
  • và biểu thức trình tạo được chuyển đổi thành danh sách và khả năng hiểu danh sách tương ứng chứa các giá trị khác nhau.

Ai đó có thể cung cấp thêm thông tin?

Câu trả lời:


75

Lưu ý : đây là một lỗi trong việc CPython xử lý các yieldbiểu thức hiểu và trình tạo, được sửa trong Python 3.8, với cảnh báo không dùng nữa trong Python 3.7. Xem báo cáo lỗi Python và các mục nhập Có gì mới cho Python 3.7Python 3.8 .

Biểu thức trình tạo, và tập hợp và hiểu chính tả được biên dịch thành các đối tượng hàm (trình tạo). Trong Python 3, các phần hiểu danh sách có cùng cách xử lý; về bản chất, tất cả chúng đều là một phạm vi lồng nhau mới.

Bạn có thể thấy điều này nếu bạn cố gắng tháo rời một biểu thức trình tạo:

>>> dis.dis(compile("(i for i in range(3))", '', 'exec'))
  1           0 LOAD_CONST               0 (<code object <genexpr> at 0x10f7530c0, file "", line 1>)
              3 LOAD_CONST               1 ('<genexpr>')
              6 MAKE_FUNCTION            0
              9 LOAD_NAME                0 (range)
             12 LOAD_CONST               2 (3)
             15 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             18 GET_ITER
             19 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             22 POP_TOP
             23 LOAD_CONST               3 (None)
             26 RETURN_VALUE
>>> dis.dis(compile("(i for i in range(3))", '', 'exec').co_consts[0])
  1           0 LOAD_FAST                0 (.0)
        >>    3 FOR_ITER                11 (to 17)
              6 STORE_FAST               1 (i)
              9 LOAD_FAST                1 (i)
             12 YIELD_VALUE
             13 POP_TOP
             14 JUMP_ABSOLUTE            3
        >>   17 LOAD_CONST               0 (None)
             20 RETURN_VALUE

Ở trên cho thấy rằng một biểu thức trình tạo được biên dịch thành một đối tượng mã, được tải dưới dạng một hàm ( MAKE_FUNCTIONtạo đối tượng hàm từ đối tượng mã). Các .co_consts[0]tài liệu tham khảo cho chúng ta thấy đối tượng mã được tạo cho các biểu hiện, và nó sử dụng YIELD_VALUEgiống như một chức năng máy phát điện sẽ.

Như vậy, yieldbiểu thức hoạt động trong ngữ cảnh đó, vì trình biên dịch coi đây là những hàm trong ngụy trang.

Đây là một lỗi; yieldkhông có vị trí trong các biểu thức này. Ngữ pháp Python trước Python 3.7 cho phép nó (đó là lý do tại sao mã có thể biên dịch được), nhưng yieldđặc tả biểu thức cho thấy rằng việc sử dụng yieldở đây không thực sự hoạt động:

Biểu thức sản lượng chỉ được sử dụng khi xác định hàm bộ tạo và do đó chỉ có thể được sử dụng trong phần thân của định nghĩa hàm.

Đây đã được xác nhận là một lỗi trong số 10544 . Giải pháp của lỗi là sử dụng yieldyield fromsẽ tăng a SyntaxErrortrong Python 3.8 ; trong Python 3.7, nó tăng mộtDeprecationWarning để đảm bảo mã ngừng sử dụng cấu trúc này. Bạn sẽ thấy cảnh báo tương tự trong Python 2.7.15 trở lên nếu bạn sử dụng công -3tắc dòng lệnh bật cảnh báo tương thích Python 3.

Cảnh báo 3.7.0b1 trông như thế này; chuyển cảnh báo thành lỗi sẽ cho bạn một SyntaxErrorngoại lệ, giống như bạn làm trong 3.8:

>>> [(yield i) for i in range(3)]
<stdin>:1: DeprecationWarning: 'yield' inside list comprehension
<generator object <listcomp> at 0x1092ec7c8>
>>> import warnings
>>> warnings.simplefilter('error')
>>> [(yield i) for i in range(3)]
  File "<stdin>", line 1
SyntaxError: 'yield' inside list comprehension

Sự khác biệt giữa cách thức hoạt động trong cách yieldhiểu danh sách và yieldtrong biểu thức trình tạo bắt nguồn từ sự khác biệt trong cách hai biểu thức này được triển khai. Trong Python 3, tính năng hiểu danh sách sử dụng LIST_APPENDcác lệnh gọi để thêm phần trên cùng của ngăn xếp vào danh sách đang được xây dựng, trong khi biểu thức trình tạo thay thế mang lại giá trị đó. Thêm vào (yield <expr>)chỉ thêm một YIELD_VALUEopcode khác vào:

>>> dis.dis(compile("[(yield i) for i in range(3)]", '', 'exec').co_consts[0])
  1           0 BUILD_LIST               0
              3 LOAD_FAST                0 (.0)
        >>    6 FOR_ITER                13 (to 22)
              9 STORE_FAST               1 (i)
             12 LOAD_FAST                1 (i)
             15 YIELD_VALUE
             16 LIST_APPEND              2
             19 JUMP_ABSOLUTE            6
        >>   22 RETURN_VALUE
>>> dis.dis(compile("((yield i) for i in range(3))", '', 'exec').co_consts[0])
  1           0 LOAD_FAST                0 (.0)
        >>    3 FOR_ITER                12 (to 18)
              6 STORE_FAST               1 (i)
              9 LOAD_FAST                1 (i)
             12 YIELD_VALUE
             13 YIELD_VALUE
             14 POP_TOP
             15 JUMP_ABSOLUTE            3
        >>   18 LOAD_CONST               0 (None)
             21 RETURN_VALUE

YIELD_VALUEopcode ở các chỉ số bytecode lần lượt là 15 và 12 là phụ, một con chim cu gáy trong tổ. Vì vậy, đối với trình tạo danh sách-hiểu-biến, bạn có 1 lợi suất tạo ra phần trên cùng của ngăn xếp mỗi lần (thay thế đầu của ngăn xếp bằng yieldgiá trị trả về) và đối với biến thể biểu thức trình tạo, bạn mang lại ở trên cùng của ngăn xếp ( số nguyên) và sau đó lại cho kết quả , nhưng bây giờ ngăn xếp chứa giá trị trả về của yieldvà bạn nhận được Nonelần thứ hai đó.

Sau đó, listđối với khả năng hiểu danh sách, đầu ra đối tượng dự định vẫn được trả về, nhưng Python 3 coi đây là trình tạo nên giá trị trả về thay vào đó được gắn với StopIterationngoại lệ dưới dạng valuethuộc tính:

>>> from itertools import islice
>>> listgen = [(yield i) for i in range(3)]
>>> list(islice(listgen, 3))  # avoid exhausting the generator
[0, 1, 2]
>>> try:
...     next(listgen)
... except StopIteration as si:
...     print(si.value)
... 
[None, None, None]

Các Noneđối tượng đó là giá trị trả về từ các yieldbiểu thức.

Và để nhắc lại điều này một lần nữa; vấn đề tương tự cũng áp dụng cho từ điển và thiết lập khả năng hiểu trong Python 2 và Python 3; trong Python 2, các yieldgiá trị trả về vẫn được thêm vào từ điển hoặc đối tượng tập hợp dự định và giá trị trả về được 'mang lại' sau cùng thay vì được đính kèm với StopIterationngoại lệ:

>>> list({(yield k): (yield v) for k, v in {'foo': 'bar', 'spam': 'eggs'}.items()})
['bar', 'foo', 'eggs', 'spam', {None: None}]
>>> list({(yield i) for i in range(3)})
[0, 1, 2, set([None])]

Lưu ý rằng theo đặc tả ngôn ngữ, yield-atomđược phép bên trong một biểu thức (bên trong một hàm trình tạo). Điều này có thể còn nhiều vấn đề hơn nếu yield-atombằng cách nào đó nó được thực hiện sai.
skyking

1
@skyking: đó là những gì tôi đang nói; ngữ pháp cho phép nó. Lỗi mà tôi đề cập đến là đang cố gắng sử dụng yield như một phần của biểu thức trình tạo bên trong hàm trình tạo , trong đó kỳ vọng là yieldáp dụng cho hàm trình tạo, không phải phạm vi lồng nhau của biểu thức trình tạo.
Martijn Pieters

Chà. Thực sự rất nhiều thông tin. Vì vậy, nếu tôi hiểu đúng, điều sau đã xảy ra: một hàm chứa cả hai yieldreturnnên, như được ghi lại, trở thành một hàm trình tạo có returngiá trị ed sẽ nằm trong StopIterationngoại lệ và mã bytecode cho một danh sách dễ hiểu với yieldngoại hình bên trong (mặc dù nó là không nhằm mục đích) giống như mã bytecode của một hàm như vậy.
zabolekar

@zabolekar: đại loại vậy; các bước tương tự như: trình biên dịch đi qua một danh sách hiểu được để xây dựng một đối tượng mã; trình biên dịch bắt gặp một yieldbiểu thức để đánh dấu đối tượng mã hiện tại là trình tạo. Thì đấy, chúng ta có một chức năng máy phát điện.
Martijn Pieters

1
@Chris_Rands có các thay đổi 2.7 khi bạn sử dụng các -3cảnh báo về khả năng tương thích.
Martijn Pieters
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.