สร้าง Python Iterator พื้นฐาน


569

เราจะสร้างฟังก์ชั่นวนซ้ำ (หรือวัตถุตัววนซ้ำ) ในไพ ธ อนได้อย่างไร?

คำตอบ:


650

iterator วัตถุในหลามสอดคล้องกับโปรโตคอล iterator ซึ่งโดยทั่วไปหมายถึงพวกเขาให้สองวิธี: และ __iter__() __next__()

  • __iter__ส่งกลับวัตถุ iterator และเป็นโดยปริยายเรียกว่าจุดเริ่มต้นของลูป

  • __next__()วิธีการส่งกลับค่าถัดไปโดยปริยายและเป็นที่เรียกว่าในแต่ละวงที่เพิ่มขึ้น วิธีนี้ทำให้เกิดข้อยกเว้น StopIteration เมื่อไม่มีค่าที่จะส่งคืนซึ่งถูกจับโดยปริยายโดยการวนลูปสร้างเพื่อหยุดการทำซ้ำ

นี่คือตัวอย่างง่ายๆของตัวนับ:

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)

สิ่งนี้จะพิมพ์:

3
4
5
6
7
8

การเขียนโดยใช้ตัวสร้างง่ายกว่าดังที่กล่าวไว้ในคำตอบก่อนหน้า:

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

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

ผลงานพิมพ์จะเหมือนกัน ภายใต้ฝากระโปรงตัวสร้างวัตถุรองรับโปรโตคอลตัววนซ้ำและทำสิ่งที่คล้ายกับตัวนับคลาส

บทความของ David Mertz คือIterators และ Simple Generatorsเป็นการแนะนำที่ดีงาม


4
นี่เป็นคำตอบที่ดีส่วนใหญ่ แต่ความจริงที่ว่าการคืนตัวเองนั้นดีกว่าเล็กน้อย ตัวอย่างเช่นถ้าคุณใช้วัตถุตัวนับเดียวกันในซ้อนซ้อนสองเท่าสำหรับลูปคุณอาจไม่ได้รับพฤติกรรมที่คุณต้องการ
Casey Rodarmor

22
ไม่ผู้ทำซ้ำควรส่งคืนตนเอง Iterables กลับ iterators แต่ iterables __next__ไม่ควรดำเนินการ counterเป็นตัววนซ้ำ แต่ไม่ใช่ลำดับ มันไม่เก็บค่าของมัน คุณไม่ควรใช้ตัวนับในวงวนซ้ำซ้อนกันเป็นสองเท่า
leewz

4
ในตัวอย่างตัวนับควรกำหนด self.current ใน__iter__(นอกเหนือจากใน__init__) มิฉะนั้นวัตถุสามารถทำซ้ำได้เพียงครั้งเดียว เช่นถ้าคุณพูดctr = Counters(3, 8)แล้วคุณจะไม่สามารถใช้งานได้for c in ctrมากกว่าหนึ่งครั้ง
Curt

7
@Curt: ไม่อย่างแน่นอน Counterเป็นตัววนซ้ำและตัววนซ้ำนั้นควรทำซ้ำครั้งเดียวเท่านั้น หากคุณรีเซ็ตself.currentใน__iter__แล้ววงที่ซ้อนกันมากกว่าCounterก็จะถูกทำลายอย่างสมบูรณ์และทุกประเภทของพฤติกรรมการสันนิษฐานของ iterators (ที่เรียกiterพวกเขาเป็น idempotent) มีการละเมิด หากคุณต้องการที่จะวนซ้ำctrมากกว่าหนึ่งครั้งจะต้องเป็นตัววนซ้ำที่ไม่ใช่ตัววนซ้ำซึ่งจะส่งคืนตัววนซ้ำตัวใหม่ทุกครั้งที่__iter__ถูกเรียกใช้ กำลังพยายามมิกซ์และจับคู่ (ตัววนซ้ำที่ถูกรีเซ็ตโดยปริยายเมื่อ__iter__ถูกเรียกใช้) ละเมิดโปรโตคอล
ShadowRanger

