Python: biểu thức trình tạo so với năng suất


90

Trong Python, có sự khác biệt nào giữa việc tạo đối tượng trình tạo thông qua biểu thức trình tạo so với sử dụng câu lệnh lợi nhuận không?

Sử dụng năng suất :

def Generator(x, y):
    for i in xrange(x):
        for j in xrange(y):
            yield(i, j)

Sử dụng biểu thức trình tạo :

def Generator(x, y):
    return ((i, j) for i in xrange(x) for j in xrange(y))

Cả hai hàm đều trả về các đối tượng của trình tạo, tạo ra các bộ giá trị, ví dụ (0,0), (0,1), v.v.

Bất kỳ lợi thế của cái này hay cái khác? Suy nghĩ?


Cảm ơn tất cả mọi người! Có rất nhiều thông tin tuyệt vời và tài liệu tham khảo thêm trong các câu trả lời này!


2
Chọn một trong những bạn thấy dễ đọc nhất.
user238424

Câu trả lời:


74

Chỉ có sự khác biệt nhỏ trong cả hai. Bạn có thể sử dụng dismô-đun để kiểm tra loại điều này cho chính mình.

Chỉnh sửa: Phiên bản đầu tiên của tôi đã dịch ngược biểu thức trình tạo được tạo ở phạm vi mô-đun trong lời nhắc tương tác. Điều đó hơi khác so với phiên bản của OP với nó được sử dụng bên trong một chức năng. Tôi đã sửa đổi điều này để phù hợp với trường hợp thực tế trong câu hỏi.

Như bạn có thể thấy bên dưới, trình tạo "lợi nhuận" (trường hợp đầu tiên) có ba hướng dẫn bổ sung trong thiết lập, nhưng so với hướng dẫn đầu tiên, FOR_ITERchúng chỉ khác nhau ở một khía cạnh: cách tiếp cận "lợi nhuận" sử dụng LOAD_FASTthay thế cho một LOAD_DEREFbên trong vòng lặp. Các LOAD_DEREF"khá chậm" hơn LOAD_FAST, vì vậy nó làm cho các phiên bản "năng suất" nhanh hơn một chút so với các biểu hiện máy phát điện cho các giá trị đủ lớn x(vòng ngoài) vì giá trị của yđược nạp nhanh hơn một chút trên từng đường chuyền. Đối với các giá trị nhỏ hơn, xnó sẽ chậm hơn một chút do mã thiết lập tăng thêm.

Nó cũng có thể đáng để chỉ ra rằng biểu thức trình tạo thường sẽ được sử dụng nội tuyến trong mã, thay vì gói nó bằng một hàm như thế. Điều đó sẽ loại bỏ một chút chi phí thiết lập và giữ cho biểu thức trình tạo nhanh hơn một chút đối với các giá trị vòng lặp nhỏ hơn ngay cả khi LOAD_FASTđã mang lại cho phiên bản "lợi nhuận" một lợi thế khác.

Trong mọi trường hợp, sự khác biệt về hiệu suất sẽ không đủ để biện minh cho việc quyết định giữa cái này hay cái kia. Khả năng đọc được tính nhiều hơn, vì vậy hãy sử dụng bất kỳ cái nào cảm thấy dễ đọc nhất cho tình huống hiện tại.

>>> def Generator(x, y):
...     for i in xrange(x):
...         for j in xrange(y):
...             yield(i, j)
...
>>> dis.dis(Generator)
  2           0 SETUP_LOOP              54 (to 57)
              3 LOAD_GLOBAL              0 (xrange)
              6 LOAD_FAST                0 (x)
              9 CALL_FUNCTION            1
             12 GET_ITER
        >>   13 FOR_ITER                40 (to 56)
             16 STORE_FAST               2 (i)

  3          19 SETUP_LOOP              31 (to 53)
             22 LOAD_GLOBAL              0 (xrange)
             25 LOAD_FAST                1 (y)
             28 CALL_FUNCTION            1
             31 GET_ITER
        >>   32 FOR_ITER                17 (to 52)
             35 STORE_FAST               3 (j)

  4          38 LOAD_FAST                2 (i)
             41 LOAD_FAST                3 (j)
             44 BUILD_TUPLE              2
             47 YIELD_VALUE
             48 POP_TOP
             49 JUMP_ABSOLUTE           32
        >>   52 POP_BLOCK
        >>   53 JUMP_ABSOLUTE           13
        >>   56 POP_BLOCK
        >>   57 LOAD_CONST               0 (None)
             60 RETURN_VALUE
