如果你一直用函数式、声明式或命令式风格编程,那么转向面向对象编程(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 关键字是一个语法上有效的占位符,它能防止 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为她的程序带来的好处。
令艾伦高兴的是,你主动提出帮忙,于是她把这个任务交给了你:编写玩家要与之战斗的外星人程序。
定义一个 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
现在,每个外星人都应该能在内部跟踪自己的位置和生命值。
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
is_alive方法你意识到,如果生命值不断下降,某个时刻它很可能会降到 0(甚至更低!)。
最好加上一个is_alive方法,让艾伦能快速调用,检查外星人是不是……嗯……还活着。😉
<alien>.is_alive()应该返回一个布尔值。
>>> alien.health
1
>>> alien.is_alive()
True
>>> alien.hit()
>>> alien.health
0
>>> alien.is_alive()
False
teleport方法在艾伦的游戏里,外星人拥有瞬移的能力!
你需要写一个teleport方法,接受新的x_coordinate和y_coordinate值,并相应地修改外星人的坐标。
>>> alien.teleport(5, -4)
>>> alien.x_coordinate
5
>>> alien.y_coordinate
-4
collision_detection方法显然,如果外星人会被什么东西击中,它们就得能检测到这种碰撞何时发生。
不过,碰撞检测算法可能很棘手,而你目前还不知道如何实现。
艾伦说她以后再实现,但她还是希望类里先出现collision_detection方法,提醒自己要把这个功能做出来。
它需要接受某种变量(很可能是另一个对象),不过你目前知道的就这些。
你要确保把方法定义放进类里后,调用它时不会出现任何错误:
>>> alien.collision_detection(other_object)
>>>
艾伦又回来找你,提出了一个新需求。 她想记录游戏运行期间一共创建了多少个外星人。 她说这和计分系统有关。
例如:
>>> 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
艾伦很喜欢你目前做的一切,不过还想请你帮最后一个忙。
她想要一个独立的函数(位于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)