Xây dựng một trình lặp Python cơ bản


569

Làm thế nào người ta có thể tạo một hàm lặp (hoặc đối tượng lặp) trong python?

Câu trả lời:


650

Các đối tượng lặp trong python tuân thủ giao thức iterator, về cơ bản có nghĩa là chúng cung cấp hai phương thức: __iter__()__next__().

  • Trả __iter__về đối tượng iterator và được gọi ngầm khi bắt đầu các vòng lặp.

  • Các __next__()phương thức trả về giá trị tiếp theo và được mặc nhiên được gọi ở mỗi số gia vòng lặp. Phương thức này làm tăng ngoại lệ StopIteration khi không còn giá trị nào để trả về, được ngầm định nắm bắt bằng cách lặp các cấu trúc để dừng lặp.

Đây là một ví dụ đơn giản về bộ đếm:

class Counter:
    def __init__(self, low, high):
        self.current = low - 1
        self.high = high

    def __iter__(self):
        return self

    def __next__(self): # Python 2: def next(self)
        self.current += 1
        if self.current < self.high:
            return self.current
        raise StopIteration


for c in Counter(3, 9):
    print(c)

Điều này sẽ in:

3
4
5
6
7
8

Điều này dễ viết hơn bằng cách sử dụng một trình tạo, như được trình bày trong câu trả lời trước:

def counter(low, high):
    current = low
    while current < high:
        yield current
        current += 1

for c in counter(3, 9):
    print(c)

Đầu ra được in sẽ giống nhau. Dưới mui xe, đối tượng trình tạo hỗ trợ giao thức iterator và thực hiện một cái gì đó gần giống với bộ đếm lớp.

Bài viết của David Mertz, Iterators và Simple Generators , là một giới thiệu khá hay.


4
Đây chủ yếu là một câu trả lời tốt, nhưng thực tế là nó trả về bản thân là một chút tối ưu. Ví dụ: nếu bạn đã sử dụng cùng một đối tượng truy cập trong một vòng lặp gấp đôi lồng nhau, bạn có thể sẽ không có được hành vi mà bạn muốn nói.
Casey Rodarmor 6/214

22
Không, lặp đi lặp lại NÊN trở lại. Lặp lại trả về các vòng lặp, nhưng các vòng lặp không nên thực hiện __next__. counterlà một trình vòng lặp, nhưng nó không phải là một chuỗi. Nó không lưu trữ giá trị của nó. Ví dụ, bạn không nên sử dụng bộ đếm trong một vòng lặp for lồng nhau.
leewz

4
Trong ví dụ về Counter, self.civerse nên được gán trong __iter__(ngoài in __init__). Mặt khác, đối tượng có thể được lặp lại một lần. Ví dụ, nếu bạn nói ctr = Counters(3, 8), thì bạn không thể sử dụng for c in ctrnhiều hơn một lần.
Curt

7
@Curt: Hoàn toàn không. Counterlà một trình vòng lặp và các trình vòng lặp chỉ được cho là được lặp lại một lần. Nếu bạn đặt lại self.currenttrong __iter__, sau đó một vòng lặp lồng nhau trên Countersẽ được hoàn toàn bị phá vỡ, và tất cả các loại hành vi giả của vòng lặp (đó gọi itervào số đó là idempotent) bị vi phạm. Nếu bạn muốn có thể lặp lại ctrnhiều lần, nó cần phải là một trình vòng lặp không lặp, nơi nó trả về một trình vòng lặp hoàn toàn mới mỗi lần __iter__được gọi. Cố gắng trộn và kết hợp (một trình vòng lặp được đặt lại ngầm khi __iter__được gọi) vi phạm các giao thức.
ShadowRanger

2
Ví dụ: nếu Counterlà một trình lặp không lặp, bạn sẽ xóa định nghĩa __next__/ nexthoàn toàn và có thể xác định lại __iter__là hàm tạo có cùng dạng với trình tạo được mô tả ở cuối câu trả lời này (ngoại trừ các giới hạn đến từ các đối số __iter__, chúng sẽ là các đối số được __init__lưu selfvà truy cập từ selftrong __iter__).
ShadowRanger

427

Có bốn cách để xây dựng hàm lặp:

Ví dụ:

# generator
def uc_gen(text):
    for char in text.upper():
        yield char

# generator expression
def uc_genexp(text):
    return (char for char in text.upper())

# iterator protocol
class uc_iter():
    def __init__(self, text):
        self.text = text.upper()
        self.index = 0
    def __iter__(self):
        return self
    def __next__(self):
        try:
            result = self.text[self.index]
        except IndexError:
            raise StopIteration
        self.index += 1
        return result

