轨道
/
Python
Python
/
练习
/
Ellen 的外星人游戏
Ellen 的外星人游戏

Ellen 的外星人游戏

学习练习

简介

Python 中的面向对象编程

如果你一直用函数式、声明式或命令式风格编程,那么转向面向对象编程(OOP)可能会让你觉得有点陌生。 面向对象的方法要求程序员把问题建模为一个或多个objects,这些对象彼此交互、保持状态,并对数据进行操作。 对象可以表示现实世界中的实体(比如汽车或猫),也可以表示更抽象的概念(比如整数、交通工具或哺乳动物)。 每个对象都会成为计算机内存中一个独一无二的实例,代表整个模型的一部分。

类

Classes 是新对象类型的定义,新的对象instances正是从它创建出来的。 它通常把数据和操作这些数据的代码或函数打包在一起。 从这个意义上说,类就是_蓝图_或一组指令,可以据此构建并使用许多类型相似的对象。 一个复杂的程序可以有很多类,每个类都能构建出各式各样的对象。 从一个类构建出一个对象的过程称为 instantiation(也就是创建该类的一个实例)。

在 Python 中定义类很直接:

class MyClass:
    # Class body goes here

类属性

类的字段(也叫 properties、attributes、data members 或 variables)可以添加到类的主体中:

class MyClass:
    number = 5
    string = "Hello!"

MyClass 的实例(对象)可以通过调用这个类来创建,并绑定到一个名字上(就像调用函数一样):

>>> new_object = MyClass()

# Class is instantiated and resulting object is bound to the "new_object" name.
# Note: the object address 'at 0x15adc55b0' will vary by computer.
>>> new_object
<__main__.MyClass at 0x15adc55b0>

Class attributes 在由同一个类创建的所有对象(或实例)之间共享,可以通过dot notation访问,也就是把 . 放在对象名之后、属性名之前:

>>> new_object = MyClass()

# Accessing the class attribute "number" via dot-notation.
>>> new_object.number
5

# Accessing the class attribute "string" via dot-notation.
>>> new_object.string
'Hello!'

# Instantiating an additional object and binding it to the "second_new_object" name.
>>> second_new_object = MyClass()

>>> second_new_object
# Note: the object address "at 0x15ad99970" will vary by computer.
<__main__.MyClass at 0x15ad99970>

# Second_new_object shares the same class attributes as new_object.
>>> new_object.number == second_new_object.number
True

类属性定义在类自身的主体中,位于其他所有方法之前。 它们归类所有,因此可以在类的各个实例之间共享。 由于这些属性是共享的,可以直接从类访问和修改它们的值。 修改类属性的值会改变_从该类实例化出的所有对象_的值:

>>> obj_one = MyClass()
>>> obj_two = MyClass()

# Accessing a class attribute from an object.
>>> obj_two.number
5

# Accessing the class attribute from the class itself.
>>> MyClass.number
5

# Modifying the value of the "number" class attribute.
>>> MyClass.number = 27

# Modifying the "number" class attribute changes the "number" attribute for all objects.
>>> obj_one.number
27

>>> obj_two.number
27

让一堆对象始终保持同步的数据,其实没多大用处。 幸运的是,从类创建出的对象可以用自己的 instance attributes(也就是实例属性、变量或字段)来定制。 顾名思义,实例属性是每个对象独有的,可以独立修改。

用 __init__() 定制对象实例化

特殊的“dunder 方法”(“双下划线方法”的简称)__init__() 用于定制类的实例,也可以用来为对象初始化实例属性。 由于它在初始化实例属性方面的作用,__init__() 也被称为 class constructor 或 initializer。 __init__() 接收一个必需的参数 self,它指代刚刚初始化或创建的对象。 实例属性的数据随后可以作为 __init__() 的实参传入,放在 self 参数之后。