2
ตัวอย่างเช่นหากCounterจะเป็นตัวทำซ้ำที่ไม่ใช่ตัวทำซ้ำคุณต้องลบคำจำกัดความของ__next__/ nextทั้งหมดและอาจนิยามใหม่__iter__ว่าเป็นฟังก์ชันตัวกำเนิดของรูปแบบเดียวกับตัวสร้างที่อธิบายไว้ท้ายคำตอบนี้ (ยกเว้นแทนขอบเขต มาจากการโต้เถียง__iter__พวกเขาจะเป็นข้อโต้แย้งเพื่อ__init__บันทึกselfและเข้าถึงได้จากselfใน__iter__)
ShadowRanger

427

มีสี่วิธีในการสร้างฟังก์ชันวนซ้ำ:

ตัวอย่าง:

# 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]

วิธีดูการทำงานทั้งสี่วิธี:

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

ซึ่งผลลัพธ์ใน:

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

หมายเหตุ :

เครื่องกำเนิดไฟฟ้าสองประเภท ( uc_genและuc_genexp) ไม่สามารถเป็นได้reversed(); the iterator ธรรมดา ( uc_iter) จะต้องใช้__reversed__วิธีเวทย์มนตร์ (ซึ่งตามเอกสารจะต้องส่งคืนตัววนซ้ำใหม่ แต่กลับselfทำงาน (อย่างน้อยใน CPython)); และ getitem iteratable ( uc_getitem) จะต้องมี__len__วิธีเวทย์มนตร์:

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

เพื่อตอบคำถามที่สองของพันเอก Panic เกี่ยวกับตัววนซ้ำที่ประเมินอย่างเกียจคร้านนี่คือตัวอย่างเหล่านั้นโดยใช้แต่ละวิธีทั้งสี่ด้านบน:

# 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

ผลลัพธ์ใดที่ (อย่างน้อยสำหรับการทดสอบตัวอย่างของฉัน):

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

วิธีการเลือกใช้อันไหน นี่เป็นเรื่องของรสนิยมเป็นส่วนใหญ่ ทั้งสองวิธีที่ผมเห็นส่วนใหญ่มักจะมีเครื่องกำเนิดไฟฟ้าและโปรโตคอล iterator เช่นเดียวกับไฮบริด ( __iter__กลับเครื่องกำเนิดไฟฟ้า)

นิพจน์ตัวสร้างมีประโยชน์สำหรับการแทนที่ความเข้าใจในรายการ (ขี้เกียจและสามารถประหยัดทรัพยากรได้)

หากต้องการความเข้ากันได้กับรุ่น Python 2.x รุ่น__getitem__ก่อน


4
ฉันชอบบทสรุปนี้เพราะมันเสร็จสมบูรณ์ สามวิธีเหล่านี้ (อัตราผลตอบแทนนิพจน์ตัวสร้างและตัววนซ้ำ) เป็นหลักเหมือนกันแม้ว่าบางวิธีจะสะดวกกว่าวิธีอื่น ตัวดำเนินการให้ผลตอบแทนจับ "ความต่อเนื่อง" ซึ่งมีสถานะ (ตัวอย่างเช่นดัชนีที่เรามีมากถึง) ข้อมูลจะถูกบันทึกใน "การปิด" ของความต่อเนื่อง วิธีตัววนซ้ำบันทึกข้อมูลเดียวกันภายในเขตข้อมูลของตัววนซ้ำซึ่งโดยพื้นฐานแล้วเป็นสิ่งเดียวกับการปิด GetItemเป็นวิธีการที่แตกต่างกันเล็กน้อยเนื่องจากดัชนีมันเข้าไปในเนื้อหาและไม่ซ้ำในธรรมชาติ
เอียน

2
@metaperl: อันที่จริงมันคือ ในทั้งสี่กรณีข้างต้นคุณสามารถใช้รหัสเดียวกันเพื่อวนซ้ำ
Ethan Furman

1
@Asterisk: ไม่ตัวอย่างของuc_iterควรจะหมดอายุเมื่อเสร็จสิ้น (มิฉะนั้นจะไม่มีที่สิ้นสุด); ถ้าคุณต้องการที่จะทำมันอีกครั้งคุณจะต้องได้รับ iterator ใหม่โดยการโทรuc_iter()อีกครั้ง
Ethan Furman

2
คุณสามารถตั้งค่าself.index = 0ใน__iter__เพื่อให้คุณสามารถย้ำหลายครั้งกว่า มิฉะนั้นคุณไม่สามารถ
John Strood