>>> def Generator_expr(x, y):
...    return ((i, j) for i in xrange(x) for j in xrange(y))
...
>>> dis.dis(Generator_expr.func_code.co_consts[1])
  2           0 SETUP_LOOP              47 (to 50)
              3 LOAD_FAST                0 (.0)
        >>    6 FOR_ITER                40 (to 49)
              9 STORE_FAST               1 (i)
             12 SETUP_LOOP              31 (to 46)
             15 LOAD_GLOBAL              0 (xrange)
             18 LOAD_DEREF               0 (y)
             21 CALL_FUNCTION            1
             24 GET_ITER
        >>   25 FOR_ITER                17 (to 45)
             28 STORE_FAST               2 (j)
             31 LOAD_FAST                1 (i)
             34 LOAD_FAST                2 (j)
             37 BUILD_TUPLE              2
             40 YIELD_VALUE
             41 POP_TOP
             42 JUMP_ABSOLUTE           25
        >>   45 POP_BLOCK
        >>   46 JUMP_ABSOLUTE            6
        >>   49 POP_BLOCK
        >>   50 LOAD_CONST               0 (None)
             53 RETURN_VALUE

Được chấp nhận - để giải thích chi tiết về sự khác biệt bằng cách sử dụng dis. Cảm ơn!
cschol

Tôi đã cập nhật để bao gồm một liên kết đến một nguồn mà tuyên bố LOAD_DEREFlà "khá chậm hơn", vì vậy nếu hiệu suất thực sự quan trọng thì một số thời gian thực timeitsẽ tốt. Một phân tích lý thuyết chỉ đi xa cho đến nay.
Peter Hansen

36

Trong ví dụ này, không thực sự. Nhưng yieldcó thể được sử dụng cho các cấu trúc phức tạp hơn - ví dụ như nó cũng có thể chấp nhận các giá trị từ trình gọi và kết quả là sửa đổi luồng. Đọc PEP 342 để biết thêm chi tiết (đó là một kỹ thuật thú vị đáng biết).

Dù sao, lời khuyên tốt nhất là sử dụng bất cứ điều gì rõ ràng hơn cho nhu cầu của bạn .

Tái bút Đây là một ví dụ về quy trình đăng quang đơn giản từ Dave Beazley :

def grep(pattern):
    print "Looking for %s" % pattern
    while True:
        line = (yield)
        if pattern in line:
            print line,

# Example use
if __name__ == '__main__':
    g = grep("python")
    g.next()
    g.send("Yeah, but no, but yeah, but no")
    g.send("A series of tubes")
    g.send("python generators rock!")

8
+1 để liên kết với David Beazley. Bài thuyết trình của anh ấy về các quy trình khám nghiệm là điều đáng suy nghĩ nhất mà tôi đã đọc trong một thời gian dài. Có thể không hữu ích bằng bài thuyết trình của anh ấy về máy phát điện, nhưng dù sao thì cũng tuyệt vời.
Robert Rossney

18

Không có sự khác biệt nào đối với loại vòng lặp đơn giản mà bạn có thể phù hợp với biểu thức trình tạo. Tuy nhiên, năng suất có thể được sử dụng để tạo ra các máy phát điện thực hiện quá trình xử lý phức tạp hơn nhiều. Đây là một ví dụ đơn giản để tạo chuỗi fibonacci:

>>> def fibgen():
...    a = b = 1
...    while True:
...        yield a
...        a, b = b, a+b

>>> list(itertools.takewhile((lambda x: x<100), fibgen()))
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89]

5
+1 điều đó thật tuyệt vời ... không thể nói rằng tôi đã từng thấy cách triển khai fib ngắn và ngọt ngào như vậy mà không cần đệ quy.
JudoWill

Đoạn mã đơn giản đến khó tin - Tôi nghĩ Fibonacci sẽ rất vui khi thấy nó !!
sử dụng Asterix

10

Trong cách sử dụng, hãy lưu ý sự phân biệt giữa đối tượng trình tạo và chức năng trình tạo.

Đối tượng trình tạo là chỉ sử dụng một lần, trái ngược với hàm trình tạo, có thể được sử dụng lại mỗi khi bạn gọi lại, vì nó trả về đối tượng trình tạo mới.

Trong thực tế, các biểu thức của trình tạo thường được sử dụng "raw", không gói chúng trong một hàm và chúng trả về một đối tượng của trình tạo.

Ví dụ:

def range_10_gen_func():
    x = 0
    while x < 10:
        yield x
        x = x + 1

print(list(range_10_gen_func()))
print(list(range_10_gen_func()))
print(list(range_10_gen_func()))

kết quả đầu ra:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

So sánh với cách sử dụng hơi khác:

range_10_gen = range_10_gen_func()
print(list(range_10_gen))
print(list(range_10_gen))
print(list(range_10_gen))

kết quả đầu ra:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[]
[]