下面,MyClass 现在有了名为 location_x 和 location_y 的实例属性。 可以看到,这两个属性被赋值为传给 __init__() 的 location(一个元组)实参的第 1 个和第 2 个下标。 现在,当你实例化这个类并创建出一个对象时,该类实例的 location_x 和 location_y 属性就会被初始化:

class MyClass:
    # These are class attributes, variables, or fields.
    number = 5
    string = "Hello!"

    # This is the class "constructor", with a "location" parameter that is a tuple.
    def __init__(self, location):

        # This is an instance or object property, attribute, or variable.
        # Note that we are unpacking the tuple argument into two separate instance variables.
        self.location_x = location[0]
        self.location_y = location[1]

# Create a new object "new_object_one", with object property (1, 2).
>>> new_object_one = MyClass((1, 2))

# Create a second new object "new_object_two" with object property (-8, -9).
>>> new_object_two = MyClass((-8, -9))

# Note that new_object_one.location_x and new_object_two.location_x two different values.
>>> new_object_one.location_x
1

>>> new_object_two.location_x
-8

注意,初始化上面的 MyClass 时你只需要传入一个实参,调用这个类时 Python 会自动传入 self。

方法

method 是绑定到类本身(称为类方法,将在后面的练习中讨论)或类的_实例_(对象)上的 function。 作用于对象(实例)的方法在定义时需要把 self 作为第一个形参。 其余的形参,你可以像定义“普通”的、非绑定的函数那样去定义:

class MyClass:
    number = 5
    string = "Hello!"

    # Class constructor.
    def __init__(self, location):
        # Instance properties
        self.location_x = location[0]
        self.location_y = location[1]

    # Instance method. Note "self" as first parameter.
    def change_location(self, amount):
        self.location_x += amount
        self.location_y += amount
        return self.location_x, self.location_y

和访问属性一样,调用方法只需要把 . 放在对象名之后、方法名之前。 被调用的方法不需要把对象的副本作为第一个参数,Python 会自动填入 self:

class MyClass:
    number = 5
    string = "Hello!"

    def __init__(self, location):
        self.location_x = location[0]
        self.location_y = location[1]

    def change_location(self, amount):
        self.location_x += amount
        self.location_y += amount
        return self.location_x, self.location_y

# Make a new test_object with location (3,7)
>>> test_object = MyClass((3,7))
>>> (test_object.location_x, test_object.location_y)
(3,7)

# Call change_location to increase location_x and location_y by 7.
>>> test_object.change_location(7)
(10, 14)

在实例方法内部访问类属性的方式,和从类外部访问时相同:

class MyClass:
    number = 5
    string = "Hello!"

    def __init__(self, location):
        self.location_x = location[0]
        self.location_y = location[1]

    # Alter instance variable location_x and location_y
    def change_location(self, amount):
        self.location_x += amount
        self.location_y += amount
        return self.location_x, self.location_y

    # Alter class variable number for all instances from within an instance.
    def increment_number(self):
        # Increment the 'number' class variable by 1.
        MyClass.number += 1


>>> test_object_one = MyClass((0,0))
>>> test_object_one.number
5

>>> test_object_two = MyClass((13, -3))
>>> test_object_two.increment_number()
>>> test_object_one.number
6

用 pass 占位或打桩

在之前的概念练习和练习题的桩代码里,你应该见过在函数主体中用 pass 关键字代替实际代码。 pass 关键字是一个语法上有效的占位符,它能防止 Python 因为空函数或空类定义而抛出语法错误。 本质上,这是在告诉 Python 翻译:“别担心!我_早晚_会把代码放在这里,只是现在还没写而已。”

class MyClass:
    number = 5
    string = "Hello!"

    def __init__(self, location):
        self.location_x = location[0]
        self.location_y = location[1]

    # Alter instance variable location_x and location_y
    def change_location(self, amount):
        self.location_x += amount
        self.location_y += amount
        return  self.location_x, self.location_y

    # Alter class variable number for all instances
    def increment_number(self):
        # Increment the 'number' class variable by 1.
        MyClass.number += 1

    # This will compile and run without error, but has no current functionality.
    def pending_functionality(self):
       # Stubbing or placholding the body of this method.
       pass

