Có một cơ sở ngôn ngữ máy phát điện như 'suất' là một ý tưởng tốt?


9

PHP, C #, Python và có thể một vài ngôn ngữ khác có một yieldtừ khóa được sử dụng để tạo các hàm tạo.

Trong PHP: http://php.net/manual/en/lingu.generators.syntax.php

Trong Python: https://www.pythoncentral.io/python-generators-and-yield-keyword/

Trong C #: https://docs.microsoft.com/en-us/dotnet/csharp/lingu-reference/keywords/yield

Tôi lo ngại rằng với tư cách là một tính năng / cơ sở ngôn ngữ, yieldphá vỡ một số quy ước. Một trong số đó là những gì tôi muốn nói đến là "sự chắc chắn". Đó là một phương thức trả về một kết quả khác nhau mỗi khi bạn gọi nó. Với một hàm không tạo thông thường, bạn có thể gọi nó và nếu nó được cung cấp cùng một đầu vào, nó sẽ trả về cùng một đầu ra. Với năng suất, nó trả về đầu ra khác nhau, dựa trên trạng thái bên trong của nó. Do đó, nếu bạn gọi ngẫu nhiên hàm tạo, không biết trạng thái trước đó, bạn không thể mong đợi nó trả về một kết quả nhất định.

Làm thế nào để một chức năng như thế này phù hợp với mô hình ngôn ngữ? Nó thực sự phá vỡ bất kỳ quy ước? Có phải là một ý tưởng tốt để có và sử dụng tính năng này? (để đưa ra một ví dụ về những gì tốt và xấu, gotođã từng là một tính năng của nhiều ngôn ngữ và vẫn còn, nhưng nó được coi là có hại và như vậy đã bị xóa khỏi một số ngôn ngữ, chẳng hạn như Java). Các trình biên dịch / trình thông dịch ngôn ngữ lập trình có phải thoát ra khỏi bất kỳ quy ước nào để thực hiện một tính năng như vậy không, ví dụ, một ngôn ngữ có phải thực hiện đa luồng cho tính năng này hoạt động không, hoặc có thể được thực hiện mà không có công nghệ luồng không?


4
yieldthực chất là một động cơ nhà nước. Nó không có nghĩa là để trả lại kết quả tương tự mọi lúc. Những gì nó sẽ làm với sự chắc chắn tuyệt đối là trả lại mục tiếp theo trong vô số mỗi lần nó được gọi. Chủ đề không bắt buộc; bạn cần đóng cửa (nhiều hơn hoặc ít hơn), để duy trì trạng thái hiện tại.
Robert Harvey

1
Đối với chất lượng của "sự chắc chắn", hãy xem xét rằng, với cùng một chuỗi đầu vào, một loạt các cuộc gọi đến trình vòng lặp sẽ mang lại chính xác các mục tương tự theo cùng một thứ tự.
Robert Harvey

4
Tôi không chắc chắn hầu hết các câu hỏi của bạn đến từ đâu vì C ++ không có yield từ khóa như Python. Nó có một phương thức tĩnh std::this_thread::yield(), nhưng đó không phải là một từ khóa. Vì vậy, this_threadsẽ trả trước hầu hết mọi cuộc gọi cho nó, làm cho nó khá rõ ràng đó là một tính năng thư viện chỉ dành cho các chủ đề năng suất, không phải là một tính năng ngôn ngữ về năng suất điều khiển nói chung.
Ixrec

liên kết được cập nhật lên C #, một cho C ++ đã bị xóa
Dennis

Câu trả lời:


16

Hãy cẩn thận trước - C # là ngôn ngữ tôi biết rõ nhất và trong khi nó có yieldngôn ngữ dường như rất giống với các ngôn ngữ khác ' yield, có thể có những khác biệt tinh tế mà tôi không biết.

Tôi lo ngại rằng với tư cách là một tính năng / cơ sở ngôn ngữ, năng suất phá vỡ một số quy ước. Một trong số đó là những gì tôi muốn nói đến là "sự chắc chắn". Đó là một phương thức trả về một kết quả khác nhau mỗi khi bạn gọi nó.

Poppycock. Bạn có thực sự mong đợi Random.Nexthoặc Console.ReadLine trả lại kết quả tương tự mỗi khi bạn gọi cho họ không? Làm thế nào về các cuộc gọi nghỉ ngơi? Xác thực? Nhận mục ra khỏi một bộ sưu tập? Có tất cả các loại chức năng (tốt, hữu ích) không tinh khiết.

Làm thế nào để một chức năng như thế này phù hợp với mô hình ngôn ngữ? Nó thực sự phá vỡ bất kỳ quy ước?

