面向对象编程(Object-Oriented Programming )介绍 对于编程语言的初学者来讲, OOP不是一个很容易理解的编程方式,大家虽然都按老师讲的都知道0OP的三大特性是 继承、封装、多态,并且大家也都知道了如何定义类、方法等面向对象的常用语法,但是一到真正写程序的时候, 还是很多人喜欢用函数式编程来写代码,特别是初学者,很容易陷入一个窘境就是 “我知道面向对象,我也会写类,但我依然没发现在使用了面向对象后,对我们的程序开发效率或其它方面带来什么好处, 因为我使用函数编程(指只用函数)就可以减少重复代码并做到程序可扩展了,为啥子还用面向对象?”。 对于此,我个人觉得原因应该还是因为你没有充分了解到面向对象能带来的好处, 今天我就写一篇关于面向对象的入门文章,希望能帮大家更好的理解和使用面向对象编程。
无论用什么形式来编程,我们都要明确记住以下原则: 1,写重复代码是非常不好的低级行为 2.你写的代码需要经常变更 开发正规的程序跟那种写个运行一次就扔了的小脚本一个很大不同就是,你的代码总是需要不断的更改, 不是修改 bug 就是添加新功能等, 所以为了日后方便程序的修改及扩展,你写的代码一定要遵循易读、易改的原则(专业术语叫可读性好、易扩展) 。 如果你把一段同样的代码复制、粘贴到了程序的多个地方以实现在程序的各个地方调用这个功能, 那日后你再对这个功能进行修改时,就需要把程序里多个地方都改一遍, 这种写程序的方式是有问题的,因为如果你不小心漏掉了一个地方没改, 那可能会导致整个程序的运行都出问题。 因此我们知道在开发中一定要努力避免写重复的代码,否则就相当于给自己再挖坑。 还好,函数的出现就能帮我们轻松的解决重复代码的问题, 对于需要重复调用的功能,只需要把它写成一个函数, 然后在程序的各个地方直接调用这个函数名就好了, 并且当需要修改这个功能时,只需改函数代码,然后整个程序就都更新了。 # 函数编程已经可以实现易改,易扩展的功能了
其实 OOP编程 的主要作用也是使你的代码修改和扩展变的更容易,那么小白要问了, 既然函数都能实现这个需求了,还要 OOP 干毛线用呢? 呵呵,说这话就像,古时候,人们打仗杀人都用刀,后来出来了枪,它的主要功能跟刀一样,也是杀人, 然后小白就问,既然刀能杀人了,那还要枪干毛线, 哈哈,显而易见,因为枪能更好更快更容易的杀人。 函数编程与 OOP 的主要区别就是 OOP 可以使程序更加容易扩展和易更改。 小白说,我读书少,你别骗我,口说无凭,证明一下,好吧,那我们就下面的例子证明给小白看。 相信大家都打过 CS 游戏吧,我们就自己开发一个简单版的 CS 来玩一玩。
# you are god, and now you wanna to create a dog class Dog: def bulk(self): # 自动出现了 self,先不用管 self 是什么 print("Chenronghua: wang wang wang!") # 这样一个模子就完成了,只能叫,无法做别的事情 # 现在需要造对象了 d1 = Dog() # 一个狗就出来了 d2 = Dog() # 两个狗就出来了 d3 = Dog() # 三个狗就出来了 # 这样造狗的速度就很快了,不用重新写一遍 # 现在需要调用它的功能,让它叫,让三个狗同时都在叫 d1.bulk() d2.bulk() d3.bulk() ---> Chenronghua: wang wang wang! Chenronghua: wang wang wang! Chenronghua: wang wang wang! # 结果是,都是 Chenronghua在叫,如何区分是不同的狗叫呢?
2 # 可以在造狗的时候起名字 class Dog: def __init__(self,name): self.name = name # 以上def 暂时不需要了解原因,这个方法的目的只是为了传名字,__init__,现在就是用来传名字的 # self.name = name 是什么,暂时不用管 def bulk(self): # 自动出现了 self,先不用管 self 是什么 print("%s: wang wang wang!"% self.name) # 现在可以传名字,造对象了 d1 = Dog("陈荣华") d2 = Dog("陈三炮") d3 = Dog("陈老泡") d1.bulk() d2.bulk() d3.bulk() ---> 陈荣华: wang wang wang! 陈三炮: wang wang wang! 陈老泡: wang wang wang! # 这时候执行,结果是一家三口都在叫 # 这样,就很容易的造三只狗
3 暂不考虑开发场地等复杂的东西,我们先从人物角色下手, 角色很简单,就俩个,恐怖份子、警察,他们除了角色不同,其它基本都一样, 每个人都有生命值、武器等。咱们先用非 OOP 的方式写出游戏的基本角色 #role 1 name = 'Alex role = 'terrorist' weapon = 'AK47' life_value = 100 #role 2 name2= 'Jack' role2 = 'police' weapon2 = 'B22' life_value2 = 100 上面定义了一个恐怖份子Alex和一个警察Jack,但只2个人不好玩呀, 一干就死了,没意思,那我们再分别一个恐怖分子和警察吧, #role 1 name = 'Alex' role = 'terrorist' weapon = 'AK47' life_value = 100 money = 10000 #role 2 name2 = "Jack" role2 = 'police' weapon2 = 'B22' life_value2 = 100 money= 10000 #role 3 name3 = 'Rain' role3 = 'terrorist' weapon3 = 'C33' life_value3 = 100 money3 = 10000 #rolw 4 name4 = 'Erict' ro1e4 = 'police' weapon4 = 'B511' life_value4 = 100 money4 = 10006
4 个角色虽然创建好了,但是有个问题就是,每创建一个角色,我都要单独命名, name1,name2,name3,name4.., 后面的调用的时候这个变量名你还都得记着, 要是再让多加几个角色,估计调用时就很容易弄混啦, 所以我们想一想,能否所有的角色的变量名都是一样的,但调用的时候又能区分开分别是谁? 当然可以,我们只需要把上面的变量改成字典的格式就可以啦。 roles ={ 1:('name':'Alex', 'role:'terrorist', 'weapon':'AK47', 'life_value':100, 'money':15000, }, 2:{'name':'Jack', 'role':'police', 'weapon':'B22', 'life value':100, 'money':15000, }, 3:{'name':'Rain', 'role':'terrorist', 'weapon':'C33', 'life_value':100, 'money':1500, }, 4:{'name':'Eirc', 'role':'police' 'weapon':'B51', 'life_value': 100, 'money':15000, print(roles[1]) # Alex print(roles[2]) # Jack 很好,这个以后调用这些角色时只需要roles[1],roles[2]就可以啦, 角色的基本属性设计完了后,我们接下来为每个角色开发以下几个功能 1,被打中后就会掉血的功能 2.开枪功能 3,换子弹 4,买枪 5.跑、走、跳、下蹲等动作 6·保护人质(仅适用于警察) 7,不能杀同伴 8.。。。 我们可以把每个功能写成一个函数,类似如下 def shot (by_who): #开了枪后要减子弹数 pass def got_shot (who): #中枪后要减血 who["1ife_value'] -= 10 pass def buy_gun(who, gun_name): #检查钱够不够,买了枪后要扣钱 pass ... so far so good,继续按照这个思路设计,再完善一下代码,游戏的简单版就出来了, 但是在往下走之前,我们来看看上面的这种代码写法有没有问题, 至少从上面的代码设计中,我看到以下几点缺陷: 1,每个角色定义的属性名称是一样的,但这种命名规则是我们自己约定的, 从程序上来讲,并没有进行属性合法性检测, 也就是说role 1 定义的代表武器的属性是weapon, role 2 ,3,4 也是一样的, 不过如果我在新增一个角色时不小心把weapon写成了wepon ,这个程序本身是检测不到的 2. terrorist 和 police 这2个角色有些功能是不同的, 比如police是不能杀人质的,但是terrorist可能,随着这个游戏开发的更复杂, 我们会发现这2个角色后续有更多的不同之处, 但现在的这种写法,我们是没办法把这2个角色适用的功能区分开来的, 也就是说,每个角色都可以直接调用任意功能,没有任何限制。 3. 我们在上面定义了got_shot()后要减血,也就是说减血这个动作是应该通过被击中这个事件来引起的,我们调用got_shot(), got_shot ()这个函数再调用每个角色里的 life-value 变量来减血。但其实我不通过 got_shot(), 直接调用角色roles[role_id]['life-value']减血也可以呀, 但是如果这样调用的话,那可以就是简单粗暴啦,因为减血之前其它还应该判断此角色是否穿了防弹衣等, 如果穿了的话,伤害值肯定要减少, got_shot()函数里就做了这样的检测,你这里直接绕过的话,程序就乱了。 因此这里应该设计成除了通过got_shot(),其它的方式是没有办法给角色减血的, 不过在上面的程序设计里,是没有办法实现的。 4. 现在需要给所有角色添加一个可以穿防弹衣的功能,那很显然你得在每个角色里放一个属性来存储此角色是否穿了防弹衣, 那就要更改每个角色的代码,给添加一个新属性,这样太low了,不符合代码可复用的原则! 上面这4点问题如果不解决,以后肯定会引出更大的坑,有同学说了,解决也不复杂呀, 直接在每个功能调用时做一下角色判断啥就好了,没错,你要非得这么霸王硬上弓的搞也肯定是可以实现的, 那你自己就开发相应的代码来对上面提到的问题进行处理好啦。 但这些问题其实能通过 OOP 就可以很简单的解决。 之前的代码改成用 OOP 中的 “类” 来实现的话如下: class Role(object): def __init__(self, name,role, weapon, life_value=100, money=15000): self.name = name self.role = role self.weapon = weapon self.lifevalue = life_value self.money = money def shot(self): print ("shooting...") def got_shot(self): print("ah...I got shot...") def buy_gun(self,gun_name): print ("just bought %s"%gun_name) r1 = Role('Alex', 'police','AK47') #生成一个角色 r2 = Role('Jack', 'terrorist',' B22') #生成一个角色 先不考虑语法细节,相比函数式写法,上面用面向对象中的类来写最直接的改进有以下2点: 1.码量少了近一牛 2.角色和它所具有的功能可以一目了然看出来 # 类是一个角色,下面的每一个函数,相当于一个功能
接下来我们一起分解一下上面的代码分别是什么意思 1-1 class Role(object): def __init__(self, name,role, weapon, life_value=100, money=15000): self.name = name self.role = role self.weapon = weapon self.lifevalue = life_value self.money = money def shot(self): print ("shooting...") def got_shot(self): print("ah...I got shot...") def buy_gun(self,gun_name): print ("%s just bought %s" % (self.name,gun_name)) r1 = Role('Alex', 'police','AK47') #生成一个角色 # 把一个类变成一个具体对象的过程,叫实例化 # 调用类,生成一个角色;叫做实例化(也可以称为 初始化一个类,造了一个对象) # 造完对象后,就是一个具体的东西了,存在 r1 里 r2 = Role('Jack', 'terrorist',' B22') #生成一个角色 r1.buy_gun("AK47") ---> Alex just bought AK47 # 整体理解类的定义,以及把类具体成人的过程 Role('Alex', 'police','AK47').got_shot() Role('Alex', 'police','AK47') # 这两句话,相当于造了两个不一样的人,内存是不一样的地址 # 所以,造完这个人后,以后如果想继续用,需要存入变量
1-1-1 r1.got_shot() r1.buy_gun("b51") # 存入变量后,就可以反复调用 1-1-2 # 实例化一个类,如果想传参数,只能通过 __init__ 方法 # 语法定义,只能如此 class Role(object): def __init__(self, name,role, weapon, life_value=100, money=15000): # __init__ 叫做 构造函数 # __init__ 作用是在实例化时,做一些类的初始化的工作 # 首先需要知道,实例化在内存中到底做了什么 self.name = name self.role = role self.weapon = weapon self.lifevalue = life_value self.money = money def shot(self): print ("shooting...") def got_shot(self): print("ah...I got shot...") def buy_gun(self,gun_name): print ("%s just bought %s" % (self.name,gun_name))
2-1 实例化在内存中到底做了什么 class Role(object): def __init__(self, name,role, weapon, life_value=100, money=15000): self.name = name self.role = role self.weapon = weapon self.lifevalue = life_value self.money = money def shot(self): print ("shooting...") def got_shot(self): print("ah...I got shot...") def buy_gun(self,gun_name): print ("%s just bought %s" % (self.name,gun_name)) print(Role) ---> <class '__main__.Role'> # 说明 虽然看不到他的内存地址,但是它是存在的 # 虽然没有执行,但是本身是已经存在的 # 不管是否有 r1 内存都会生成 # 之前说,如果没有赋值变量名,就没了;因为用完后找不到了 # 认为只要没有变量名指向它,内存就可以销毁了 # 为了不让它被销毁,所以赋值变量名 r1;使他不会被销毁 # Role('Alex', 'police','AK47') 调用,就会立刻在内存中开辟一块空间,将 name ,role, weapon 等传给类 # 类中进行 self.name = name 等操作,数据是如何传进去的? # 类先为 调用的实例 开辟一块新的内存,传入 name = alex, role = Police 等; # 这时内存已经存下来了,然后 Role('Alex', 'police','AK47') 告诉 r1 # 实例化时,直接将 r1 同时传进去 调用函数 ---> 执行 ---> 返回结果 # __init__ 叫做 构造函数 # __init__ 作用是在实例化时,做一些类的初始化的工作 # 首先需要知道,实例化在内存中到底做了什么 def __init__(self, name,role, weapon, life_value=100, money=15000): self.name = name self.role = role self.weapon = weapon self.lifevalue = life_value self.money = money # 是一个初始化的过程,本身也是一个函数,我们会默认为,调用函数 ---> 执行 ---> 返回结果 # 返回的结果,就是那个对象 # 中间的过程,self.name = name 等,就是开辟了内存,往里面存值,最后返回一个内存地址 # 于是变成了 r1 = Role.__init__() return x324342 # r1 调到内存地址,然后往里面存一些值 但其实,事实并不是这样,虽然可以这样实现;但并不是采取 r1 = Role.__init__() return x324342 这种方式 采取的是 Role(r1) 这种方式把 r1 本身当做 参数 传进去 Role(r1,"Alex","Police","15000") 将 r1 变量名传进去,然后往 r1 中存东西 r1.name = "Alex" r1.role = "Police" r1.money = 15000 是这样的实现方式,所以不需要返回值,就没有返回内存地址,因为在外面已经赋好了内存地址 就是 r1 这样赋值就 ok 了 是采取这样的方式 role 自己把 r1 传进去了,所以 前面还得有一个参数;所以 __init__ 都自动带一个 self 就是为了接收 r1 这个变量名 这个 self 就相当于 r1 class Role(object): def __init__(self, name,role, weapon, life_value=100, money=15000): self.name = name self.role = role self.weapon = weapon self.lifevalue = life_value self.money = money # 这个是给每个实例的 # 下面这些是给类中共有的,是在类的内存中存着的 def shot(self): print ("shooting...") def got_shot(self): print("%s:ah...I got shot..."%self.name) # 本来是 r1.name 但 实际上是 self.name,因为是将 r1 传给self def buy_gun(self,gun_name): print ("%s just bought %s" % (self.name,gun_name)) r1 = Role('Alex', 'police','AK47') #生成一个角色 r1.buy_gun() # 其实是到类中调用,r1 中没有buy_gun() # 所以其实是 Role.buy_gun() # 内部就是转成了 Role.buy_gun(r1) r1 传进去 # 所以 buy_gun() 函数必须能够接收 r1,所以类中每写一个方法,就至少必须要有 一个 Self # 这个 self 就是接收 r1,因为要知道是谁 买了抢 # self 就是谁调用这个类就是谁
3-1 class Role(object): def __init__(self, name,role, weapon, life_value=100, money=15000): self.name = name self.role = role self.weapon = weapon self.lifevalue = life_value self.money = money # 这个是给每个实例的 # 下面这些是给类中共有的,是在类的内存中存着的 def shot(self): print ("shooting...") def got_shot(self): print("%s:ah...I got shot..."%self.name) def buy_gun(self,gun_name): print ("%s just bought %s" % (self.name,gun_name)) r2 = Role('Jack', 'terrorist', 'B22') r2.got_shot() # 没有显示的写,但实际上转成了 Role.got_shot(r2) # r2 在 got_shot 中就又变成了 self ---> Jack:ah...I got shot... 所以,整个过程就是在初始化的时候,在初始化构造函数中做的事情就是开辟一块内存,把东西存进去,存到 r1 变量中 __init__ 下面的函数 永远还是在 Role类的内存中,通过 Role 实例化出来的实例如果想调用方法,就得到 Role 中去取,而不是在自己的实例中