Python Iterator类索引在for循环运行

时间:2018-01-22 05:38:27

标签: python iterator

我定义了一个包含对象列表集合的类,并定义了__iter____next__方法以使其成为 loopable 。这里的集合是一个Deck类,它包含Card个对象的列表。

代码:

import random

class Card:

    @staticmethod
    def get_ranks():
        return ("A", "K", "Q", "J", "10", "9", "8", "7", "6", "5", "4", "3", "2") # A is highest, 2 is lowest

    @staticmethod
    def get_suites():
        return ("H", "D", "S", "C")

    def __init__(self, suite, rank):
        if suite not in Card.get_suites():
            raise Exception("Invalid suite")
        if rank not in Card.get_ranks():
            raise Exception("Invalid rank")
        self.suite = suite
        self.rank = rank

    def __lt__(self, card2):
        self_rank = Card.get_ranks().index(self.rank)
        card2_rank = Card.get_ranks().index(card2.rank)
        return self_rank > card2_rank

    def __le__(self, card2):
        self_rank = Card.get_ranks().index(self.rank)
        card2_rank = Card.get_ranks().index(card2.rank)
        return self_rank >= card2_rank

    def __gt__(self, card2):
        self_rank = Card.get_ranks().index(self.rank)
        card2_rank = Card.get_ranks().index(card2.rank)
        return self_rank < card2_rank

    def __ge__(self, card2):
        self_rank = Card.get_ranks().index(self.rank)
        card2_rank = Card.get_ranks().index(card2.rank)
        return self_rank <= card2_rank

    def __eq__(self, card2):
        self_rank = Card.get_ranks().index(self.rank)
        card2_rank = Card.get_ranks().index(card2.rank)
        return self_rank == card2_rank

    def __ne__(self, card2):
        self_rank = Card.get_ranks().index(self.rank)
        card2_rank = Card.get_ranks().index(card2.rank)
        return self_rank != card2_rank

    def __str__(self):
        return(self.rank + self.suite)

    def __repr__(self):
        return str(self)

class Deck:

    def __init__(self):
        self.contents = [Card(suite, rank) for suite in Card.get_suites() for rank in Card.get_ranks()]
        random.shuffle(self.contents)
        self.index = 0

    def __len__(self):
        return len(self.contents)

    def __iter__(self):
        return self

    def __next__(self):
        if self.index == len(self.contents):
            raise StopIteration
        item = self.contents[self.index]
        self.index += 1
        return item

    def pick_card(self):
        choice = random.randrange(len(self))
        card = self.contents.pop(choice)
        return card

    def return_card_and_shuffle(self, card):
        self.contents.append(card)
        random.shuffle(self.contents)

    def __str__(self):
        dstr = ''
        for card in self:
            dstr += str(card) + ", "
        return "{} cards: ".format(len(self)) + dstr[:-2]

def deal_bookends(deck):
        card1 = deck.pick_card()
        card2 = deck.pick_card()
        if card1 > card2:
            temp = card1
            card1 = card2
            card2 = temp
        return (card1, card2)

if __name__ == '__main__':
    deck = Deck()
    for _ in range(3):
        c1, c2 = deal_bookends(deck)
        print("We have {} and {}".format(c1, c2))
        print(deck)
        deck.return_card_and_shuffle(c1)
        print(deck)
        print(deck.contents[-4:])
        deck.return_card_and_shuffle(c2)
        print(deck)
        print(deck.contents[-4:])

在运行时,我收到以下错误:

We have 8H and KH
50 cards: 9H, 8C, AC, 7C, 6H, 2S, 2D, 5C, 10H, 5H, JS, 5S, KD, JH, JC, QS, 2H, 3H, 3S, 3D, 4C, 4H, AD, KS, JD, QH, 10D, 6S, 5D, 8D, 3C, 6C, 7D, AS, 7H, AH, 9S, 10C, QC, QD, 7S, 2C, KC, 8S, 4D, 4S, 6D, 10S, 9D, 9C
51 cards: QS
[7D, 5C, 10H, QS]
52 cards: 10C
[KC, 3S, 9H, 10C]
We have 2C and QD
Traceback (most recent call last):
  File "playing_cards.py", line 106, in <module>
    print(deck)
  File "playing_cards.py", line 88, in __str__
    for card in self:
  File "playing_cards.py", line 73, in __next__
    item = self.contents[self.index]
IndexError: list index out of range

当我将卡片对象推回到列表中时,似乎事情不适用于for循环的第二次运行。如何在保持弹出,推送功能的同时解决这个问题。

修改:self.index在第一次调用print()后为50。当卡被添加回列表时,索引保持在50,而甲板长度现在是51卡。因此,在第二次(和第三次)打印调用打印最后一张卡而不是整个卡。然后出现错误。

我想我在这里读过documentation错了。我的问题是我应该在StopIteration位重置索引。这是正确的方法吗,或者索引是否应该自行重置?

4 个答案:

答案 0 :(得分:2)

注意:如果您试图通过实现自己的迭代器来学习迭代器的工作方式,那么上述建议就成立了。如果您只想让Deck迭代,您可以在Deck中执行此操作:

def __iter__(self):
    return self.contents  # lists are already iterable

更好的是,如果你希望你的套牌表现得像一个列表(迭代,索引,切片,删除),你可以扩展list

了解迭代器的工作原理:

