1-Python - 面向对象之类与对象
楔子: 少年,你对盖伦一无所知
截止到目前为止,我们已经可以用Python来解决大部分问题了。现在有一个新的任务。需求是开发一款对战类的游戏。游戏中有很多角色,可以相互攻击,角色有各自的名字、移动速度、攻击力、生命值。
我们用学过的知识来用代码来实现。
def person(name, speed, attack, hp):
return {"name": name, "speed": speed, "attack": attack, "hp": hp}
def animal(name, speed, attack, hp):
return {"name": name, "speed": speed, "attack": attack, "hp": hp}
Garen = person('Garen', 340, 64, 616)
Gnar = animal('Gnar', 340, 66, 558)
上例中两个函数,负责返回不同的角色。
那么,接下来,我们怎么用代码描述两个角色怎么互相攻击呢?比如盖伦用技能"q"攻击了纳尔一次。
def jud(p1, p2):
''' 盖伦的审判技能 '''
p2['hp'] -= p1['attack']
print('%s 中了 %s 的审判技能,受到 %s 点伤害' % (p2['name'], p1['name'], p1['attack']))
def boom(p1, p2):
''' 纳尔的回旋镖技能 '''
p2['hp'] -= p1['attack']
print('%s 中了 %s 的回旋镖技能,受到 %s 点伤害' % (p2['name'], p1['name'], p1['attack']))
jud(Garen, Gnar)
boom(Gnar, Garen)
boom(Garen, Gnar)
'''
Gnar 中了 Garen 的审判技能,受到 64 点伤害
Garen 中了 Gnar 的回旋镖技能,受到 66 点伤害
Gnar 中了 Garen 的回旋镖技能,受到 64 点伤害
'''
上例中,通过定义两个不同角色的不同的技能,完成了攻击操作,但这么写,如果一不小心,就会出错。因为通过第15行的打印结果。显然盖伦不具有纳尔的回旋镖技能。虽然代码层面没有错,但是逻辑设计出现了问题,我们要重新设计逻辑,修改代码。
def person(name, speed, attack, hp):
self_dict = {"name": name, "speed": speed, "attack": attack, "hp": hp}
def jud(animal):
''' 盖伦的审判技能 '''
animal['hp'] -= self_dict['attack']
print('%s 中了 %s 的审判技能,受到 %s 点伤害' % (animal['name'], self_dict['name'], animal['attack']))
self_dict['jud'] = jud
return self_dict
def animal(name, speed, attack, hp):
self_dict = {"name": name, "speed": speed, "attack": attack, "hp": hp}
def boom(person):
''' 纳尔的回旋镖技能 '''
person['hp'] -= self_dict['attack']
print('%s 中了 %s 的回旋镖技能,受到 %s 点伤害' % (person['name'], self_dict['name'], person['attack']))
self_dict['boom'] = boom
return self_dict
Garen = person('Garen', 340, 64, 616)
Gnar = animal('Gnar', 340, 66, 558)
Garen['jud'](Gnar)
Gnar['boom'](Garen)
'''
Gnar 中了 Garen 的审判技能,受到 66 点伤害
Garen 中了 Gnar 的回旋镖技能,受到 64 点伤害
'''
上例中,属于盖伦的技能被放到了person函数中(第4行),并且将技能添加到盖伦自己的字典中(第8行),纳尔同样如此。经过这么一番修改,如果该游戏只有这两个角色,那么程序堪称完美。但很显然不只是有这两个角色,那么如果在有新的角色出现,比如再开发一个新的的角色,不管是人还是动物,上面创建角色的person和animal函数都不适用了,因为新角色不能拥有审判或者回旋镖技能,这种技能只属于某一个角色。
上面这种来一个角色就只针对该角色设计代码,被称为面向过程程序设计,这种编程方式我们称为——面向过程编程(Procedure Oriented Programming,简称POP)。这种思想的核心是过程,即先干什么再干什么,就好比精致的流水线,是一种机械化的思维方式。
面向过程的优点是:复杂的问题通过一系列的流程设计,最终以简单化的手法实现。
缺点也很明显:这条“流水线”只能解决一个问题,比如上面的例子中,创建盖伦的函数无法创建别的角色。即便是能,也要经过很大的改动,可能最后被改的面目全非。反倒不如重新建一条“流水线”。
面向过程编程的应用场景一般是,这条“流水线”一旦完成就基本不怎么变了的应用场景。
虽然面向过程编程有其缺点,但无法否定其强大之处,因为对于一个软件的质量来说,可扩展性只是其中的一个方面。如下图展示了评价软件质量的几个属性。
面向过程是“该怎么做”的思想,这种思想的产物就是流水线作业,先怎么着,再怎么着。后来慢慢的出现了另一种“谁来做”思想,我们就像上帝一样,负责把“谁”生产出来,然后由很多个“谁”共同完成。这种思想在程序上称为面向对象程序设计思想(Object-Oriented Programming,简称OOP),这些个“谁”就是对象。面向对象的程序设计有效地解决了可扩展性。
本章我们来讨论面向对象这种程序设计思想在Python中的实现与应用。
类与对象
目前为止,我们经常提到对象和类这些字眼,那么问题来了,到底是先有鸡(对象)呢?还是先有蛋(类)呢?这要从不同的角度来阐释了。
在现实中,先有对象,再有类。
一个人是一个对象,一株草是一个对象,一只羊是一个对象,对象指具体的事物。随着时代发展,人们将这些事物划分为不同的种类,如人属于人类,无论黄、白、黑,同理,花花草草归属于植物类,羊被归属为动物类。类是一类事物的统称,并不是真实存在的。
而在程序(虚拟)里,先有类,由类产生对象。
程序里,必须先有类,然后由类产生一个个独特的对象,就像先定义函数,然后调用函数,会执行函数内部的代码,返回执行结果。而调用类,则返回的是对象。
那么Python中呢?也是先定义类,由类产生对象。我想你肯定迫不及待要弄明白类和对象了吧。let’s go。
类的创建
初识类
在Python中,用class语句来创建类,class也是Python中的关键字。
import keyword
print(keyword.iskeyword('class')) # True
我们这里根据上面的游戏需求创建一个Person类。
class Person: pass
是不是很简单,我们通过上例就创建了一个类。通过这个Person类,来看一下创类的语法与基本格式。
class Person: # 类名
role = "人" # 类中的代码块
print(Person) # <class '__main__.Person'>
print(Person.role) # 人
Python用class加类名定义一个类,内部的role为类中的语句体。我们稍后在说语句体中都有什么。
一般的,我们以class开头,空格后跟类名,类名比函数名只多了一个要求,就是首字母大写,冒号标明类名定义结束。
第3行打印结果中,__main__.Person
指当前脚本(文件)下的Person类。
类的作用
在之前我们就聊过,类是一类事物的集合,可以产生一个个独特的对象,如上面的Person类,可以产生盖伦、瑞文等属于人类的角色,但不可以产生植物的角色。
实例化
那类该如何“产生”角色(对象)呢?比如我们如何“产生”一个盖伦对象呢?
class Person: pass
garen = Person()
print(garen) # <__main__.Person object at 0x01580FF0>
上例中,当你看到第2行的类名加括号,是不是感觉很熟悉,想到了函数名加括号。是的,函数名加括号触发函数的执行。而这里的类名加括号是在“生产”盖伦这个对象并赋值给变量garen。第3行的打印结果说明了这个盖伦对象是属于Person类的。让我们来记住一些术语。
类:Python将一类事物的统称化为代码实现。
对象:由上面的类“生产”出来的具体事务。
实例化:类名加括号“生产”对象的过程我们称为实例化的过程。
实例:我们称为实例化后的对象为实例,很明显,实例化后的对象和生产它的类有必然的关系,我们后面的介绍中再讨论。我们有时也直接称对象为实例。某个类的某个实例,这样是不是更加清晰。
在Python中,没有什么是“点”语句干不了的:object.attribute。
虽然上面的例子用简单的两三行代码就把盖伦生出来了,但是,实例化的过程并不是你看到的那么简单。此刻让我们想象妈妈子宫中的盖伦,在实例化的过程中,从一个受精卵慢慢的有了人的形状、有了脑袋和脸庞、有了手脚和五脏,最终成为一个完整的、优秀的宝宝,再生出来。而这里的Person类在实例化garen的时候,是不是很突兀的就生出来了?并没有做别的动作。所以,我们接下来要把盖伦变得更加健壮、完美。
class Person:
role = '人'
obj = Person()
obj.name = 'garen'
obj.speed = 340
obj.attack = 64
obj.hp = 616
print(obj.__dict__)
'''
{'name': 'garen', 'hp': 616, 'attack': 64, 'speed': 340}
'''
上面的例子中,我们手动为这个对象取了名字、设置初始的移动速度、攻击力、血量,在Python中,我们称这些为对象的属性。并且,在第8行打印出来,看第10行的结果,是不是跟楔子中我们用函数版的存储方式一样,也是以字典的方式存储。
是的,对象也用字典存储,只是存储在__dict__
属性中。学到这里也可以发现,在Python中,通过什么点什么,就能得到一些东西,比如我们在学习模块时,通过模块名点属性名,也可以得到一个对象,这个对象可以是一个具体的变量名,函数名,包括类名,只不过都称为模块的属性。类中也是这样,通过点可以获取想要的东西。
对象通过自带的__dict__
属性,以字典的形式存贮其属性。类也能这样。
class Person:
role = '人'
obj = Person()
print(Person.__dict__)
'''
{'role': '人', '__weakref__': <attribute '__weakref__' of 'Person' objects>, '__module__': '__main__', '__dict__': <attribute '__dict__' of 'Person' objects>, '__doc__': None}
'''
上例中,通过类名点__dict__
属性,可以看到我们定义的role和自带的其他属性。
之前的例子中,虽然给对象添加了各种属性,但是那种添加属性的办法不够灵活,我们做些针对性的改变。
class Person:
role = '人'
def __init__(self, name, speed, attack, hp):
self.name = name
self.speed = speed
self.attack = attack
self.hp = hp
obj = Person('garen', 340, 64, 616)
print(obj)
print(obj.__dict__)
'''
<__main__.Person object at 0x014FD910>
{'hp': 616, 'attack': 64, 'speed': 340, 'name': 'garen'}
'''
上例中,__init__
方法是固定的写法(第3行),并且在类中,还有很多这种双下划綫格式的方法,它们各自执行不同的功能。这些特殊的方法也具有普通函数的所有功能,如传参。而__init__
方法在这里的作用是,在第8行实例化对象的时候,__init__
函数自动执行,负责具体的实例化的过程,在这个过程中,__init__
函数接收来自第8行的参数。这个对象需要什么,我们就传什么参数进去。让我们稍后再说第一个self参数,从name参数开始,一一对应接收传递过来的参数。并添加到这个对象的属性字典中去。让我们再记住一些术语。
属性:盖伦有攻击力,我们说攻击力就是盖伦的属性,应用到了代码中就是,攻击力就成了obj的attack属性。
object.attribute
:无论是为对象添加还时获取某个属性,都通过这个对象点属性名来完成。这种通过点的方式贯穿我们整个Python的生涯。
方法:一般的,普通的函数,我们就称为函数,而在类中定义的普通函数(是的,__init__
没有你想象的那么神秘复杂),我们称为方法。所以,在以后,如果你看到一个函数被称为方法,那你可能就要明白,这个函数是属于某个类了。
你可能对__init__
方法在接收参数的时候,没有处理self参数,感到迷惑,这里我们通过创建一个动物类在来了解在实例化的过程中的一个细节。
class Animal:
def __init__(self, name):
self.name = name
obj1 = Animal('gnar')
obj2 = Animal('nasus')
print(obj1.name) # gnar
print(obj2.name) # nasus
上例中,在类定义以后,在第4行,通过类名加括号,开始实例化对象obj1,自动执行第2行的__init__
方法,此时此刻这个对象没有名字,name只是这个对象的属性,所以在类中,此时临时称这个对象为self,代表这个对象自己,然后执行__init__
方法中的代码,也就是为这个self对象添加属性,或者执行其他的操作。等__init__
方法执行完毕,对象实例化完毕,在重新赋值给obj1变量,这个对象才有了自己的名字obj1。可以简单的理解为,self也就是obj1。在实例化的过程中,Python自动帮我们传递了这个self参数。
当第5行在实例化对象obj2的时候,此时的self代表的是obj2。切记,这两个对象是各自独立的。
实例化的过程发生在__init__
方法中,所以,我们称__init__
方法为实例化方法。
虽然此时的盖伦对象已经具有了自己的属性。但是现在它还不具备技能,让我们来继续完善。
class Person:
role = '人'
def __init__(self, name, speed, attack, hp):
self.name = name
self.speed = speed
self.attack = attack
self.hp = hp
def passive_skill(self):
''' 对象的被动技能 '''
self.hp += 100
garen = Person('garen', 340, 64, 616)
print(garen.hp) # 616
garen.passive_skill()
print(garen.hp) # 716
上例中,我们在11行实例化一个盖伦角色后,又在第8行定义了一个被动技能passive_skill方法。那么如何调用呢?还是用对象点方法,如第13行,通过盖伦点它的方法(技能),该方法就执行内部代码,血量加100。结果如14行所示,我们在实例化盖伦时,它的血量是616,执行了被动技能后,血量就增加了。第10行的self.hp中的self此时代表盖伦。在类内部都用self来代指具体的对象。注意,self也只是一个变量,但是约定成俗使用self了。通过对象点方法的时候,Python也会自动的帮助我们传递self。
除了上面的对象点方法的调用方式,还有一种方式可以调用方法。一种我们并不推荐的方法。
class Plants:
role = '植物'
def __init__(self, name):
self.name = name
def passive_skill(self):
print(self)
# 法1: obj.attribute
obj = Plants('maokai')
obj.passive_skill()
# 法2: method.attribute(obj)
Plants.passive_skill(obj)
'''
<__main__.Plants object at 0x00FF0FD0>
<__main__.Plants object at 0x00FF0FD0>
'''
上例中,第一种我们推荐的方式。通过对象点方法就可以直接调用属性,并且Python自动帮我们传递self参数,这是我们常用的方式。第二种,通过类名点方法,就需要我们手动传递self参数了。但结果一致(第13~14行)。
那你可能要问,游戏角色不是还有其他的技能呢吗?是的,我们来继续完善。
class Person:
role = '人'
def __init__(self, name, speed, attack, hp):
self.name = name
self.speed = speed
self.attack = attack
self.hp = hp
def passive_skill(self):
''' 对象的被动技能 '''
self.hp += 100
def q(self, enemy):
''' 对象的q技能 '''
enemy.hp -= self.attack
print('%s 中了 %s 的q技能,受到 %s 点伤害' % (enemy.name, self.name, self.attack ))
garen = Person('garen', 340, 64, 616)
riven = Person('riven', 340, 76, 558)
garen.q(riven)
riven.q(garen)
'''
riven 中了 garen 的q技能,受到 64 点伤害
garen 中了 riven 的q技能,受到 76 点伤害
'''
上例中,在第11我们定义了一个q方法(技能)。并且q技能需要一个“敌人”参数,意思是你要用q技能攻击谁。这样,实例化出来的盖伦和瑞文都有了被动和q技能。在第17~18行两个角色使用自己的q技能把“敌人”这个对象传递进去。相互攻击一次。此时,我们要理解,第11行的enemy参数接收的是一个对象,那么此时enemy就是该对象了,具有了其所有的属性。
不知不觉中,我们学会了两个对象的交互,我们再来完善动物的类,以便更清晰的理解。
class Animal:
role = '动物'
def __init__(self, name, speed, attack, hp):
self.name = name
self.speed = speed
self.attack = attack
self.hp = hp
def passive_skill(self):
''' 对象的被动技能 '''
self.attack += 25
def w(self, enemy):
''' 对象的q技能 '''
enemy.hp -= self.attack
print('%s 中了 %s 的w技能,受到 %s 点伤害' % (enemy.name, self.name, self.attack))
gnar = Animal('gnar', 340, 70, 600)
garen.q(gnar) # 盖伦使用q技能攻击纳尔
gnar.w(garen) # 纳尔使用w技能攻击盖伦
gnar.passive_skill() # 纳尔不敌盖伦,使用被动技能
gnar.w(garen) # 纳尔的w技能攻击力得到提高
'''
gnar 中了 garen 的q技能,受到 64 点伤害
garen 中了 gnar 的w技能,受到 70 点伤害
garen 中了 gnar 的w技能,受到 95 点伤害
'''
上例中,动物类的被动技能变为攻击力加25(第8~10行)。并且,动物类有了w技能(方法)。
第16行盖伦首先调用q技能攻击了纳尔,在第17行被纳尔的w技能还击,第18行纳尔调用自己被动技能passive_skill提高了自身的攻击力,19行的w技能伤害增加。效果如第21~23行所示。虽然上述以白话的方式,描述了两个角色的相互攻击,但是,代码层面来说,就是一个对象调用自己的某个方法,作用于另一个角色。另一个角色以同样的方式展开反击。这就是对象的交互。
我们目前为止定义了三个类,你肯定对那个一直存在却从不提起的role变量有疑惑,我们再通过下面例子来探讨一下,role是什么。
class Plants:
role = "植物"
print(Plants.__dict__)
"""
{
'__module__': '__main__',
'role': '植物',
'__dict__': <attribute '__dict__' of 'Plants' objects>,
'__weakref__': <attribute '__weakref__' of 'Plants' objects>,
'__doc__': None
}
"""
通过上例可以看到,role变量存在于Plants类的字典中,那么就可以取出来。
class Plants:
role = "植物"
print(Plants.__dict__['role']) # 植物
那么既然存在于类中,就可以被对象调用。
class Plants:
role = "植物"
obj = Plants()
print(obj.__dict__) # {}
但通过上例中的返回结果来看,role并不存在与对象中,那么到底是干嘛用的呢?对象能调用吗?
class Plants:
role = "植物"
obj = Plants()
print(obj.role) # 植物
是的,能被对象调用,在类中,直接定义的这种变量,是为了供所有的对象使用的,比如这个role变量,在Plants类实例化对象后,每个对象都有一个共同属性,那就是这个对象的角色是植物。让我们再来记住一些术语。
class Animal:
role = '动物' # 类的静态属性
静态属性:在类中,我们称role这类变量为类的静态属性,或者叫做类变量,是大家(所有实例化的对象)公有的。