1
หากคุณสามารถสละเวลาได้ฉันขอขอบคุณคำอธิบายว่าทำไมคุณถึงเลือกวิธีการอื่น ๆ
aaaaaa

103

ก่อนอื่นโมดูล itertoolsนั้นมีประโยชน์อย่างมากในทุกกรณีที่ตัววนซ้ำจะมีประโยชน์ แต่นี่คือทั้งหมดที่คุณต้องสร้างตัววนซ้ำในไพ ธ อน:

ผล

มันเจ๋งใช่มั้ย อัตราผลตอบแทนสามารถนำมาใช้เพื่อแทนที่ผลตอบแทนปกติในฟังก์ชั่น มันส่งคืนวัตถุเพียงเหมือนกัน แต่แทนที่จะทำลายสถานะและออกจากมันจะบันทึกสถานะเมื่อคุณต้องการดำเนินการซ้ำต่อไป นี่คือตัวอย่างของการดำเนินการดึงโดยตรงจากรายการฟังก์ชัน itertools :

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

ตามที่ระบุไว้ในคำอธิบายฟังก์ชั่น (มันเป็นฟังก์ชั่นการนับ ()จากโมดูล itertools ... ) มันสร้าง iterator ที่ส่งกลับจำนวนเต็มต่อเนื่องเริ่มต้นด้วย n

นิพจน์ตัวสร้างเป็นเวิร์มอื่น ๆ ทั้งหมด (เวิร์มที่ยอดเยี่ยม!) พวกมันอาจถูกนำมาใช้แทนที่List Comprehensionเพื่อบันทึกหน่วยความจำ (list comprehensions สร้างรายการในหน่วยความจำที่ถูกทำลายหลังการใช้งานหากไม่ได้กำหนดให้กับตัวแปร แต่นิพจน์ตัวสร้างสามารถสร้าง Generator Object ... ซึ่งเป็นวิธีที่ พูดซ้ำ) นี่คือตัวอย่างของคำนิยามนิพจน์ตัวสร้าง:

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

นี่คล้ายกับนิยามตัววนซ้ำของเราด้านบนยกเว้นช่วงเต็มรูปแบบที่กำหนดไว้ล่วงหน้าให้อยู่ระหว่าง 0 ถึง 10

ฉันเพิ่งพบxrange () (แปลกใจที่ฉันไม่เคยเห็นมาก่อน ... ) และเพิ่มเข้าไปในตัวอย่างด้านบน xrange ()เป็นรุ่นที่ทำซ้ำได้ของrange ()ซึ่งมีข้อได้เปรียบในการไม่สร้างรายการล่วงหน้า มันจะมีประโยชน์มากถ้าคุณมีคลังข้อมูลขนาดยักษ์ที่จะทำซ้ำและมีหน่วยความจำเหลือเฟือที่จะทำ


20
ตั้งแต่ python 3.0 ไม่มี xrange () และ range ใหม่ () จะทำงานเหมือน xrange เก่า ()

6
คุณควรใช้ xrange ใน 2._ เนื่องจาก 2to3 จะแปลโดยอัตโนมัติ
Phob

100

ผมเห็นบางส่วนของคุณทำในreturn self __iter__ฉันแค่ต้องการที่จะทราบว่า__iter__ตัวเองสามารถเป็นเครื่องกำเนิดไฟฟ้า (ดังนั้นจึงไม่จำเป็นต้อง__next__ยกและเพิ่มStopIterationข้อยกเว้น)

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

แน่นอนที่นี่หนึ่งอาจสร้างเครื่องกำเนิดไฟฟ้าโดยตรง แต่สำหรับชั้นเรียนที่ซับซ้อนมากขึ้นมันจะมีประโยชน์


5
ที่ดี! มันจึงน่าเบื่อเขียนเพียงในreturn self __iter__เมื่อฉันจะลองใช้yieldมันฉันพบว่ารหัสของคุณทำสิ่งที่ฉันต้องการลอง
เรย์

3
แต่ในกรณีนี้เราจะนำไปใช้next()อย่างไร? return iter(self).next()?
Lenna

4
@ Lenna มันได้รับการ "นำไปใช้แล้ว" เนื่องจาก iter (self) ส่งคืนตัววนซ้ำไม่ใช่อินสแตนซ์ของช่วง
Manux

