Tôi đang bắt đầu tìm hiểu Python và tôi đã bắt gặp các hàm của trình tạo, những hàm có tuyên bố lợi suất trong đó. Tôi muốn biết những loại vấn đề mà các chức năng này thực sự tốt để giải quyết.
Tôi đang bắt đầu tìm hiểu Python và tôi đã bắt gặp các hàm của trình tạo, những hàm có tuyên bố lợi suất trong đó. Tôi muốn biết những loại vấn đề mà các chức năng này thực sự tốt để giải quyết.
Câu trả lời:
Máy phát điện cho bạn đánh giá lười biếng. Bạn sử dụng chúng bằng cách lặp lại chúng, hoặc rõ ràng bằng 'cho' hoặc ngầm định bằng cách chuyển nó đến bất kỳ chức năng nào hoặc xây dựng lặp đi lặp lại. Bạn có thể nghĩ về các trình tạo như trả lại nhiều mục, như thể chúng trả về một danh sách, nhưng thay vì trả lại tất cả chúng cùng một lúc, chúng sẽ trả lại từng cái một và chức năng của trình tạo được tạm dừng cho đến khi mục tiếp theo được yêu cầu.
Các trình tạo rất tốt cho việc tính toán các tập kết quả lớn (đặc biệt là các phép tính liên quan đến các vòng lặp) mà bạn không biết liệu mình sẽ cần tất cả các kết quả hay không, nơi bạn không muốn phân bổ bộ nhớ cho tất cả các kết quả cùng một lúc . Hoặc đối với các tình huống trong đó trình tạo sử dụng một trình tạo khác hoặc tiêu thụ một số tài nguyên khác và sẽ thuận tiện hơn nếu điều đó xảy ra càng muộn càng tốt.
Một cách sử dụng khác cho máy phát điện (điều này thực sự giống nhau) là thay thế các cuộc gọi lại bằng phép lặp. Trong một số tình huống bạn muốn một chức năng thực hiện nhiều công việc và đôi khi báo cáo lại cho người gọi. Theo truyền thống, bạn sẽ sử dụng chức năng gọi lại cho việc này. Bạn chuyển cuộc gọi lại này đến chức năng công việc và nó sẽ định kỳ gọi cuộc gọi lại này. Cách tiếp cận của trình tạo là hàm làm việc (bây giờ là trình tạo) không biết gì về cuộc gọi lại và chỉ mang lại bất cứ khi nào nó muốn báo cáo điều gì đó. Người gọi, thay vì viết một cuộc gọi lại riêng biệt và chuyển nó đến chức năng công việc, thực hiện tất cả các công việc báo cáo trong một vòng lặp 'for' nhỏ xung quanh trình tạo.
Ví dụ: giả sử bạn đã viết chương trình 'tìm kiếm hệ thống tập tin'. Bạn có thể thực hiện toàn bộ tìm kiếm, thu thập kết quả và sau đó hiển thị từng kết quả. Tất cả các kết quả sẽ phải được thu thập trước khi bạn hiển thị lần đầu tiên và tất cả các kết quả sẽ nằm trong bộ nhớ cùng một lúc. Hoặc bạn có thể hiển thị kết quả trong khi bạn tìm thấy chúng, sẽ hiệu quả hơn về bộ nhớ và thân thiện hơn với người dùng. Điều thứ hai có thể được thực hiện bằng cách chuyển chức năng in kết quả cho chức năng tìm kiếm hệ thống tệp hoặc có thể được thực hiện bằng cách chỉ làm cho chức năng tìm kiếm trở thành trình tạo và lặp lại kết quả.
Nếu bạn muốn xem một ví dụ về hai cách tiếp cận sau, hãy xem os.path.walk () (chức năng đi bộ hệ thống tập tin cũ với gọi lại) và os.walk () (trình tạo hệ thống tập tin đi bộ mới.) Tất nhiên, nếu bạn thực sự muốn thu thập tất cả các kết quả trong một danh sách, cách tiếp cận trình tạo là không quan trọng để chuyển đổi sang cách tiếp cận danh sách lớn:
big_list = list(the_generator)
yield
và join
sau đó để có kết quả tiếp theo, nó không thực thi song song (và không có trình tạo thư viện chuẩn nào thực hiện việc này; Trình tạo tạm dừng tại mỗi yield
cho đến khi giá trị tiếp theo được yêu cầu. Nếu trình tạo gói I / O, HĐH có thể chủ động lưu trữ dữ liệu từ tệp theo giả định rằng nó sẽ được yêu cầu ngay, nhưng đó là HĐH, Python không liên quan.
Một trong những lý do để sử dụng máy phát điện là làm cho giải pháp rõ ràng hơn đối với một số loại giải pháp.
Cách khác là xử lý từng kết quả một lần, tránh xây dựng danh sách kết quả khổng lồ mà bạn sẽ xử lý tách biệt bằng mọi cách.
Nếu bạn có chức năng Wikipedia-up-to-n như thế này:
# function version
def fibon(n):
a = b = 1
result = []
for i in xrange(n):
result.append(a)
a, b = b, a + b
return result
Bạn có thể dễ dàng viết hàm như thế này:
# generator version
def fibon(n):
a = b = 1
for i in xrange(n):
yield a
a, b = b, a + b
Chức năng rõ ràng hơn. Và nếu bạn sử dụng chức năng như thế này:
for x in fibon(1000000):
print x,
trong ví dụ này, nếu sử dụng phiên bản trình tạo, toàn bộ danh sách 1000000 mục sẽ không được tạo, chỉ một giá trị tại một thời điểm. Điều đó sẽ không xảy ra khi sử dụng phiên bản danh sách, trong đó danh sách sẽ được tạo trước tiên.
list(fibon(5))
Xem phần "Động lực" trong PEP 255 .
Việc sử dụng máy phát điện không rõ ràng là tạo ra các chức năng ngắt, cho phép bạn thực hiện những việc như cập nhật UI hoặc chạy một số công việc "đồng thời" (thực tế xen kẽ) trong khi không sử dụng các luồng.
Tôi tìm thấy lời giải thích này mà xóa bỏ nghi ngờ của tôi. Bởi vì có khả năng người không biết Generators
cũng không biết vềyield
Trở về
Câu lệnh return là nơi tất cả các biến cục bộ bị hủy và giá trị kết quả được trả lại (trả lại) cho người gọi. Nếu cùng một hàm được gọi một thời gian sau, hàm sẽ nhận được một bộ biến mới.
Năng suất
Nhưng điều gì sẽ xảy ra nếu các biến cục bộ không bị vứt đi khi chúng ta thoát khỏi một hàm? Điều này ngụ ý rằng chúng ta có thể resume the function
nơi chúng ta rời đi. Đây là nơi khái niệm generators
được giới thiệu và yield
tuyên bố nối lại nơi function
trái.
def generate_integers(N):
for i in xrange(N):
yield i
In [1]: gen = generate_integers(3)
In [2]: gen
<generator object at 0x8117f90>
In [3]: gen.next()
0
In [4]: gen.next()
1
In [5]: gen.next()
Vì vậy, đó là sự khác biệt giữa return
và các yield
câu lệnh trong Python.
Câu lệnh năng suất là những gì làm cho một chức năng một chức năng tạo.
Vì vậy, máy phát điện là một công cụ đơn giản và mạnh mẽ để tạo các vòng lặp. Chúng được viết như các hàm thông thường, nhưng chúng sử dụng yield
câu lệnh bất cứ khi nào chúng muốn trả về dữ liệu. Mỗi lần gọi () tiếp theo, trình tạo lại tiếp tục ở nơi nó dừng lại (nó ghi nhớ tất cả các giá trị dữ liệu và câu lệnh nào được thực hiện lần cuối).
Giả sử bạn có 100 triệu tên miền trong bảng MySQL của mình và bạn muốn cập nhật thứ hạng Alexa cho mỗi tên miền.
Điều đầu tiên bạn cần là chọn tên miền của bạn từ cơ sở dữ liệu.
Giả sử tên bảng của bạn là domains
và tên cột là domain
.
Nếu bạn sử dụng, SELECT domain FROM domains
nó sẽ trả về 100 triệu hàng sẽ tiêu tốn nhiều bộ nhớ. Vì vậy, máy chủ của bạn có thể bị sập.
Vì vậy, bạn quyết định chạy chương trình theo đợt. Giả sử kích thước lô của chúng tôi là 1000.
Trong đợt đầu tiên của chúng tôi, chúng tôi sẽ truy vấn 1000 hàng đầu tiên, kiểm tra thứ hạng Alexa cho mỗi tên miền và cập nhật hàng cơ sở dữ liệu.
Trong đợt thứ hai của chúng tôi, chúng tôi sẽ làm việc trên 1000 hàng tiếp theo. Trong đợt thứ ba của chúng tôi sẽ là từ năm 2001 đến 3000 và cứ thế.
Bây giờ chúng ta cần một hàm tạo tạo ra các lô của chúng tôi.
Đây là chức năng tạo của chúng tôi:
def ResultGenerator(cursor, batchsize=1000):
while True:
results = cursor.fetchmany(batchsize)
if not results:
break
for result in results:
yield result
Như bạn có thể thấy, chức năng của chúng tôi tiếp tục đưa yield
ra kết quả. Nếu bạn đã sử dụng từ khóa return
thay vì yield
, thì toàn bộ chức năng sẽ được kết thúc sau khi đạt được lợi nhuận.
return - returns only once
yield - returns multiple times
Nếu một hàm sử dụng từ khóa yield
thì đó là một trình tạo.
Bây giờ bạn có thể lặp lại như thế này:
db = MySQLdb.connect(host="localhost", user="root", passwd="root", db="domains")
cursor = db.cursor()
cursor.execute("SELECT domain FROM domains")
for result in ResultGenerator(cursor):
doSomethingWith(result)
db.close()
Đệm. Khi có hiệu quả để tìm nạp dữ liệu trong các khối lớn, nhưng xử lý dữ liệu đó thành các khối nhỏ, thì trình tạo có thể giúp:
def bufferedFetch():
while True:
buffer = getBigChunkOfData()
# insert some code to break on 'end of data'
for i in buffer:
yield i
Ở trên cho phép bạn dễ dàng tách bộ đệm khỏi xử lý. Hàm người tiêu dùng giờ đây có thể chỉ nhận các giá trị từng cái một mà không phải lo lắng về bộ đệm.
Tôi đã thấy rằng các trình tạo rất hữu ích trong việc làm sạch mã của bạn và bằng cách cung cấp cho bạn một cách rất độc đáo để đóng gói và mô đun hóa mã. Trong một tình huống mà bạn cần một cái gì đó để không ngừng nhổ ra giá trị dựa trên xử lý nội bộ riêng của mình và khi đó một cái gì đó cần phải được gọi từ bất cứ nơi nào trong mã của bạn (và không chỉ trong vòng một hoặc một khối chẳng hạn), máy phát điện là các tính năng để sử dụng.
Một ví dụ trừu tượng sẽ là một trình tạo số Fibonacci không sống trong một vòng lặp và khi nó được gọi từ bất cứ đâu sẽ luôn trả về số tiếp theo trong chuỗi:
def fib():
first = 0
second = 1
yield first
yield second
while 1:
next = first + second
yield next
first = second
second = next
fibgen1 = fib()
fibgen2 = fib()
Bây giờ bạn có hai đối tượng trình tạo số Fibonacci mà bạn có thể gọi từ bất kỳ đâu trong mã của mình và chúng sẽ luôn trả về các số Fibonacci lớn hơn theo trình tự như sau:
>>> fibgen1.next(); fibgen1.next(); fibgen1.next(); fibgen1.next()
0
1
1
2
>>> fibgen2.next(); fibgen2.next()
0
1
>>> fibgen1.next(); fibgen1.next()
3
5
Điều đáng yêu về máy phát điện là chúng đóng gói trạng thái mà không phải trải qua các vòng tạo vật thể. Một cách nghĩ về chúng là "các chức năng" ghi nhớ trạng thái bên trong của chúng.
Tôi đã lấy ví dụ về Fibonacci từ Trình tạo Python - Chúng là gì? và với một chút trí tưởng tượng, bạn có thể đưa ra rất nhiều tình huống khác trong đó các máy phát điện tạo ra một sự thay thế tuyệt vời cho for
các vòng lặp và các cấu trúc lặp truyền thống khác.
Giải thích đơn giản: Xem xét một for
tuyên bố
for item in iterable:
do_stuff()
Rất nhiều thời gian, tất cả các mục trong iterable
không cần phải có ngay từ đầu, nhưng có thể được tạo ra ngay lập tức khi chúng được yêu cầu. Điều này có thể hiệu quả hơn rất nhiều trong cả hai
Những lần khác, bạn thậm chí không biết tất cả các mục trước thời hạn. Ví dụ:
for command in user_input():
do_stuff_with(command)
Bạn không có cách nào biết trước tất cả các lệnh của người dùng, nhưng bạn có thể sử dụng một vòng lặp đẹp như thế này nếu bạn có một trình tạo bàn giao cho bạn các lệnh:
def user_input():
while True:
wait_for_command()
cmd = get_command()
yield cmd
Với các trình tạo, bạn cũng có thể lặp lại các chuỗi vô hạn, điều này tất nhiên là không thể khi lặp qua các container.
Sử dụng yêu thích của tôi là hoạt động "lọc" và "giảm".
Giả sử chúng ta đang đọc một tệp và chỉ muốn các dòng bắt đầu bằng "##".
def filter2sharps( aSequence ):
for l in aSequence:
if l.startswith("##"):
yield l
Sau đó chúng ta có thể sử dụng hàm tạo trong một vòng lặp thích hợp
source= file( ... )
for line in filter2sharps( source.readlines() ):
print line
source.close()
Ví dụ giảm là tương tự. Giả sử chúng ta có một tệp mà chúng ta cần xác định vị trí các khối <Location>...</Location>
dòng. [Không phải thẻ HTML, nhưng các dòng xảy ra trông giống thẻ.]
def reduceLocation( aSequence ):
keep= False
block= None
for line in aSequence:
if line.startswith("</Location"):
block.append( line )
yield block
block= None
keep= False
elif line.startsWith("<Location"):
block= [ line ]
keep= True
elif keep:
block.append( line )
else:
pass
if block is not None:
yield block # A partial block, icky
Một lần nữa, chúng ta có thể sử dụng trình tạo này trong một vòng lặp thích hợp.
source = file( ... )
for b in reduceLocation( source.readlines() ):
print b
source.close()
Ý tưởng là một hàm tạo cho phép chúng ta lọc hoặc giảm một chuỗi, tạo ra một giá trị một chuỗi khác tại một thời điểm.
fileobj.readlines()
sẽ đọc toàn bộ tập tin vào một danh sách trong bộ nhớ, đánh bại mục đích sử dụng máy phát điện. Vì các đối tượng tập tin đã có thể lặp lại, bạn có thể sử dụng for b in your_generator(fileobject):
thay thế. Bằng cách đó, tệp của bạn sẽ được đọc từng dòng một, để tránh đọc toàn bộ tệp.
Một ví dụ thực tế nơi bạn có thể sử dụng máy phát điện là nếu bạn có một số hình dạng và bạn muốn lặp lại qua các góc, cạnh hoặc bất cứ thứ gì. Đối với dự án của riêng tôi (mã nguồn ở đây ) tôi đã có một hình chữ nhật:
class Rect():
def __init__(self, x, y, width, height):
self.l_top = (x, y)
self.r_top = (x+width, y)
self.r_bot = (x+width, y+height)
self.l_bot = (x, y+height)
def __iter__(self):
yield self.l_top
yield self.r_top
yield self.r_bot
yield self.l_bot
Bây giờ tôi có thể tạo một hình chữ nhật và lặp qua các góc của nó:
myrect=Rect(50, 50, 100, 100)
for corner in myrect:
print(corner)
Thay vì __iter__
bạn có thể có một phương pháp iter_corners
và gọi đó với for corner in myrect.iter_corners()
. Nó chỉ đơn giản hơn để sử dụng __iter__
kể từ đó chúng ta có thể sử dụng tên thể hiện của lớp trực tiếp trong for
biểu thức.
Tuy nhiên, một số câu trả lời hay ở đây, tôi cũng khuyên bạn nên đọc toàn bộ hướng dẫn Lập trình chức năng Python để giúp giải thích một số trường hợp sử dụng mạnh hơn của các trình tạo.
Vì phương thức gửi của trình tạo chưa được đề cập, đây là một ví dụ:
def test():
for i in xrange(5):
val = yield
print(val)
t = test()
# Proceed to 'yield' statement
next(t)
# Send value to yield
t.send(1)
t.send('2')
t.send([3])
Nó cho thấy khả năng gửi một giá trị đến một trình tạo đang chạy. Một khóa học nâng cao hơn về máy phát điện trong video dưới đây (bao gồm yield
từ khám phá, máy phát để xử lý song song, thoát khỏi giới hạn đệ quy, v.v.)
Cọc các thứ. Bất cứ lúc nào bạn muốn tạo một chuỗi các mục, nhưng không muốn phải 'cụ thể hóa' tất cả chúng thành một danh sách cùng một lúc. Ví dụ: bạn có thể có một trình tạo đơn giản trả về các số nguyên tố:
def primes():
primes_found = set()
primes_found.add(2)
yield 2
for i in itertools.count(1):
candidate = i * 2 + 1
if not all(candidate % prime for prime in primes_found):
primes_found.add(candidate)
yield candidate
Sau đó, bạn có thể sử dụng điều đó để tạo ra các sản phẩm của các số nguyên tố tiếp theo:
def prime_products():
primeiter = primes()
prev = primeiter.next()
for prime in primeiter:
yield prime * prev
prev = prime
Đây là những ví dụ khá tầm thường, nhưng bạn có thể thấy nó hữu ích như thế nào khi xử lý các bộ dữ liệu lớn (có khả năng vô hạn!) Mà không tạo ra chúng trước, đây chỉ là một trong những cách sử dụng rõ ràng hơn.
Cũng tốt để in các số nguyên tố lên đến n:
def genprime(n=10):
for num in range(3, n+1):
for factor in range(2, num):
if num%factor == 0:
break
else:
yield(num)
for prime_num in genprime(100):
print(prime_num)