你遇到的问题是你正在将一个集合与一个迭代器混为一谈。集合应该包含一组项目。您的Deck是一个集合。一个集合是可迭代的,这意味着我可以对它进行for x in collection。当我们执行for x in collection时,Python实际上会for x in iter(collection),它将集合转换为迭代器。

您希望迭代器和集合是分开的。如果你的集合是它自己的迭代器,那么你一次只能有一个迭代器(它本身)。另请注意,迭代器应该只使用一次。通过在self.index = 0中执行__iter__,您可以使迭代器(Deck)重复使用。

请考虑以下事项:

nums = [1, 2, 3]

for i in nums:
    for j in nums:
        print(i, j)

我们希望这会回来:

1 1
1 2
1 3
2 1
2 2
2 3
3 1
3 2
3 3

请注意,每次内循环遍历整个集合。如果nums是它自己的迭代器,那么我们就会遇到一些问题:

# Here internally nums sets the current index as 0
for i in nums:
   # Here internally nums sets the current index as 0 again
   for j in nums:
       print(i, j)

   # Once this inner loop finishes the current index is 4.
   # But that is also the index for the outer loop, so the
   # outer loop ends too

意外输出:

1 1
1 2
1 3

解决方案是Deck.__iter__应该返回一个名为DeckIterator的新对象,该对象会跟踪自己的indexDeckIterator.__iter__应该返回self(如required by the docs),但这只是一个细节。通过这样做,您可以立即在甲板上启用多次迭代,按预期工作。

所以最小的例子是:

class Deck:
    # ... snip ...

    def __iter__(self):
        return DeckIterator(self.contents)

class DeckIterator:
    def __init__(self, cards):
        self.index = 0
        self.cards = cards

    def __iter__(self):
        return self

    def __next__(self):
        if self.index >= len(self.cards):
            # We've gotten the end of the deck/list
            raise StopIteration

        item = self.cards[self.index]
        self.index += 1
        return item

另外,如果你不相信我这个列表作为它自己的迭代器,这里有一个表现出这种不良行为的列表:

class BadList(list):
    def __iter__(self):
        self._current_index = 0
        return self

    def __next__(self):
        print(f'current index is {self._current_index}', end='')

        if self._current_index >= len(self):
            print(' which is the end, so ending iteration')
            raise StopIteration

        item = self[self._current_index]
        print(f' so returning value {item}')
        self._current_index += 1
        return item

# Using letters instead of numbers so difference between indices
# and items is more clear
letters = BadList('abc')

for i in letters:
    for j in letters:
        print(i, j)

从中输出:

current index is 0 so returning value "a"
current index is 0 so returning value "a"
a a
current index is 1 so returning value "b"
a b
current index is 2 so returning value "c"
a c
current index is 3 which is the end, so ending iteration
current index is 3 which is the end, so ending iteration

答案 1 :(得分:0)

不确定你是怎么到达那里的,但是你已经超出了你的名单。建议你比较列表的>=长度,如:

def __next__(self):
    if self.index >= len(self.contents):
        raise StopIteration
    .....

答案 2 :(得分:0)

进行以下更改

def __iter__(self):
    self.index = 0
    return self

因此,每次调用__iter__时,都会重置index。 在迭代结束时,deck迭代self.index == len(self.contents)后,您收到此错误的原因是{。{1}}。

下次重复时,self.index应重置为0

我做了上述修改,对我有用。

答案 3 :(得分:0)

目前您的具体问题是由于您的__next__方法检查不够通用,无法检测到您已经迭代过self.contents中最后一个值的所有情况。由于self.contents可以更改,因此您需要使用大于或等于的测试:

if self.index >= len(self.contents):

这将解决当前问题,但您仍会遇到其他问题,因为您的Deck只能迭代一次。这是因为您已经实现了迭代器协议,而不是可迭代协议。这些很容易混淆,所以如果你不立即明白这些差异,不要感到难过。

iterable是具有返回迭代器的__iter__方法的任何对象。有些iterables每次调用它们都会返回不同的迭代器,因此您可以多次迭代它们。

迭代器实现__next__方法,该方法产生下一个值或引发StopIteration。迭代器还必须有一个__iter__方法,它返回自身,允许在需要迭代的地方使用迭代器,尽管它只能迭代一次。

对于Deck,实现可迭代协议可能是有意义的,并且每次调用__iter__时都返回一个单独的迭代器。它实现自己的迭代器类型很少有用,但如果你想测试你对不同协议如何组合在一起的知识,那就很有趣了:

class Deck:
    def __init__(self):
        self.contents = [Card(suite, rank)
                         for suite in Card.get_suites()
                         for rank in Card.get_ranks()]
        random.shuffle(self.contents)
        # no index here

    def __iter__(self):
        return DeckIterator(self)

    # other methods, but no __next__

class DeckIterator:
    def __init__(self, deck):
        self.deck = deck
        self.index = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.index > len(self.deck):
            raise StopIteration

        value = self.deck.contents[self.index]
        self.index += 1
        return value

更实用的方法是让Deck.__iter__借用一些方便的迭代器类型。例如,您可以执行return iter(self.contents)并获得一个与上面的自定义版本完全相同的迭代器。另一个选择是使__iter__成为生成器函数,因为生成器对象是迭代器。如果您在迭代它时需要对每个项目进行一些处理,这将非常方便。