3
วิธีนี้เป็นวิธีที่ง่ายที่สุดในการดำเนินการและไม่เกี่ยวข้องกับการติดตามเช่นself.currentหรือตัวนับอื่น ๆ นี่ควรเป็นคำตอบที่ได้รับคะแนนสูงสุด!
astrofrog

4
ต้องมีความชัดเจนวิธีการนี้จะทำให้การเรียนของคุณiterableแต่ไม่ได้เป็นiterator คุณจะได้รับตัววนซ้ำที่สดใหม่ทุกครั้งที่คุณเรียกiterใช้อินสแตนซ์ของคลาส แต่ไม่ใช่อินสแตนซ์ของคลาส
ShadowRanger

13

คำถามนี้เกี่ยวกับวัตถุที่ทำซ้ำได้ไม่ใช่เกี่ยวกับตัววนซ้ำ ใน Python ซีเควนซ์นั้นสามารถวนซ้ำได้เช่นกันดังนั้นวิธีหนึ่งในการสร้างคลาส iterable คือการทำให้มันทำงานเหมือนลำดับคือให้มัน__getitem__และ__len__เมธอด ฉันได้ทดสอบสิ่งนี้กับ Python 2 และ 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
มันไม่จำเป็นต้องมี__len__()วิธี __getitem__เพียงอย่างเดียวกับพฤติกรรมที่คาดหวังก็เพียงพอแล้ว
BlackJack

5

คำตอบทั้งหมดในหน้านี้ยอดเยี่ยมมากสำหรับวัตถุที่ซับซ้อน แต่สำหรับผู้ที่มี builtin ประเภท iterable เป็นคุณลักษณะเช่นstr, list, setหรือdictหรือการดำเนินการใด ๆ ที่collections.Iterableคุณสามารถละเว้นสิ่งบางอย่างในชั้นเรียนของคุณ

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)

มันสามารถใช้เช่น:

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

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

1
ในขณะที่คุณกล่าวว่าสตริงที่มีอยู่แล้วเพื่อให้ iterable ทำไมแสดงออกกำเนิดเสริมในระหว่างแทนเพียงขอสตริงสำหรับ iterator return iter(self.string)(ซึ่งแสดงออกกำเนิดไม่ภายใน):
BlackJack

@ BlackJack คุณพูดถูกจริงๆ ฉันไม่รู้ว่าฉันชักชวนให้เขียนแบบนั้น บางทีฉันพยายามหลีกเลี่ยงความสับสนในคำตอบพยายามอธิบายการทำงานของ iterator syntax ในรูปของ iterator syntax มากกว่า
John Strood

3

yieldนี้เป็นฟังก์ชั่นโดยไม่ต้อง iterable มันใช้ประโยชน์จากiterฟังก์ชั่นและการปิดซึ่งทำให้สถานะเป็นแบบไม่แน่นอน ( list) ในขอบเขตการล้อมรอบสำหรับ 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)

สำหรับ Python 3 สถานะการปิดจะยังคงอยู่ในสถานะไม่เปลี่ยนรูปในขอบเขตการปิดล้อมและnonlocalใช้ในขอบเขตภายในเพื่ออัพเดตตัวแปรสถานะ

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)  

ทดสอบ;

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

ฉันมักจะชื่นชมการใช้สองอาร์เจนอย่างชาญฉลาดiterแต่เพื่อความชัดเจน: นี่ซับซ้อนและมีประสิทธิภาพน้อยกว่าการใช้yieldฟังก์ชันตัวสร้างแบบอิง Python มีการสนับสนุนล่ามมากมายสำหรับyieldฟังก์ชั่นเครื่องกำเนิดไฟฟ้าที่คุณไม่สามารถใช้ประโยชน์จากที่นี่ทำให้รหัสนี้ช้าลงอย่างมีนัยสำคัญ อย่างไรก็ตามยังไม่ได้รับการโหวต
ShadowRanger

2

หากคุณกำลังมองหาอะไรที่สั้นและเรียบง่ายบางทีมันอาจจะเพียงพอสำหรับคุณ:

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

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

ตัวอย่างการใช้งาน:

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

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

-1

แรงบันดาลใจจากคำตอบของ Matt Gregory ที่นี่เป็นตัววนซ้ำที่ซับซ้อนกว่าเล็กน้อยซึ่งจะส่งกลับ 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)
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.