Bạn có thể sử dụng các hàm tạo Python để làm gì?


213

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.


6
có lẽ một câu hỏi tốt hơn sẽ là khi chúng ta không nên sử dụng 'em
cregox

1
Ví dụ thế giới thực ở đây
Giri

Câu trả lời:


239

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)

Liệu một trình tạo như một danh sách tạo ra danh sách hệ thống tập tin thực hiện các hành động song song với mã chạy trình tạo đó trong một vòng lặp? Lý tưởng nhất là máy tính sẽ chạy phần thân của vòng lặp (xử lý kết quả cuối cùng) trong khi đồng thời làm bất cứ điều gì mà trình tạo phải làm để có được giá trị tiếp theo.
Steven Lu

@StevenLu: Trừ khi gặp rắc rối khi tự khởi chạy các luồng trước yieldjoinsau đó để 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 yieldcho đế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.
ShadowRanger

90

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.


18
và nếu bạn cần một danh sách, bạn luôn có thể làmlist(fibon(5))
endolith

41

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.


1
Phần Động lực rất hay ở chỗ nó có một ví dụ cụ thể: "Khi một hàm sản xuất có một công việc đủ cứng mà nó yêu cầu duy trì trạng thái giữa các giá trị được tạo ra, hầu hết các ngôn ngữ lập trình không cung cấp giải pháp hiệu quả và dễ chịu ngoài việc thêm chức năng gọi lại vào đối số của nhà sản xuất liệt kê ... Ví dụ: tokenize.py trong thư viện tiêu chuẩn thực hiện phương pháp này "
Ben Creasy

38

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 Generatorscũ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 functionnơi chúng ta rời đi. Đây là nơi khái niệm generatorsđược giới thiệu và yieldtuyên bố nối lại nơi functiontrá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 returnvà các yieldcâ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 yieldcâ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).


33

Ví dụ thế giới thực

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à domainsvà tên cột là domain.

Nếu bạn sử dụng, SELECT domain FROM domainsnó 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 yieldra kết quả. Nếu bạn đã sử dụng từ khóa returnthay 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 yieldthì đó 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()

nó sẽ thực tế hơn, nếu năng suất có thể được giải thích dưới dạng lập trình đệ quy / dyanmic!
igaurav

27

Đệ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.


3
Nếu getBigChuckOfData không lười biếng, thì tôi không hiểu lợi ích mang lại ở đây là gì. Trường hợp sử dụng cho chức năng này là gì?
Sean Geoffrey Pietz

1
Nhưng vấn đề là, IIUC, bufferedFetch đang lười biếng gọi lệnh getBigChunkOfData. Nếu getBigChunkOfData đã lười biếng, thì buffFetch sẽ vô dụng. Mỗi lệnh gọi tới bufferedFetch () sẽ trả về một phần tử bộ đệm, mặc dù BigChunk đã được đọc. Và bạn không cần phải đếm rõ ràng phần tử tiếp theo để trả về, bởi vì các cơ chế của năng suất chỉ thực hiện điều đó một cách ngầm định.
hmijail thương tiếc người từ chức

21

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 forcác vòng lặp và các cấu trúc lặp truyền thống khác.


19

Giải thích đơn giản: Xem xét một fortuyên bố

for item in iterable:
   do_stuff()

Rất nhiều thời gian, tất cả các mục trong iterablekhô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

  • không gian (bạn không bao giờ cần lưu trữ tất cả các mục cùng một lúc) và
  • thời gian (việc lặp lại có thể kết thúc trước khi tất cả các mục là cần thiết).

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.


... và một chuỗi vô hạn có thể được tạo ra bằng cách lặp đi lặp lại trên một danh sách nhỏ, trở về đầu sau khi kết thúc. Tôi sử dụng điều này để chọn màu sắc trong biểu đồ, hoặc tạo ra các trình điều khiển hoặc trình quay bận rộn trong văn bản.
Andrej Panjkov

@mataap: Có một cái itertoolcho điều đó - xem cycles.
martineau

12

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.


8
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.
nosklo

GiảmLocation là một sản phẩm khá kỳ lạ trong danh sách, tại sao không chỉ mang lại từng dòng? Ngoài ra, bộ lọc và giảm là các nội trang với các hành vi dự kiến ​​(xem trợ giúp trong ipython, v.v.), cách sử dụng "giảm" của bạn cũng giống như bộ lọc.
James Antill

Điểm tốt trên các đường dẫn (). Tôi thường nhận ra rằng các tệp là các trình vòng lặp dòng hạng nhất trong quá trình kiểm tra đơn vị.
S.Lott

Trên thực tế, "giảm" đang kết hợp nhiều dòng riêng lẻ thành một đối tượng tổng hợp. Được rồi, đó là một danh sách, nhưng nó vẫn là một giảm từ nguồn.
S.Lott

9

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_cornersvà 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 forbiểu thức.


Tôi ngưỡng mộ ý tưởng vượt qua các trường lớp tương tự như một máy phát điện
eusoubrasileiro

7

Về cơ bản tránh các chức năng gọi lại khi lặp qua trạng thái duy trì đầu vào.

Xem ở đâyở đây để biết tổng quan về những gì có thể được thực hiện bằng cách sử dụng máy phát điện.


4

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.


3

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 yieldtừ khám phá, máy phát để xử lý song song, thoát khỏi giới hạn đệ quy, v.v.)

David Beazley trên máy phát điện tại PyCon 2014


2

Tôi sử dụng máy phát điện khi máy chủ web của chúng tôi hoạt động như một proxy:

  1. Máy khách yêu cầu một url được ủy quyền từ máy chủ
  2. Máy chủ bắt đầu tải url đích
  3. Máy chủ mang lại kết quả trả về cho khách hàng ngay khi nhận được chúng

1

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.


nếu không phải bất kỳ (ứng cử viên% số nguyên tố cho số nguyên tố trong primes_found) sẽ là nếu tất cả (ứng cử viên% số nguyên tố cho số nguyên tố trong primes_found)
rjmunro

Có, tôi có ý viết "nếu không phải là bất kỳ (ứng cử viên% Prime == 0 cho số nguyên tố trong primes_found). Tuy nhiên, bạn hơi gọn gàng hơn. :)
Nick Johnson

Tôi đoán bạn đã quên xóa 'không' nếu không phải tất cả (ứng cử viên% chính cho số nguyên tố trong primes_found)
Thava

0

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)
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.