实验7: 迭代器、生成器、面向对象编程
Lab 7: Iterators, Generators, Object-Oriented Programming

Due by 11:59pm on Tuesday, October 13.

查看英文原文

初始文件

下载 lab07.zip。 在压缩包中,你可以找到本实验问题的初始文件,以及一份 Ok 自动评分器。

主要内容

如果你需要复习本实验的材料,请参考本节。你可以直接跳到问题部分,遇到困难再回到这里。

Iterators

An iterable is any object that can be iterated through, or gone through one element at a time. One construct that we've used to iterate through an iterable is a for loop:

for elem in iterable:
    # do something

for loops work on any object that is iterable. We previously described it as working with any sequence -- all sequences are iterable, but there are other objects that are also iterable! We define an iterable as an object on which calling the built-in function iter function returns an iterator. An iterator is another type of object that allows us to iterate through an iterable by keeping track of which element is next in the sequence.

To illustrate this, consider the following block of code, which does the exact same thing as a the for statement above:

iterator = iter(iterable)
try:
    while True:
        elem = next(iterator)
        # do something
except StopIteration:
    pass

Here's a breakdown of what's happening:

  • First, the built-in iter function is called on the iterable to create a corresponding iterator.
  • To get the next element in the sequence, the built-in next function is called on this iterator.
  • When next is called but there are no elements left in the iterator, a StopIteration error is raised. In the for loop construct, this exception is caught and execution can continue.