# getitem method
class uc_getitem():
    def __init__(self, text):
        self.text = text.upper()
    def __getitem__(self, index):
        return self.text[index]

Để xem tất cả bốn phương thức đang hoạt động:

for iterator in uc_gen, uc_genexp, uc_iter, uc_getitem:
    for ch in iterator('abcde'):
        print(ch, end=' ')
    print()

Kết quả nào trong:

A B C D E
A B C D E
A B C D E
A B C D E

Lưu ý :

Hai loại máy phát ( uc_genuc_genexp) không thể là reversed(); iterator đơn giản ( uc_iter) sẽ cần __reversed__phương thức ma thuật (mà theo các tài liệu , phải trả về một trình vòng lặp mới, nhưng trả về selfcác tác phẩm (ít nhất là trong CPython)); và getitem iterizable ( uc_getitem) phải có __len__phương thức ma thuật:

    # for uc_iter we add __reversed__ and update __next__
    def __reversed__(self):
        self.index = -1
        return self
    def __next__(self):
        try:
            result = self.text[self.index]
        except IndexError:
            raise StopIteration
        self.index += -1 if self.index < 0 else +1
        return result

    # for uc_getitem
    def __len__(self)
        return len(self.text)

Để trả lời câu hỏi phụ của Đại tá Panic về một trình vòng lặp được đánh giá lười biếng vô hạn, đây là những ví dụ, sử dụng một trong bốn phương pháp trên:

# generator
def even_gen():
    result = 0
    while True:
        yield result
        result += 2


# generator expression
def even_genexp():
    return (num for num in even_gen())  # or even_iter or even_getitem
                                        # not much value under these circumstances

# iterator protocol
class even_iter():
    def __init__(self):
        self.value = 0
    def __iter__(self):
        return self
    def __next__(self):
        next_value = self.value
        self.value += 2
        return next_value

# getitem method
class even_getitem():
    def __getitem__(self, index):
        return index * 2

import random
for iterator in even_gen, even_genexp, even_iter, even_getitem:
    limit = random.randint(15, 30)
    count = 0
    for even in iterator():
        print even,
        count += 1
        if count >= limit:
            break
    print

Kết quả nào (ít nhất là cho lần chạy mẫu của tôi):

0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40 42 44 46 48 50 52 54
0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38
0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30
0 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32

Làm thế nào để chọn cái nào để sử dụng? Đây chủ yếu là một vấn đề của hương vị. Hai phương thức tôi thấy thường xuyên nhất là trình tạo và giao thức lặp, cũng như kết hợp ( __iter__trả về một trình tạo).

Các biểu thức của trình tạo rất hữu ích để thay thế việc hiểu danh sách (chúng lười biếng và do đó có thể tiết kiệm tài nguyên).

Nếu một người cần tương thích với các phiên bản Python 2.x trước đó, hãy sử dụng __getitem__.


4
Tôi thích tóm tắt này vì nó đã hoàn thành. Ba cách đó (năng suất, biểu thức trình tạo và trình lặp) về cơ bản là giống nhau, mặc dù một số cách thuận tiện hơn các cách khác. Toán tử năng suất nắm bắt "sự tiếp tục" có chứa trạng thái (ví dụ: chỉ số mà chúng ta đang có). Thông tin được lưu trong phần "đóng" của phần tiếp theo. Cách lặp lặp lưu thông tin tương tự bên trong các trường của trình vòng lặp, về cơ bản giống như cách đóng. Các GetItem phương pháp là một khác nhau chút vì nó chỉ vào nội dung và không phải là lặp đi lặp lại trong tự nhiên.
Ian

2
@metaperl: Thật ra là vậy. Trong tất cả bốn trường hợp trên, bạn có thể sử dụng cùng một mã để lặp lại.
Ethan Furman

1
@Asterisk: Không, một trường hợp uc_iternên hết hạn khi hoàn thành (nếu không thì sẽ vô hạn); nếu bạn muốn làm lại, bạn phải có một trình vòng lặp mới bằng cách gọi uc_iter()lại.
Ethan Furman

2
Bạn có thể đặt self.index = 0trong __iter__để bạn có thể lặp lại nhiều lần. Nếu không thì bạn không thể.
John Strood

1
Nếu bạn có thể dành thời gian, tôi sẽ đánh giá cao một lời giải thích cho lý do tại sao bạn chọn bất kỳ phương pháp nào so với các phương pháp khác.
aaaaaa

103

Trước hết, mô-đun itertools cực kỳ hữu ích cho tất cả các loại trường hợp trong đó một trình vòng lặp sẽ hữu ích, nhưng đây là tất cả những gì bạn cần để tạo một trình vòng lặp trong python:

năng suất

Điều đó không tuyệt sao? Năng suất có thể được sử dụng để thay thế một lợi nhuận bình thường trong một chức năng. Nó trả về đối tượng giống nhau, nhưng thay vì hủy trạng thái và thoát, nó lưu trạng thái khi bạn muốn thực hiện lần lặp tiếp theo. Dưới đây là một ví dụ về nó trong hành động được lấy trực tiếp từ danh sách hàm itertools :

def count(n=0):
    while True:
        yield n
        n += 1

Như đã nêu trong mô tả hàm (đó là hàm Count () từ mô đun itertools ...), nó tạo ra một trình vòng lặp trả về các số nguyên liên tiếp bắt đầu bằng n.

Các biểu thức của trình tạo là một loạt các con sâu khác (những con sâu tuyệt vời!). Chúng có thể được sử dụng thay cho Hiểu danh sách để lưu bộ nhớ (việc hiểu danh sách tạo ra một danh sách trong bộ nhớ bị hủy sau khi sử dụng nếu không được gán cho một biến, nhưng các biểu thức trình tạo có thể tạo Đối tượng trình tạo ... đó là một cách ưa thích nói lặp đi lặp lại). Dưới đây là một ví dụ về định nghĩa biểu thức trình tạo:

gen = (n for n in xrange(0,11))

Điều này rất giống với định nghĩa lặp của chúng tôi ở trên, ngoại trừ phạm vi đầy đủ được xác định trước là từ 0 đến 10.

Tôi chỉ tìm thấy xrange () (ngạc nhiên là tôi chưa từng thấy nó trước đây ...) và thêm nó vào ví dụ trên. xrange () là một phiên bản lặp lại của phạm vi () có lợi thế là không xây dựng trước danh sách. Sẽ rất hữu ích nếu bạn có một khối dữ liệu khổng lồ để lặp đi lặp lại và chỉ có quá nhiều bộ nhớ để thực hiện.


20
kể từ python 3.0, không còn xrange () và phạm vi mới () hoạt động giống như xrange ()

6
Bạn vẫn nên sử dụng xrange trong 2._, vì 2to3 tự động dịch nó.
Phob

100

Tôi thấy một số bạn làm return selftrong __iter__. Tôi chỉ muốn lưu ý rằng __iter__chính nó có thể là một trình tạo (do đó loại bỏ sự cần thiết __next__và nâng cao StopIterationngoại lệ)

class range:
  def __init__(self,a,b):
    self.a = a
    self.b = b
  def __iter__(self):
    i = self.a
    while i < self.b:
      yield i
      i+=1

Tất nhiên ở đây người ta cũng có thể trực tiếp tạo ra một trình tạo, nhưng đối với các lớp phức tạp hơn thì nó có thể hữu ích.


5
Tuyệt quá! Thật nhàm chán khi viết chỉ return selftrong __iter__. Khi tôi định thử sử dụng yieldnó, tôi thấy mã của bạn đang hoạt động chính xác những gì tôi muốn thử.
Ray

3
Nhưng trong trường hợp này, làm thế nào một người sẽ thực hiện next()? return iter(self).next()?
Lenna

4
@Lenna, nó đã được "triển khai" vì iter (self) trả về một iterator, không phải là một thể hiện phạm vi.
Manux

3
Đây là cách dễ nhất để làm điều đó và không liên quan đến việc phải theo dõi ví dụ self.currenthoặc bất kỳ bộ đếm nào khác. Đây phải là câu trả lời được bình chọn hàng đầu!
chiêm tinh

4
Để rõ ràng, cách tiếp cận này làm cho lớp của bạn lặp đi lặp lại , nhưng không phải là một trình vòng lặp . Bạn nhận được các trình vòng lặp mới mỗi khi bạn gọi các iterthể hiện của lớp, nhưng chúng không phải là các thể hiện của lớp.
ShadowRanger

13

Câu hỏi này là về các đối tượng lặp, không phải về các vòng lặp. Trong Python, các chuỗi cũng có thể lặp lại được vì vậy một cách để tạo một lớp lặp là làm cho nó hoạt động giống như một chuỗi, tức là đưa ra nó __getitem____len__các phương thức. Tôi đã thử nghiệm điều này trên Python 2 và 3.

class CustomRange:

    def __init__(self, low, high):
        self.low = low
        self.high = high

    def __getitem__(self, item):
        if item >= len(self):
            raise IndexError("CustomRange index out of range")
        return self.low + item

    def __len__(self):
        return self.high - self.low


cr = CustomRange(0, 10)
for i in cr:
    print(i)

