Là có một cơ sở ngôn ngữ máy phát điện như yield
mộ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ù yield
từ 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ư sum
chấ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ẽ.
yield
thự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.