Calling iter on an iterable multiple times returns a new iterator each time with distinct states (otherwise, you'd never be able to iterate through a iterable more than once). You can also call iter on the iterator itself, which will just return the same iterator without changing its state. However, note that you cannot call next directly on an iterable.

Let's see the iter and next functions in action with an iterable we're already familiar with -- a list.

>>> lst = [1, 2, 3, 4]
>>> next(lst)             # Calling next on an iterable
TypeError: 'list' object is not an iterator
>>> list_iter = iter(lst) # Creates an iterator for the list
>>> list_iter
<list_iterator object ...>
>>> next(list_iter)       # Calling next on an iterator
1
>>> next(list_iter)       # Calling next on the same iterator
2
>>> next(iter(list_iter)) # Calling iter on an iterator returns itself
3
>>> list_iter2 = iter(lst)
>>> next(list_iter2)      # Second iterator has new state
1
>>> next(list_iter)       # First iterator is unaffected by second iterator
4
>>> next(list_iter)       # No elements left!
StopIteration
>>> lst                   # Original iterable is unaffected
[1, 2, 3, 4]

Since you can call iter on iterators, this tells us that that they are also iterables! Note that while all iterators are iterables, the converse is not true - that is, not all iterables are iterators. You can use iterators wherever you can use iterables, but note that since iterators keep their state, they're only good to iterate through an iterable once:

>>> list_iter = iter([4, 3, 2, 1])
>>> for e in list_iter:
...     print(e)
4
3
2
1
>>> for e in list_iter:
...     print(e)

Analogy: An iterable is like a book (one can flip through the pages) and an iterator for a book would be a bookmark (saves the position and can locate the next page). Calling iter on a book gives you a new bookmark independent of other bookmarks, but calling iter on a bookmark gives you the bookmark itself, without changing its position at all. Calling next on the bookmark moves it to the next page, but does not change the pages in the book. Calling next on the book wouldn't make sense semantically. We can also have multiple bookmarks, all independent of each other.

Iterable Uses

We know that lists are one type of built-in iterable objects. You may have also encountered the range(start, end) function, which creates an iterable of ascending integers from start (inclusive) to end (exclusive).

>>> for x in range(2, 6):
...     print(x)
...
2
3
4
5

Ranges are useful for many things, including performing some operations for a particular number of iterations or iterating through the indices of a list.

There are also some built-in functions that take in iterables and return useful results:

  • map(f, iterable) - Creates iterator over f(x) for each x in iterable
  • filter(f, iterable) - Creates iterator over x for each x in iterable if f(x)
  • zip(iter1, iter2) - Creates iterator over co-indexed pairs (x, y) from both input iterables
  • reversed(iterable) - Creates iterator over all the elements in the input iterable in reverse order
  • list(iterable) - Creates a list containing all the elements in the input iterable
  • tuple(iterable) - Creates a tuple containing all the elements in the input iterable
  • sorted(iterable) - Creates a sorted list containing all the elements in the input iterable

Generators

We can create our own custom iterators by writing a generator function, which returns a special type of iterator called a generator. Generator functions have yield statements within the body of the function instead of return statements. Calling a generator function will return a generator object and will not execute the body of the function.

For example, let's consider the following generator function:

def countdown(n):
    print("Beginning countdown!")
    while n >= 0:
        yield n
        n -= 1
    print("Blastoff!")

Calling countdown(k) will return a generator object that counts down from k to 0. Since generators are iterators, we can call iter on the resulting object, which will simply return the same object. Note that the body is not executed at this point; nothing is printed and no numbers are output.

>>> c = countdown(5)
>>> c
<generator object countdown ...>
>>> c is iter(c)
True

So how is the counting done? Again, since generators are iterators, we call next on them to get the next element! The first time next is called, execution begins at the first line of the function body and continues until the yield statement is reached. The result of evaluating the expression in the yield statement is returned. The following interactive session continues from the one above.

>>> next(c)
Beginning countdown!
5

Unlike functions we've seen before in this course, generator functions can remember their state. On any consecutive calls to next, execution picks up from the line after the yield statement that was previously executed. Like the first call to next, execution will continue until the next yield statement is reached. Note that because of this, Beginning countdown! doesn't get printed again.

>>> next(c)
4
>>> next(c)
3

The next 3 calls to next will continue to yield consecutive descending integers until 0. On the following call, a StopIteration error will be raised because there are no more values to yield (i.e. the end of the function body was reached before hitting a yield statement).

>>> next(c)
2
>>> next(c)
1
>>> next(c)
0
>>> next(c)
Blastoff!
StopIteration

Separate calls to countdown will create distinct generator objects with their own state. Usually, generators shouldn't restart. If you'd like to reset the sequence, create another generator object by calling the generator function again.

>>> c1, c2 = countdown(5), countdown(5)
>>> c1 is c2
False
>>> next(c1)
5
>>> next(c2)
5

Here is a summary of the above:

  • A generator function has a yield statement and returns a generator object.
  • Calling the iter function on a generator object returns the same object without modifying its current state.
  • The body of a generator function is not evaluated until next is called on a resulting generator object. Calling the next function on a generator object computes and returns the next object in its sequence. If the sequence is exhausted, StopIteration is raised.
  • A generator "remembers" its state for the next next call. Therefore,

    • the first next call works like this:

      1. Enter the function and run until the line with yield.
      2. Return the value in the yield statement, but remember the state of the function for future next calls.
    • And subsequent next calls work like this:

      1. Re-enter the function, start at the line after the yield statement that was previously executed, and run until the next yield statement.
      2. Return the value in the yield statement, but remember the state of the function for future next calls.
  • Calling a generator function returns a brand new generator object (like calling iter on an iterable object).
  • A generator should not restart unless it's defined that way. To start over from the first element in a generator, just call the generator function again to create a new generator.

Another useful tool for generators is the yield from statement (introduced in Python 3.3). yield from will yield all values from an iterator or iterable.

>>> def gen_list(lst):
...     yield from lst
...
>>> g = gen_list([1, 2, 3, 4])
>>> next(g)
1
>>> next(g)
2
>>> next(g)
3
>>> next(g)
4
>>> next(g)
StopIteration

Object-Oriented Programming

Object-oriented programming (OOP) is a style of programming that allows you to think of code in terms of "objects." Here's an example of a Car class:

class Car(object):
    num_wheels = 4

    def __init__(self, color):
        self.wheels = Car.num_wheels
        self.color = color

    def drive(self):
        if self.wheels <= Car.num_wheels:
            return self.color + ' car cannot drive!'
        return self.color + ' car goes vroom!'

    def pop_tire(self):
        if self.wheels > 0:
            self.wheels -= 1

Here's some terminology:

  • class: a blueprint for how to build a certain type of object. The Car class (shown above) describes the behavior and data that all Car objects have.
  • instance: a particular occurrence of a class. In Python, we create instances of a class like this:

    >>> my_car = Car('red')

    my_car is an instance of the Car class.

  • attribute or field: a variable that belongs to the class. Think of an attribute as a quality of the object: cars have wheels and color, so we have given our Car class self.wheels and self.color attributes. We can access attributes using dot notation:

    >>> my_car.color
    'red'
    >>> my_car.wheels
    4
  • method: Methods are just like normal functions, except that they are tied to an instance or a class. Think of a method as a "verb" of the class: cars can drive and also pop their tires, so we have given our Car class the methods drive and pop_tire. We call methods using dot notation:

    >>> my_car = Car('red')
    >>> my_car.drive()
    'red car goes vroom!'
  • constructor: As with data abstraction, constructors describe how to build an instance of the class. Most classes have a constructor. In Python, the constructor of the class defined as __init__. For example, here is the Car class's constructor:

    def __init__(self, color):
        self.wheels = Car.num_wheels
        self.color = color

    The constructor takes in one argument, color. As you can see, the constructor also creates the self.wheels and self.color attributes.

  • self: in Python, self is the first parameter for many methods (in this class, we will only use methods whose first parameter is self). When a method is called, self is bound to an instance of the class. For example:

    >>> my_car = Car('red')
    >>> car.drive()

    Notice that the drive method takes in self as an argument, but it looks like we didn't pass one in! This is because the dot notation implicitly passes in car as self for us.


必答题

迭代器与生成器

生成器允许我们表示无限序列,例如下面函数中显示的自然数序列 (1, 2, ...)!

def naturals():
    """A generator function that yields the infinite sequence of natural
    numbers, starting at 1.

    >>> m = naturals()
    >>> type(m)
    <class 'generator'>
    >>> [next(m) for _ in range(10)]
    [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    """
    i = 1
    while True:
        yield i
        i += 1

Q1: 缩放序列

实现生成器函数 scale(it, multiplier),它会产生可迭代对象 it 中的元素, 并将每个元素按 multiplier 进行缩放。 作为额外挑战,尝试使用 yield from 语句来实现这个函数!

def scale(it, multiplier):
    """Yield elements of the iterable it scaled by a number multiplier.

    >>> m = scale([1, 5, 2], 5)
    >>> type(m)
    <class 'generator'>
    >>> list(m)
    [5, 25, 10]

    >>> m = scale(naturals(), 2)
    >>> [next(m) for _ in range(5)]
    [2, 4, 6, 8, 10]
    """
    "*** YOUR CODE HERE ***"

使用 Ok 来测试你的代码:

python3 ok -q scale --local

Q2: 冰雹序列

编写一个生成器,它输出作业1 中的冰雹序列。

以下是冰雹序列的定义:

  1. 选择一个正整数 n 作为起始值。
  2. 如果 n 是偶数,则将其除以 2。
  3. 如果 n 是奇数,则将其乘以 3 并加 1。
  4. 重复此过程,直到 n 变为 1。

注意: 强烈建议(但不强制要求)使用递归来编写解决方案,以获得额外的练习。 由于 hailstone 返回一个生成器,你可以使用 yield from 调用 hailstone

def hailstone(n):
    """
    >>> for num in hailstone(10):
    ...     print(num)
    ...
    10
    5
    16
    8
    4
    2
    1
    """
    "*** YOUR CODE HERE ***"

使用 Ok 来测试你的代码:

python3 ok -q hailstone --local

WWPD: 对象

Q3: 汽车类 (Car Class)

注意: 这些问题涉及继承。要了解继承的概述,请参考课本中的 继承部分

下面是 Car 类的定义,我们将在后续 WWPD(Python 会输出什么?)问题中使用它。 注意: 该定义也可以在 car.py 文件中找到。

class Car(object):
    num_wheels = 4
    gas = 30
    headlights = 2
    size = 'Tiny'

    def __init__(self, make, model):
        self.make = make
        self.model = model
        self.color = 'No color yet. You need to paint me.'
        self.wheels = Car.num_wheels
        self.gas = Car.gas

    def paint(self, color):
        self.color = color
        return self.make + ' ' + self.model + ' is now ' + color

    def drive(self):
        if self.wheels < Car.num_wheels or self.gas <= 0:
            return 'Cannot drive!'
        self.gas -= 10
        return self.make + ' ' + self.model + ' goes vroom!'

    def pop_tire(self):
        if self.wheels > 0:
            self.wheels -= 1

    def fill_gas(self):
        self.gas += 20
        return 'Gas level: ' + str(self.gas)

使用 Ok 测试你对以下 "Python 会输出什么"(WWPD)问题的理解。

python3 ok -q wwpd-car -u --local

如果产生错误,请输入 Error。如果没有输出,请输入 Nothing

>>> deneros_car = Car('Tesla', 'Model S')
>>> deneros_car.model
______
'Model S'
>>> deneros_car.gas = 10 >>> deneros_car.drive()
______
'Tesla Model S goes vroom!'
>>> deneros_car.drive()
______
'Cannot drive!'
>>> deneros_car.fill_gas()
______
'Gas level: 20'
>>> deneros_car.gas
______
20
>>> Car.gas
______
30
>>> deneros_car = Car('Tesla', 'Model S')
>>> deneros_car.wheels = 2
>>> deneros_car.wheels
______
2
>>> Car.num_wheels
______
4
>>> deneros_car.drive()
______
'Cannot drive!'
>>> Car.drive()
______
Error (TypeError)
>>> Car.drive(deneros_car)
______
'Cannot drive!'

对于以下问题,我们参考 MonsterTruck 类。 注意MonsterTruck 类也可以在 car.py 中找到。

 class MonsterTruck(Car):
     size = 'Monster'

     def rev(self):
         print('Vroom! This Monster Truck is huge!')

     def drive(self):
         self.rev()
         return Car.drive(self)
>>> deneros_car = MonsterTruck('Monster', 'Batmobile')
>>> deneros_car.drive()
______
Vroom! This Monster Truck is huge! 'Monster Batmobile goes vroom!'
>>> Car.drive(deneros_car)
______
'Monster Batmobile goes vroom!'
>>> MonsterTruck.drive(deneros_car)
______
Vroom! This Monster Truck is huge! 'Monster Batmobile goes vroom!'
>>> Car.rev(deneros_car)
______
Error (AttributeError)

Magic: The Lambda-ing

在本实验的下一部分,我们将实现一款卡牌游戏!

你可以通过以下命令启动游戏:

python3 cardgame.py

这个游戏目前还不能运行。如果我们现在运行它,代码会报错,因为我们还没有实现任何内容。 当游戏正常运行后,你可以使用 Ctrl-CCtrl-D 退出游戏并返回命令行。

这个游戏使用了多个不同的文件:

  • 本实验所有问题的代码都可以在 classes.py 中找到。
  • 游戏的一些工具代码在 cardgame.py 中,但你不需要打开或阅读这个文件。 这个文件实际上不会直接修改任何实例,而是调用不同类的方法,严格保持抽象屏障。
  • 如果你想在以后修改游戏,添加自定义卡牌和卡组,可以查看 cards.py。 这个文件包含了所有标准卡牌和默认卡组;你可以在这里添加更多卡牌,或者更改你和对手使用的卡组。 这些卡牌在设计时没有考虑平衡性,因此你可以随意调整属性,添加或移除卡牌。

游戏规则 这个游戏相对复杂,但远没有它的同名游戏那么复杂。游戏流程如下:

游戏有两名玩家。每位玩家都有一手牌和一个卡组,每轮开始时,每位玩家从卡组中抽一张牌。 如果玩家在尝试抽牌时卡组已空,则该玩家会自动输掉游戏。 卡牌具有名称、攻击力和防御力。每轮,每位玩家从手牌中选择一张卡牌进行对战。 具有更高战斗力的卡牌赢得该轮对战。 每张已出战卡牌的战斗力计算方式如下:

(player card's attack) - (opponent card's defense) / 2

例如,假设 Player 1 出了一张攻击力 2000 / 防御力 1000 的卡牌,而 Player 2 出了一张攻击力 1500 / 防御力 3000 的卡牌。 他们的卡牌战斗力计算如下:

P1: 2000 - 3000/2 = 2000 - 1500 = 500
P2: 1500 - 1000/2 = 1500 - 500 = 1000

因此,Player 2 赢得这一轮对战。

第一个赢得 8 轮的 Player 将赢得整场比赛!

不过,在可选问题部分,我们还可以添加一些特殊效果,使游戏更加有趣。卡牌分为 Tutor、TA 和 Professor 类型, 每种类型的卡牌在出战时都会触发不同的 效果,并且这些效果会在当轮战斗力计算之前生效:

  • Tutor 使对手弃掉手牌中的前三张卡,并重新抽取三张。
  • TA 交换对手卡牌的攻击力和防御力。
  • Professor 将对手卡牌的攻击力和防御力加到对手卡组的所有卡牌上, 然后移除对手卡组中所有攻击力 防御力与该卡牌相同的卡。

规则较多,如果需要回顾规则,请随时返回此处查看。现在让我们开始制作这款游戏吧!

Q4: 创建卡牌

要玩卡牌游戏,我们首先需要制作卡牌!让我们先实现 Card 类的基本功能。

首先,在 classes.py 中实现 Card 类的构造方法,该方法接受三个参数:

  • 卡牌的 name(名称),字符串类型
  • 卡牌的 attack(攻击力),整数类型
  • 卡牌的 defense(防御力),整数类型

每个 Card 实例都应该使用实例属性 nameattackdefense 来存储这些值。

你还需要实现 Card 类中的 power 方法,该方法接收另一张卡牌作为输入, 并计算当前卡牌的战斗力。如果需要回顾战斗力计算方式,请查看规则部分。

class Card:
    cardtype = 'Staff'

    def __init__(self, name, attack, defense):
        """
        Create a Card object with a name, attack,
        and defense.
        >>> staff_member = Card('staff', 400, 300)
        >>> staff_member.name
        'staff'
        >>> staff_member.attack
        400
        >>> staff_member.defense
        300
        >>> other_staff = Card('other', 300, 500)
        >>> other_staff.attack
        300
        >>> other_staff.defense
        500
        """
        "*** YOUR CODE HERE ***"

    def power(self, other_card):
        """
        Calculate power as:
        (player card's attack) - (opponent card's defense)/2
        where other_card is the opponent's card.
        >>> staff_member = Card('staff', 400, 300)
        >>> other_staff = Card('other', 300, 500)
        >>> staff_member.power(other_staff)
        150.0
        >>> other_staff.power(staff_member)
        150.0
        >>> third_card = Card('third', 200, 400)
        >>> staff_member.power(third_card)
        200.0
        >>> third_card.power(staff_member)
        50.0
        """
        "*** YOUR CODE HERE ***"

使用 Ok 来测试你的代码:

python3 ok -q Card.__init__ --local
python3 ok -q Card.power --local

Q5: 创建 Player

现在我们已经有了卡牌,并且可以组成卡组,但仍然需要 Player 来使用这些卡牌。 现在,我们将完善 Player 类的实现。

每个 Player 实例有三个实例属性:

  • name:Player 的名称。在游戏中,你可以输入自己的名字,名称会被转换为字符串并传递给构造方法。
  • deckDeck 类的一个实例。你可以使用其 .draw() 方法从中抽取卡牌。
  • hand:一个包含 Card 实例的列表。每个 Player 应该在游戏开始时从 deck 中抽取 5 张卡牌作为起始手牌。 在游戏过程中,手牌中的卡牌可以通过索引进行选择。当 Player 抽取新卡牌时,该卡牌会被添加到列表末尾。

完成 Player 类的构造方法,使 self.hand 被初始化为 5 张从 deck 中抽取的卡牌列表。

接下来,在 Player 类中实现 drawplay 方法:

  • draw 方法用于从 deck 中抽取一张卡,并将其加入 Player 的手牌。
  • play 方法用于根据给定的索引从 Player 的手牌中移除并返回一张卡。

在实现 Player.__init__Player.draw 方法时,需要调用 deck.draw()。 不必关心该方法的内部实现,直接使用抽象提供的功能即可!

class Player:
    def __init__(self, deck, name):
        """Initialize a Player object.
        A Player starts the game by drawing 5 cards from their deck. Each turn,
        a Player draws another card from the deck and chooses one to play.
        >>> test_card = Card('test', 100, 100)
        >>> test_deck = Deck([test_card.copy() for _ in range(6)])
        >>> test_player = Player(test_deck, 'tester')
        >>> len(test_deck.cards)
        1
        >>> len(test_player.hand)
        5
        """
        self.deck = deck
        self.name = name
        "*** YOUR CODE HERE ***"

    def draw(self):
        """Draw a card from the player's deck and add it to their hand.
        >>> test_card = Card('test', 100, 100)
        >>> test_deck = Deck([test_card.copy() for _ in range(6)])
        >>> test_player = Player(test_deck, 'tester')
        >>> test_player.draw()
        >>> len(test_deck.cards)
        0
        >>> len(test_player.hand)
        6
        """
        assert not self.deck.is_empty(), 'Deck is empty!'
        "*** YOUR CODE HERE ***"

    def play(self, card_index):
        """Remove and return a card from the player's hand at the given index.
        >>> from cards import *
        >>> test_player = Player(standard_deck, 'tester')
        >>> ta1, ta2 = TACard("ta_1", 300, 400), TACard("ta_2", 500, 600)
        >>> tutor1, tutor2 = TutorCard("t1", 200, 500), TutorCard("t2", 600, 400)
        >>> test_player.hand = [ta1, ta2, tutor1, tutor2]
        >>> test_player.play(0) is ta1
        True
        >>> test_player.play(2) is tutor2
        True
        >>> len(test_player.hand)
        2
        """
        "*** YOUR CODE HERE ***"

使用 Ok 来测试你的代码:

python3 ok -q Player.__init__ --local
python3 ok -q Player.draw --local
python3 ok -q Player.play --local

完成这个问题后,你将获得一个可运行的游戏版本!输入:

python3 cardgame.py

即可开始玩 Magic: The Lambda-ing!

当前版本还没有为不同类型的卡牌实现特殊效果。如果你想让这些效果生效,可以尝试完成下面的可选问题。

选做题

以下的代码编写问题都将在 classes.py 中完成。

对于接下来的部分,不要修改代码中已经提供的任何内容。此外,在实现每个方法后,请确保取消注释所有对 print 的调用。这些调用用于向用户显示信息,修改它们可能会导致你未能通过测试。

Q6: Tutors: Flummox

为了使这个卡牌游戏更加有趣,我们的卡牌应该具有一些效果!我们将通过 effect 函数来实现这些效果,它接受对手的卡牌、当前玩家和对手玩家作为输入。

实现 Tutors 的 effect 方法,使其导致对手丢弃手中的前三张卡牌,并且从对方的牌库中抽取三张新卡。假设对手的手牌中至少有三张卡,并且对方的牌库中至少有三张卡。

记得完成后取消注释中对 print 的调用!

class TutorCard(Card):
    cardtype = 'Tutor'

    def effect(self, other_card, player, opponent):
        """
        Discard the first 3 cards in the opponent's hand and have
        them draw the same number of cards from their deck.
        >>> from cards import *
        >>> player1, player2 = Player(player_deck, 'p1'), Player(opponent_deck, 'p2')
        >>> other_card = Card('other', 500, 500)
        >>> tutor_test = TutorCard('Tutor', 500, 500)
        >>> initial_deck_length = len(player2.deck.cards)
        >>> tutor_test.effect(other_card, player1, player2)
        p2 discarded and re-drew 3 cards!
        >>> len(player2.hand)
        5
        >>> len(player2.deck.cards) == initial_deck_length - 3
        True
        """
        "*** YOUR CODE HERE ***"
        #Uncomment the line below when you've finished implementing this method!
        #print('{} discarded and re-drew 3 cards!'.format(opponent.name))

使用 Ok 来测试你的代码:

python3 ok -q TutorCard.effect --local

Q7: TAs: Shift

现在我们为 TAs 添加一个效果!实现 TAs 的 effect 方法,它会交换对手卡牌的攻击力和防御力。

class TACard(Card):
    cardtype = 'TA'

    def effect(self, other_card, player, opponent):
        """
        Swap the attack and defense of an opponent's card.
        >>> from cards import *
        >>> player1, player2 = Player(player_deck, 'p1'), Player(opponent_deck, 'p2')
        >>> other_card = Card('other', 300, 600)
        >>> ta_test = TACard('TA', 500, 500)
        >>> ta_test.effect(other_card, player1, player2)
        >>> other_card.attack
        600
        >>> other_card.defense
        300
        """
        "*** YOUR CODE HERE ***"

使用 Ok 来测试你的代码:

python3 ok -q TACard.effect --local
html 复制代码

Q8: The Professor Arrives

一个新的挑战者出现了!实现 Professor 的 effect 方法,它会将对手卡牌的攻击力和防御力加到玩家卡组中的所有卡牌上,然后移除对手卡组中所有与对手卡牌的攻击力或防御力相同的卡牌。

注意:当你在遍历列表时修改该列表,可能会遇到问题。试着遍历列表的副本!你可以通过切片来复制列表:

  >>> lst = [1, 2, 3, 4]
  >>> copy = lst[:]
  >>> copy
  [1, 2, 3, 4]
  >>> copy is lst
  False
class ProfessorCard(Card):
    cardtype = 'Professor'

    def effect(self, other_card, player, opponent):
        """
        Adds the attack and defense of the opponent's card to
        all cards in the player's deck, then removes all cards
        in the opponent's deck that share an attack or defense
        stat with the opponent's card.
        >>> test_card = Card('card', 300, 300)
        >>> professor_test = ProfessorCard('Professor', 500, 500)
        >>> opponent_card = test_card.copy()
        >>> test_deck = Deck([test_card.copy() for _ in range(8)])
        >>> player1, player2 = Player(test_deck.copy(), 'p1'), Player(test_deck.copy(), 'p2')
        >>> professor_test.effect(opponent_card, player1, player2)
        3 cards were discarded from p2's deck!
        >>> [(card.attack, card.defense) for card in player1.deck.cards]
        [(600, 600), (600, 600), (600, 600)]
        >>> len(player2.deck.cards)
        0
        """
        orig_opponent_deck_length = len(opponent.deck.cards)
        "*** YOUR CODE HERE ***"
        discarded = orig_opponent_deck_length - len(opponent.deck.cards)
        if discarded:
            #Uncomment the line below when you've finished implementing this method!
            #print('{} cards were discarded from {}\'s deck!'.format(discarded, opponent.name))
            return

使用 Ok 来测试你的代码:

python3 ok -q ProfessorCard.effect --local

完成这个问题后,我们将拥有一个功能完整的 Magic: The Lambda-ing 游戏!不过,这不必是结束 - 我们鼓励你发挥创意,添加更多卡牌类型、效果,甚至将更多自定义卡牌加入你的卡组!