实验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, aStopIteration
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 callingiter
on a bookmark gives you the bookmark itself, without changing its position at all. Callingnext
on the bookmark moves it to the next page, but does not change the pages in the book. Callingnext
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 overf(x)
for eachx
initerable
filter(f, iterable)
- Creates iterator overx
for eachx
initerable
iff(x)
zip(iter1, iter2)
- Creates iterator over co-indexed pairs (x, y) from both input iterablesreversed(iterable)
- Creates iterator over all the elements in the input iterable in reverse orderlist(iterable)
- Creates a list containing all the elements in the input iterabletuple(iterable)
- Creates a tuple containing all the elements in the input iterablesorted(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 thenext
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:- Enter the function and run until the line with
yield
. - Return the value in the
yield
statement, but remember the state of the function for futurenext
calls.
- Enter the function and run until the line with
-
And subsequent
next
calls work like this:- Re-enter the function, start at the line after the
yield
statement that was previously executed, and run until the nextyield
statement. - Return the value in the
yield
statement, but remember the state of the function for futurenext
calls.
- Re-enter the function, start at the line after the
-
- 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 allCar
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 theCar
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
classself.wheels
andself.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 methodsdrive
andpop_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 theCar
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 theself.wheels
andself.color
attributes. -
self
: in Python,self
is the first parameter for many methods (in this class, we will only use methods whose first parameter isself
). 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 inself
as an argument, but it looks like we didn't pass one in! This is because the dot notation implicitly passes incar
asself
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 中的冰雹序列。
以下是冰雹序列的定义:
- 选择一个正整数
n
作为起始值。 - 如果
n
是偶数,则将其除以 2。 - 如果
n
是奇数,则将其乘以 3 并加 1。 - 重复此过程,直到
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-C
或 Ctrl-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
实例都应该使用实例属性 name
、attack
和 defense
来存储这些值。
你还需要实现 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 的名称。在游戏中,你可以输入自己的名字,名称会被转换为字符串并传递给构造方法。deck
:Deck
类的一个实例。你可以使用其.draw()
方法从中抽取卡牌。hand
:一个包含Card
实例的列表。每个 Player 应该在游戏开始时从deck
中抽取 5 张卡牌作为起始手牌。 在游戏过程中,手牌中的卡牌可以通过索引进行选择。当 Player 抽取新卡牌时,该卡牌会被添加到列表末尾。
完成 Player
类的构造方法,使 self.hand
被初始化为 5 张从 deck
中抽取的卡牌列表。
接下来,在 Player
类中实现 draw
和 play
方法:
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 游戏!不过,这不必是结束 - 我们鼓励你发挥创意,添加更多卡牌类型、效果,甚至将更多自定义卡牌加入你的卡组!