说明

艾伦正在制作一款游戏,玩家要在游戏中与外星人战斗。 她刚刚学习了面向对象编程(OOP),迫不及待地想利用classes为她的程序带来的好处。

令艾伦高兴的是,你主动提出帮忙,于是她把这个任务交给了你:编写玩家要与之战斗的外星人程序。

1. 创建 Alien 类

定义一个 Alien 类,它的构造函数接受两个形参<x_coordinate>和<y_coordinate>,并把它们分别存入x_coordinate和y_coordinate两个实例变量。 每个外星人一开始的生命值都是 3,所以health变量也要初始化。

>>> alien = Alien(2, 0)
>>> alien.x_coordinate
2
>>> alien.y_coordinate
0
>>> alien.health
3

现在,每个外星人都应该能在内部跟踪自己的位置和生命值。

2. hit方法

艾伦希望 Alien 的class拥有一个hit方法,调用时能把外星人对象的生命值减 1。 这样,她只要调用<alien>.hit()就行,不必手动修改外星人的生命值。 hit()是把生命值减到零、还是减到零以下,由你决定。

>>> alien = Alien(0, 0)

# Initialized health value.
>>> alien.health
3

# Decrements health by 1 point.
>>> alien.hit()
>>> alien.health
2

3. is_alive方法

你意识到,如果生命值不断下降,某个时刻它很可能会降到 0(甚至更低!)。 最好加上一个is_alive方法,让艾伦能快速调用,检查外星人是不是……嗯……还活着。😉 <alien>.is_alive()应该返回一个布尔值。

>>> alien.health
1
>>> alien.is_alive()
True
>>> alien.hit()
>>> alien.health
0
>>> alien.is_alive()
False

4. teleport方法

在艾伦的游戏里,外星人拥有瞬移的能力! 你需要写一个teleport方法,接受新的x_coordinate和y_coordinate值,并相应地修改外星人的坐标。

>>> alien.teleport(5, -4)
>>> alien.x_coordinate
5
>>> alien.y_coordinate
-4

5. collision_detection方法

显然,如果外星人会被什么东西击中,它们就得能检测到这种碰撞何时发生。 不过,碰撞检测算法可能很棘手,而你目前还不知道如何实现。 艾伦说她以后再实现,但她还是希望类里先出现collision_detection方法,提醒自己要把这个功能做出来。 它需要接受某种变量(很可能是另一个对象),不过你目前知道的就这些。 你要确保把方法定义放进类里后,调用它时不会出现任何错误:

>>> alien.collision_detection(other_object)
>>>

6. 外星人计数器

艾伦又回来找你,提出了一个新需求。 她想记录游戏运行期间一共创建了多少个外星人。 她说这和计分系统有关。

例如:

>>> alien_one = Alien(5, 1)
>>> alien_one.total_aliens_created
1
>>> alien_two = Alien(3, 0)
>>> alien_two.total_aliens_created
2
>>> alien_one.total_aliens_created
2
# Accessing the variable from the class directly
>>> Alien.total_aliens_created
2

7. 创建外星人数组

艾伦很喜欢你目前做的一切,不过还想请你帮最后一个忙。 她想要一个独立的函数(位于Alien()类之外),给定一个位置数组(用tuples表示),它能创建一个由Alien()对象组成的list。

例如:

>>> alien_start_positions = [(4, 7), (-1, 0)]
>>> aliens = new_aliens_collection(alien_start_positions)
...
>>> for alien in aliens:
...     print(alien.x_coordinate, alien.y_coordinate)
(4, 7)
(-1, 0)
通过 GitHub 编辑 链接将在新窗口或新标签页中打开
Python Exercism

准备好开始 Ellen 的外星人游戏 了吗?

注册 Exercism,借助 17 个概念146 个练习 和真人导师指导,学习并掌握 Python,全部免费。