Có, yieldchơi rất tệ với try/catch/finallyvà không được phép ( https://bloss.msdn.microsoft.com/ericlippert/2009/07/16/iterator-blocks-part-three-why-no-yield-in-finally/ cho thêm thông tin).

Có phải là một ý tưởng tốt để có và sử dụng tính năng này?

Đó chắc chắn là một ý tưởng tốt để có tính năng này. Những thứ như LINQ của C # thực sự rất hay - các bộ sưu tập đánh giá một cách lười biếng mang lại lợi ích hiệu năng lớn và yieldcho phép loại điều đó được thực hiện trong một phần nhỏ của mã với một phần lỗi mà trình vòng lặp cuộn bằng tay sẽ xảy ra.

Điều đó nói rằng, không có một tấn sử dụng cho yieldbên ngoài xử lý bộ sưu tập kiểu LINQ. Tôi đã sử dụng nó để xử lý xác nhận, tạo lịch biểu, ngẫu nhiên hóa và một vài thứ khác, nhưng tôi hy vọng rằng hầu hết các nhà phát triển chưa bao giờ sử dụng nó (hoặc sử dụng sai nó).

Các trình biên dịch / trình thông dịch ngôn ngữ lập trình có phải thoát ra khỏi bất kỳ quy ước nào để thực hiện một tính năng như vậy không, ví dụ, một ngôn ngữ có phải thực hiện đa luồng cho tính năng này hoạt động không, hoặc có thể được thực hiện mà không có công nghệ luồng không?

Không chính xác. Trình biên dịch tạo ra một trình lặp máy trạng thái theo dõi nơi nó dừng lại để nó có thể bắt đầu lại ở đó vào lần tiếp theo khi nó được gọi. Quá trình tạo mã thực hiện một cái gì đó tương tự như Kiểu tiếp tục truyền, trong đó mã sau yieldđược kéo vào khối riêng của nó (và nếu nó có bất kỳ yields, khối phụ nào khác, v.v.). Đó là một cách tiếp cận nổi tiếng được sử dụng thường xuyên hơn trong Lập trình chức năng và cũng hiển thị trong phần biên dịch không đồng bộ / chờ đợi của C #.

Không cần phân luồng, nhưng nó đòi hỏi một cách tiếp cận khác để tạo mã trong hầu hết các trình biên dịch và có một số xung đột với các tính năng ngôn ngữ khác.

Tất cả trong tất cả, yieldlà một tính năng tác động tương đối thấp thực sự giúp với một tập hợp con cụ thể của các vấn đề.


Tôi chưa bao giờ sử dụng C # một cách nghiêm túc nhưng yieldtừ khóa này tương tự như coroutines, vâng, hoặc một cái gì đó khác nhau? Nếu vậy tôi ước tôi có một cái trong C! Tôi có thể nghĩ về ít nhất một số đoạn mã hợp lý sẽ dễ viết hơn rất nhiều với tính năng ngôn ngữ như vậy.

2
@DrunkCoder - tương tự, nhưng với một số hạn chế, theo tôi hiểu.
Telastyn

1
Bạn cũng sẽ không muốn thấy sản lượng sử dụng sai. Ngôn ngữ càng có nhiều tính năng, bạn càng có nhiều khả năng tìm thấy một chương trình được viết sai bằng ngôn ngữ đó. Tôi không chắc cách tiếp cận đúng để viết một ngôn ngữ có thể tiếp cận là ném tất cả vào bạn và xem những gì gậy.
Neil

1
@DrunkCoder: nó là phiên bản giới hạn của bán coroutines. Trên thực tế, nó được xử lý như một mẫu cú pháp bởi trình biên dịch được mở rộng thành một chuỗi các lệnh gọi phương thức, các lớp và các đối tượng. (Về cơ bản, trình biên dịch sẽ tạo ra một đối tượng tiếp tục mà chụp bối cảnh hiện nay trong các lĩnh vực.) Các mặc định thực hiện cho bộ sưu tập là một semi-coroutine, nhưng do quá tải "ma thuật" phương pháp sử dụng các trình biên dịch, bạn thực sự có thể tùy chỉnh hành vi. Ví dụ, trước khi async/ awaitđược thêm vào ngôn ngữ, ai đó đã triển khai nó bằng cách sử dụng yield.
Jörg W Mittag

1
@Neil Nói chung có thể sử dụng sai hầu như bất kỳ tính năng ngôn ngữ lập trình nào. Nếu những gì bạn nói là đúng, thì việc lập trình sử dụng C kém hơn Python hoặc C # sẽ khó hơn nhiều, nhưng đây không phải là trường hợp vì những ngôn ngữ đó có rất nhiều công cụ bảo vệ các lập trình viên khỏi nhiều lỗi rất dễ để thực hiện với C. Trong thực tế, nguyên nhân của các chương trình tồi là các lập trình viên tồi - đó là một vấn đề không thể biết về ngôn ngữ.
Ben Cottrell

12

Là có một cơ sở ngôn ngữ máy phát điện như yieldmột ý tưởng tốt?

Tôi muốn trả lời câu hỏi này từ góc độ Python với một câu trả lời rõ ràng , đó là một ý tưởng tuyệt vời .

Tôi sẽ bắt đầu bằng cách giải quyết một số câu hỏi và giả định trong câu hỏi của bạn trước, sau đó chứng minh tính phổ biến của các trình tạo và tính hữu dụng vô lý của chúng trong Python sau.

Với một hàm không tạo thông thường, bạn có thể gọi nó và nếu nó được cung cấp cùng một đầu vào, nó sẽ trả về cùng một đầu ra. Với năng suất, nó trả về đầu ra khác nhau, dựa trên trạng thái bên trong của nó.

Điều này là sai. Các phương thức trên các đối tượng có thể được coi là các hàm, với trạng thái bên trong riêng của chúng. Trong Python, vì mọi thứ đều là một đối tượng, bạn thực sự có thể lấy một phương thức từ một đối tượng và truyền xung quanh phương thức đó (liên kết với đối tượng mà nó xuất phát, vì vậy nó nhớ trạng thái của nó).

Các ví dụ khác bao gồm các chức năng ngẫu nhiên có chủ ý cũng như các phương thức nhập như mạng, hệ thống tệp và thiết bị đầu cuối.

Làm thế nào để một chức năng như thế này phù hợp với mô hình ngôn ngữ?

Nếu mô hình ngôn ngữ hỗ trợ những thứ như các hàm hạng nhất và các trình tạo hỗ trợ các tính năng ngôn ngữ khác như giao thức Iterable, thì chúng sẽ khớp hoàn toàn.

Nó thực sự phá vỡ bất kỳ quy ước?

Không. Vì nó được đưa vào ngôn ngữ, các quy ước được xây dựng xung quanh và bao gồm (hoặc yêu cầu!) Việc sử dụng máy phát điện.

Các trình biên dịch / phiên dịch ngôn ngữ lập trình phải thoát ra khỏi bất kỳ quy ước nào để thực hiện một tính năng như vậy

Như với bất kỳ tính năng nào khác, trình biên dịch chỉ cần được thiết kế để hỗ trợ tính năng này. Trong trường hợp của Python, các hàm đã là các đối tượng có trạng thái (chẳng hạn như các đối số mặc định và các chú thích hàm).

một ngôn ngữ phải thực hiện đa luồng cho tính năng này hoạt động, hoặc nó có thể được thực hiện mà không có công nghệ luồng?

Sự thật thú vị: Việc triển khai Python mặc định hoàn toàn không hỗ trợ luồng. Nó có tính năng Khóa phiên dịch toàn cầu (GIL), vì vậy không có gì thực sự chạy đồng thời trừ khi bạn tạo ra một quy trình thứ hai để chạy một phiên bản Python khác.


lưu ý: ví dụ trong Python 3

Ngoài năng suất

Mặc dù yieldtừ khóa có thể được sử dụng trong bất kỳ chức năng nào để biến nó thành một trình tạo, nhưng đó không phải là cách duy nhất để tạo một từ khóa. Python có tính năng Trình tạo biểu thức, một cách mạnh mẽ để thể hiện rõ ràng trình tạo theo thuật ngữ lặp khác (bao gồm các trình tạo khác)

>>> pairs = ((x,y) for x in range(10) for y in range(10) if y >= x)
>>> pairs
<generator object <genexpr> at 0x0311DC90>
>>> sum(x*y for x,y in pairs)
1155

Như bạn có thể thấy, không chỉ cú pháp rõ ràng và dễ đọc, mà các hàm tích hợp như sumchấp nhận trình tạo.

Với

Kiểm tra Đề xuất cải tiến Python cho câu lệnh With . Nó rất khác so với bạn có thể mong đợi từ một câu lệnh With trong các ngôn ngữ khác. Với một chút trợ giúp từ thư viện tiêu chuẩn, các trình tạo của Python hoạt động tuyệt vời như các trình quản lý bối cảnh cho chúng.

>>> from contextlib import contextmanager
>>> @contextmanager
def debugWith(arg):
        print("preprocessing", arg)
        yield arg
        print("postprocessing", arg)


>>> with debugWith("foobar") as s:
        print(s[::-1])


preprocessing foobar
raboof
postprocessing foobar

Tất nhiên, in mọi thứ là về những điều nhàm chán nhất bạn có thể làm ở đây, nhưng nó cho thấy kết quả rõ ràng. Các tùy chọn thú vị hơn bao gồm tự động quản lý tài nguyên (mở và đóng tệp / luồng / kết nối mạng), khóa để đồng thời, tạm thời gói hoặc thay thế một chức năng và giải nén sau đó giải nén dữ liệu. Nếu các hàm gọi giống như tiêm mã vào mã của bạn, thì với các câu lệnh giống như gói các phần mã của bạn trong mã khác. Tuy nhiên, bạn sử dụng nó, đó là một ví dụ chắc chắn về một cái móc dễ dàng vào cấu trúc ngôn ngữ. Các trình tạo dựa trên năng suất không phải là cách duy nhất để tạo các trình quản lý bối cảnh, nhưng chúng chắc chắn là một cách thuận tiện.

Kiệt sức một phần

Đối với các vòng lặp trong Python hoạt động theo một cách thú vị. Chúng có định dạng sau:

for <name> in <iterable>:
    ...

Đầu tiên, biểu thức tôi gọi <iterable>được ước tính để có được một đối tượng có thể lặp lại. Thứ hai, iterable đã __iter__gọi nó và iterator kết quả được lưu trữ phía sau hậu trường. Sau đó, __next__được gọi trên iterator để lấy giá trị để liên kết với tên bạn đặt <name>. Bước này lặp lại cho đến khi cuộc gọi __next__ném a StopIteration. Ngoại lệ bị nuốt bởi vòng lặp for và việc thực thi tiếp tục từ đó.

Quay trở lại máy phát điện: khi bạn gọi __iter__máy phát điện, nó sẽ tự trả về.

>>> x = (a for a in "boring generator")
>>> id(x)
51502272
>>> id(x.__iter__())
51502272

Điều này có nghĩa là bạn có thể tách rời việc lặp đi lặp lại một thứ gì đó khỏi điều bạn muốn làm với nó và thay đổi hành vi đó giữa chừng. Dưới đây, lưu ý cách cùng một trình tạo được sử dụng trong hai vòng và trong lần thứ hai, nó bắt đầu thực thi từ nơi nó rời khỏi đầu tiên.

>>> generator = (x for x in 'more boring stuff')
>>> for letter in generator:
        print(ord(letter))
        if letter > 'p':
                break


109
111
114
>>> for letter in generator:
        print(letter)


e

b
o
r
i
n
g

s
t
u
f
f

Đánh giá lười biếng

Một trong những mặt trái của máy phát điện so với danh sách là thứ duy nhất bạn có thể truy cập trong máy phát điện là thứ tiếp theo được tạo ra từ nó. Bạn không thể quay lại và như một kết quả trước đó, hoặc chuyển sang kết quả sau mà không thông qua kết quả trung gian. Mặt trái của điều này là một trình tạo có thể chiếm gần như không có bộ nhớ so với danh sách tương đương của nó.

>>> import sys
>>> sys.getsizeof([x for x in range(10000)])
43816
>>> sys.getsizeof(range(10000000000))
24
>>> sys.getsizeof([x for x in range(10000000000)])
Traceback (most recent call last):
  File "<pyshell#10>", line 1, in <module>
    sys.getsizeof([x for x in range(10000000000)])
  File "<pyshell#10>", line 1, in <listcomp>
    sys.getsizeof([x for x in range(10000000000)])
MemoryError

Máy phát điện cũng có thể bị xiềng xích một cách lười biếng.

logfile = open("logs.txt")
lastcolumn = (line.split()[-1] for line in logfile)
numericcolumn = (float(x) for x in lastcolumn)
print(sum(numericcolumn))

Các dòng đầu tiên, thứ hai và thứ ba chỉ định nghĩa mỗi trình tạo, nhưng không thực hiện bất kỳ công việc thực tế nào. Khi dòng cuối cùng được gọi, sum yêu cầu số lượng cho một giá trị, số màu cần một giá trị từ lastcolumn, lastcolumn yêu cầu một giá trị từ logfile, sau đó thực sự đọc một dòng từ tệp. Ngăn xếp này thư giãn cho đến khi tổng có được số nguyên đầu tiên của nó. Sau đó, quá trình xảy ra một lần nữa cho dòng thứ hai. Tại thời điểm này, sum có hai số nguyên và nó cộng chúng lại với nhau. Lưu ý rằng dòng thứ ba chưa được đọc từ tệp. Sum sau đó tiếp tục yêu cầu các giá trị từ số màu (hoàn toàn không biết đến phần còn lại của chuỗi) và thêm chúng, cho đến khi hết số.

Phần thực sự thú vị ở đây là các dòng được đọc, tiêu thụ và loại bỏ riêng lẻ. Không có lúc nào toàn bộ tập tin trong bộ nhớ cùng một lúc. Điều gì xảy ra nếu tệp nhật ký này là, một terabyte? Nó chỉ hoạt động, bởi vì nó chỉ đọc một dòng tại một thời điểm.

Phần kết luận

Đây không phải là một đánh giá đầy đủ về tất cả việc sử dụng các trình tạo trong Python. Đáng chú ý, tôi đã bỏ qua các máy phát vô hạn, các máy trạng thái, chuyển các giá trị trở lại và mối quan hệ của chúng với các coroutines.

Tôi tin rằng nó đủ để chứng minh rằng bạn có thể có máy phát điện như một tính năng ngôn ngữ hữu ích, được tích hợp sạch sẽ.


6

Nếu bạn đã quen với các ngôn ngữ OOP cổ điển, các trình tạo và yieldcó vẻ bị chói tai vì trạng thái có thể thay đổi được ghi lại ở cấp độ chức năng thay vì cấp đối tượng.

Câu hỏi về "sự chắc chắn" là một cá trích đỏ. Nó thường được gọi là độ trong suốt tham chiếu và về cơ bản có nghĩa là hàm luôn trả về cùng một kết quả cho cùng một đối số. Ngay khi bạn có trạng thái có thể thay đổi, bạn sẽ mất tính minh bạch tham chiếu. Trong OOP, các đối tượng thường có trạng thái có thể thay đổi, có nghĩa là kết quả của lệnh gọi phương thức không chỉ phụ thuộc vào các đối số, mà còn cả trạng thái bên trong của đối tượng.

Câu hỏi là nơi để nắm bắt trạng thái đột biến. Trong một OOP cổ điển, trạng thái đột biến tồn tại ở cấp đối tượng. Nhưng nếu một hỗ trợ ngôn ngữ đóng, bạn có thể có trạng thái có thể thay đổi ở cấp độ chức năng. Ví dụ: trong JavaScript:

function getCounter() {
   var cnt = 1;
   return function(){ return cnt++; }
}
var counter = getCounter();
counter() --> 1
counter() --> 2

Nói tóm lại, yieldlà tự nhiên trong một ngôn ngữ hỗ trợ các bao đóng, nhưng sẽ không phù hợp với ngôn ngữ như phiên bản Java cũ hơn, nơi trạng thái có thể thay đổi chỉ tồn tại ở cấp đối tượng.


Tôi cho rằng nếu các tính năng ngôn ngữ có phổ, năng suất sẽ càng xa chức năng càng tốt. Đó không hẳn là một điều xấu. OOP đã từng rất thời trang, và một lần nữa sau đó là lập trình chức năng. Tôi cho rằng sự nguy hiểm của nó thực sự lên đến sự pha trộn và kết hợp các tính năng như năng suất với một thiết kế chức năng khiến chương trình của bạn hoạt động theo những cách không ngờ tới.
Neil

0

Theo tôi, nó không phải là một tính năng tốt. Đó là một tính năng xấu, chủ yếu bởi vì nó cần phải được dạy rất cẩn thận, và mọi người đều dạy nó sai. Mọi người sử dụng từ "trình tạo", tương đương giữa chức năng của trình tạo và đối tượng trình tạo. Câu hỏi là: chỉ ai hoặc cái gì đang làm cho năng suất thực tế?

Đây không chỉ là ý kiến ​​của tôi. Ngay cả Guido, trong bản tin PEP mà ông quy định về vấn đề này, thừa nhận rằng chức năng của máy phát điện không phải là máy phát điện mà là "nhà máy phát điện".

Điều đó rất quan trọng, bạn có nghĩ vậy không? Nhưng đọc 99% tài liệu ngoài đó, bạn sẽ có ấn tượng rằng chức năng của trình tạo là trình tạo thực tế và họ có xu hướng bỏ qua thực tế bạn cũng cần một đối tượng trình tạo.

Guido đã cân nhắc việc thay thế "def" cho "gen" cho các chức năng này và nói Không. Nhưng tôi cho rằng dù sao đi nữa cũng không đủ. Nó thực sự nên là:

def make_gen(args)
    def_gen foo
        # Put in "yield" and other beahvior
    return_gen foo
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.