1
Nó không phải có một __len__()phương pháp. __getitem__Một mình với hành vi dự kiến ​​là đủ.
BlackJack

5

Tất cả các câu trả lời trên trang này thực sự tuyệt vời cho một đối tượng phức tạp. Nhưng đối với những người chứa được xây dựng trong các loại iterable như là thuộc tính, giống như str, list, sethay dict, hay bất cứ thực hiện collections.Iterable, bạn có thể bỏ qua những điều nào đó trong lớp học của bạn.

class Test(object):
    def __init__(self, string):
        self.string = string

    def __iter__(self):
        # since your string is already iterable
        return (ch for ch in self.string)
        # or simply
        return self.string.__iter__()
        # also
        return iter(self.string)

Nó có thể được sử dụng như:

for x in Test("abcde"):
    print(x)

# prints
# a
# b
# c
# d
# e

1
Như bạn đã nói, chuỗi đã được lặp lại, vậy tại sao biểu thức trình tạo thêm ở giữa thay vì chỉ hỏi chuỗi cho trình lặp (mà biểu thức trình tạo thực hiện bên trong) : return iter(self.string).
BlackJack

@BlackJack Bạn thực sự đúng. Tôi không biết điều gì đã thuyết phục tôi viết theo cách đó. Có lẽ tôi đã cố gắng tránh bất kỳ sự nhầm lẫn nào trong một câu trả lời khi cố gắng giải thích hoạt động của cú pháp lặp theo thuật ngữ cú pháp lặp nhiều hơn.
John Strood

3

Đây là một chức năng lặp mà không có yield. Nó sử dụng iterhàm và một bao đóng giữ trạng thái của nó ở trạng thái có thể thay đổi ( list) trong phạm vi kèm theo cho python 2.

def count(low, high):
    counter = [0]
    def tmp():
        val = low + counter[0]
        if val < high:
            counter[0] += 1
            return val
        return None
    return iter(tmp, None)

Đối với Python 3, trạng thái đóng được giữ ở mức không thay đổi trong phạm vi kèm theo và nonlocalđược sử dụng trong phạm vi cục bộ để cập nhật biến trạng thái.

def count(low, high):
    counter = 0
    def tmp():
        nonlocal counter
        val = low + counter
        if val < high:
            counter += 1
            return val
        return None
    return iter(tmp, None)  

Kiểm tra;

for i in count(1,10):
    print(i)
1
2
3
4
5
6
7
8
9

Tôi luôn đánh giá cao việc sử dụng hai arg một cách thông minh iter, nhưng chỉ cần rõ ràng: Điều này phức tạp và kém hiệu quả hơn so với việc chỉ sử dụng yieldchức năng tạo dựa trên; Python có rất nhiều hỗ trợ trình thông dịch cho các yieldhàm tạo dựa trên mà bạn không thể tận dụng ở đây, làm cho mã này chậm hơn đáng kể. Tuy nhiên, đã bỏ phiếu.
ShadowRanger

2

Nếu bạn đang tìm kiếm một cái gì đó ngắn gọn và đơn giản, có lẽ nó sẽ đủ cho bạn:

class A(object):
    def __init__(self, l):
        self.data = l

    def __iter__(self):
        return iter(self.data)

ví dụ về việc sử dụng:

In [3]: a = A([2,3,4])

In [4]: [i for i in a]
Out[4]: [2, 3, 4]

-1

Lấy cảm hứng từ câu trả lời của Matt Gregory ở đây là một trình vòng lặp phức tạp hơn một chút sẽ trả về a, b, ..., z, aa, ab, ..., zz, aaa, aab, ..., zzy, zzz

    class AlphaCounter:
    def __init__(self, low, high):
        self.current = low
        self.high = high

    def __iter__(self):
        return self

    def __next__(self): # Python 3: def __next__(self)
        alpha = ' abcdefghijklmnopqrstuvwxyz'
        n_current = sum([(alpha.find(self.current[x])* 26**(len(self.current)-x-1)) for x in range(len(self.current))])
        n_high = sum([(alpha.find(self.high[x])* 26**(len(self.high)-x-1)) for x in range(len(self.high))])
        if n_current > n_high:
            raise StopIteration
        else:
            increment = True
            ret = ''
            for x in self.current[::-1]:
                if 'z' == x:
                    if increment:
                        ret += 'a'
                    else:
                        ret += 'z'
                else:
                    if increment:
                        ret += alpha[alpha.find(x)+1]
                        increment = False
                    else:
                        ret += x
            if increment:
                ret += 'a'
            tmp = self.current
            self.current = ret[::-1]
            return tmp

for c in AlphaCounter('a', 'zzz'):
    print(c)
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.