Và so sánh với biểu thức trình tạo:

range_10_gen_expr = (x for x in range(10))
print(list(range_10_gen_expr))
print(list(range_10_gen_expr))
print(list(range_10_gen_expr))

mà cũng xuất ra:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
[]
[]

8

Sử dụng yieldlà tốt nếu biểu thức phức tạp hơn chỉ là các vòng lặp lồng nhau. Trong số những thứ khác, bạn có thể trả về giá trị đầu tiên hoặc giá trị cuối cùng đặc biệt. Xem xét:

def Generator(x):
  for i in xrange(x):
    yield(i)
  yield(None)

5

Khi nghĩ về trình vòng lặp, itertoolsmô-đun:

... tiêu chuẩn hóa một tập hợp cốt lõi của các công cụ nhanh, hiệu quả về bộ nhớ hữu ích tự chúng hoặc kết hợp với nhau. Cùng với nhau, chúng tạo thành một “đại số trình lặp” để có thể tạo các công cụ chuyên dụng một cách ngắn gọn và hiệu quả bằng Python thuần túy.

Đối với hiệu suất, hãy xem xét itertools.product(*iterables[, repeat])

Tích số Descartes của các đầu vào lặp lại.

Tương đương với vòng lặp for lồng nhau trong biểu thức trình tạo. Ví dụ, product(A, B)trả về giống như ((x,y) for x in A for y in B).

>>> import itertools
>>> def gen(x,y):
...     return itertools.product(xrange(x),xrange(y))
... 
>>> [t for t in gen(3,2)]
[(0, 0), (0, 1), (1, 0), (1, 1), (2, 0), (2, 1)]
>>> 

4

Có một sự khác biệt.

Đối với biểu thức trình tạo (x for var in expr), iter(expr)được gọi khi biểu thức được tạo .

Khi sử dụng defyieldtạo trình tạo, như trong:

def my_generator():
    for var in expr:
        yield x

g = my_generator()

iter(expr)vẫn chưa được gọi. Nó sẽ chỉ được gọi khi lặp lại trên g(và có thể hoàn toàn không được gọi).

Lấy trình lặp này làm ví dụ:

from __future__ import print_function


class CountDown(object):
    def __init__(self, n):
        self.n = n

    def __iter__(self):
        print("ITER")
        return self

    def __next__(self):
        if self.n == 0:
            raise StopIteration()
        self.n -= 1
        return self.n

    next = __next__  # for python2

Mã này:

g1 = (i ** 2 for i in CountDown(3))  # immediately prints "ITER"
print("Go!")
for x in g1:
    print(x)

trong khi:

def my_generator():
    for i in CountDown(3):
        yield i ** 2


g2 = my_generator()
print("Go!")
for x in g2:  # "ITER" is only printed here
    print(x)

Vì hầu hết các trình lặp không thực hiện nhiều nội dung __iter__, nên rất dễ bỏ lỡ hành vi này. Một ví dụ trong thế giới thực sẽ là Django's QuerySet, tìm nạp dữ liệu__iter__data = (f(x) for x in qs)có thể mất rất nhiều thời gian, trong khi def g(): for x in qs: yield f(x)theo sau data=g()sẽ trả về ngay lập tức.

Để biết thêm thông tin và định nghĩa chính thức, hãy tham khảo PEP 289 - Biểu thức máy phát điện .


0

Có một sự khác biệt có thể quan trọng trong một số bối cảnh vẫn chưa được chỉ ra. Việc sử dụng yieldngăn bạn sử dụng returncho việc gì khác ngoài việc ngầm nâng cấp StopIteration (và những thứ liên quan đến coroutines) .

Điều này có nghĩa là mã này không hợp lệ (và việc cung cấp mã này cho thông dịch viên sẽ cung cấp cho bạn AttributeError):

class Tea:

    """With a cloud of milk, please"""

    def __init__(self, temperature):
        self.temperature = temperature

def mary_poppins_purse(tea_time=False):
    """I would like to make one thing clear: I never explain anything."""
    if tea_time:
        return Tea(355)
    else:
        for item in ['lamp', 'mirror', 'coat rack', 'tape measure', 'ficus']:
            yield item

print(mary_poppins_purse(True).temperature)

Mặt khác, mã này hoạt động giống như một sự quyến rũ:

class Tea:

    """With a cloud of milk, please"""

    def __init__(self, temperature):
        self.temperature = temperature

def mary_poppins_purse(tea_time=False):
    """I would like to make one thing clear: I never explain anything."""
    if tea_time:
        return Tea(355)
    else:
        return (item for item in ['lamp', 'mirror', 'coat rack',
                                  'tape measure', 'ficus'])

print(mary_poppins_purse(True).temperature)
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.