面向对象程序设计1
人狗大战
我们接到了一个游戏开发任务,需要开发一款叫做“人狗大战”的游戏。
思考:
人狗大战,那至少需要2个角色,一个是人, 一个是狗,且人和狗都有不同的技能,比如人可以打狗, 狗可以咬人,怎么描述这种不同的角色和他们的功能呢?
- 角色
- 狗:名字、种类、攻击力、生命值…
- 人:名字、年龄
- 动作
- 狗咬人
- 人打狗
1 | # 用字典定义角色 |
为了避免重复,可以把字典也写成函数:
1 | def dog(name, d_type, attack_val, hp): |
{'name': '大黄', 'd_type': '金毛', 'attack_val': 70, 'hp': 70}
{'name': '老黑', 'd_type': '藏獒', 'attack_val': 150, 'hp': 150}
可以这样认为:
函数dog就是一个狗的模板
变量d1和d2就是基于这个模板创建的实体
我们还可以把狗的attack_val和与狗的d_type关联起来,给hp设置一个默认值:
1 | attack_vals = { |
{'name': '大黄', 'd_type': '金毛', 'hp': 100, 'attack_val': 70}
{'name': '老黑', 'd_type': '藏獒', 'hp': 100, 'attack_val': 150}
接下来照方抓药,把人的角色也写出来:
1 | def person(name, age): |
{'name': 'Tom', 'age': 25, 'hp': 100, 'attack_val': 50}
{'name': 'Jerry', 'age': 17, 'hp': 100, 'attack_val': 30}
接下来是动作,动作可以让角色直接产生交互:
1 | def dog_bite(dog_obj, person_obj): |
合并一下,完成交互:
1 | attack_vals = { |
狗[大黄]咬了人[Tom]一口,人生命值减少[70],生命值还剩[30]...
练习:
在刚才程序的基础上增加一个人打狗的函数person_beat
接下来考虑一个问题:
如果我们在交互的时候不小心写错了会怎么样?
比如:dog_bite(p1, d1)
1 | def person_beat(person_obj, dog_obj): |
1 | attack_vals = { |
狗[Tom]咬了人[大黄]一口,人生命值减少[50],生命值还剩[50]...
按照正常逻辑,dog_bite只能狗来使用,person_beat只能人使用,否则就会产生逻辑混乱。
怎么解决这个问题呢?
我们可以增加一些判断,也可以把dog_bite函数写到dog函数内部…
总结
其实上面写的代码,就是面向对象的代码。
我们在设计角色时,为了让一个角色可以变成多个实体对象,设计了一个基础模板,只要传入不同参数,就会产生不同的狗。
这代表我们已经开始切换成上帝视角看事情 ,上帝视角就是面向对象编程的视角,上帝要造世界万物,他肯定不是一个一个的造出来,他肯定是设计出一个个的物种的模板,然后通过模子批量批一个个的实体造出来。造出来的实体各有特色,属性、功能都不尽相同,这些人之间会发生什么关系 ,上帝懒的管,上帝只控制大局。
面向过程 VS 面向对象
编程范式
编程是程序员用特定的语法+数据结构+算法组成的代码来告诉计算机如何执行任务的过程 ,一个程序是程序员为了得到一个任务结果而编写的一组指令的集合。
正所谓条条大路通罗马,实现一个任务的方式有很多种不同的方式, 对这些不同的编程方式的特点进行归纳总结出来的编程方式类别,即为编程范式。
不同的编程范式本质上代表对各种类型的任务采取的不同的解决问题的思路, 大多数语言只支持一种编程范式,当然也有些语言可以同时支持多种编程范式。
两种最重要的编程范式分别是面向过程编程和面向对象编程。
面向过程编程(Procedural Programming)
Procedural programming uses a list of instructions to tell the computer what to do step-by-step.
面向过程编程依赖procedures,一个procedure包含一组要被进行计算的步骤, 面向过程又被称为top-down languages, 就是程序从上到下一步步执行,一步步从上到下,从头到尾的解决问题 。基本设计思路就是程序一开始是要着手解决一个大的问题,然后把一个大问题分解成很多个小问题或子过程,这些子过程再执行的过程再继续分解直到小问题足够简单到可以在一个小步骤范围内解决。
举个典型的面向过程的例子, 有个需求是对网站日志进行分析,生成邮件报告,整个流程分以下几步:
到各台服务器上收集日志,因为有多台网站服务器,共同对外提供服务
对日志进行各种维度分析,比如pv,uv, 来源地区、访问的设备等
生成报告,发送邮件
代码如下:
1 | # 1 整合日志 |
这样做的问题也是显而易见的,就是如果你要对程序进行修改,对你修改的那部分有依赖的各个部分你都也要跟着修改, 举个例子,如果程序开头你设置了一个变量值为1 ,但如果其它子过程依赖这个值为1的变量才能正常运行,那如果你改了这个变量,那这个子过程你也要修改,假如又有一个其它子程序依赖这个子过程 ,那就会发生一连串的影响,随着程序越来越大,这种编程方式的维护难度会越来越高。
所以我们一般认为, 如果你只是写一些简单的脚本,去做一些一次性任务,用面向过程的方式很合适,但如果你要处理的任务是复杂的,且需要不断迭代和维护的,那还是用面向对象比较好。
面向对象编程(Object-Oriented Programming)
OOP编程是利用“类”和“对象”来创建各种模型来实现对真实世界的描述,使用面向对象编程的原因一方面是因为它可以使程序的维护和扩展变得更简单,并且可以大大提高程序开发效率 ,另外,基于面向对象的程序可以使它人更加容易理解你的代码逻辑,从而使团队开发变得更从容。
面向对象的几个核心特性:
- Class 类
一个类即是对一类拥有相同属性的对象的抽象、蓝图、原型。在类中定义了这些对象的都具备的属性(variables(data))、共同的方法
前面我们写到“人狗大战”中dog、person其实就是类
- Object 对象
一个对象即是一个类的实例化后实例,一个类必须经过实例化后方可在程序中调用,一个类可以实例化多个对象,每个对象亦可以有不同的属性,就像人类是指所有人,每个人是指具体的对象,人与人之前有共性,亦有不同
前面我们写到“人狗大战”中d1、d2、p1、p2其实就是对象
- Encapsulation 封装
在类中对数据的赋值、内部调用对外部用户是透明的,这使类变成了一个胶囊或容器,里面包含着类的数据和方法
- Inheritance 继承
一个类可以派生出子类,在这个父类里定义的属性、方法自动被子类继承
- Polymorphism 多态
多态是面向对象的重要特性,简单点说:“一个接口,多种实现”,指一个基类中派生出了不同的子类,且每个子类在继承了同样的方法名的同时又对父类的方法做了不同的实现,这就是同一种事物表现出的多种形态。
编程其实就是一个将具体世界进行抽象化的过程,多态就是抽象化的一种体现,把一系列具体事物的共同点抽象出来, 再通过这个抽象的事物, 与不同的具体事物进行对话。
对不同类的对象发出相同的消息将会有不同的行为。比如,你的老板让所有员工在九点钟开始工作, 他只要在九点钟的时候说:“开始工作”即可,而不需要对销售人员说:“开始销售工作”,对技术人员说:“开始技术工作”, 因为“员工”是一个抽象的事物, 只要是员工就可以开始工作,他知道这一点就行了。至于每个员工,当然会各司其职,做各自的工作。
多态允许将子类的对象当作父类的对象使用,某父类型的引用指向其子类型的对象,调用的方法是该子类型的方法。这里引用和调用方法的代码编译前就已经决定了,而引用所指向的对象可以在运行期间动态绑定。
面向对象vs面向过程总结
面向过程的程序设计的核心是过程(流水线式思维),过程即解决问题的步骤,面向过程的设计就好比精心设计好一条流水线,考虑周全什么时候处理什么东西。
优点是:极大的降低了写程序的复杂度,只需要顺着要执行的步骤,堆叠代码即可。
缺点是:一套流水线或者流程就是用来解决一个问题,代码牵一发而动全身。
面向对象的程序设计的核心是对象(上帝式思维),要理解对象为何物,必须把自己当成上帝,上帝眼里世间存在的万物皆为对象,不存在的也可以创造出来。面向对象的程序设计好比如来设计西游记,如来要解决的问题是把经书传给东土大唐,如来想了想解决这个问题需要四个人:唐僧,沙和尚,猪八戒,孙悟空,每个人都有各自的特征和技能(这就是对象的概念,特征和技能分别对应对象的属性和方法),然而这并不好玩,于是如来又安排了一群妖魔鬼怪,为了防止师徒四人在取经路上被搞死,又安排了一群神仙保驾护航,这些都是对象。然后取经开始,师徒四人与妖魔鬼怪神仙互相缠斗着直到最后取得真经。如来根本不会管师徒四人按照什么流程去取。
面向对象的程序设计的
优点:解决了程序的扩展性。对某一个对象单独修改,会立刻反映到整个体系中,如对游戏中一个人物参数的特征和技能修改都很容易。
缺点:可控性差,无法向面向过程的程序设计流水线式的可以很精准的预测问题的处理流程与结果,面向对象的程序一旦开始就由对象之间的交互解决问题,即便是上帝也无法预测最终结果。于是我们经常看到一个游戏人某一参数的修改极有可能导致阴霸的技能出现,一刀砍死3个人,这个游戏就失去平衡。
应用场景:需求经常变化的软件,一般需求的变化都集中在用户层,互联网应用,企业内部软件,游戏等都是面向对象的程序设计大显身手的好地方。
面向对象程序设计
类的定义
1 | help(list) |
Help on class list in module builtins:
class list(object)
| list() -> new empty list
| list(iterable) -> new list initialized from iterable's items
|
| Methods defined here:
|
| __add__(self, value, /)
| Return self+value.
|
| __contains__(self, key, /)
| Return key in self.
|
| __delitem__(self, key, /)
| Delete self[key].
|
| __eq__(self, value, /)
| Return self==value.
|
| __ge__(self, value, /)
| Return self>=value.
|
| __getattribute__(self, name, /)
| Return getattr(self, name).
|
| __getitem__(...)
| x.__getitem__(y) <==> x[y]
|
| __gt__(self, value, /)
| Return self>value.
|
| __iadd__(self, value, /)
| Implement self+=value.
|
| __imul__(self, value, /)
| Implement self*=value.
|
| __init__(self, /, *args, **kwargs)
| Initialize self. See help(type(self)) for accurate signature.
|
| __iter__(self, /)
| Implement iter(self).
|
| __le__(self, value, /)
| Return self<=value.
|
| __len__(self, /)
| Return len(self).
|
| __lt__(self, value, /)
| Return self<value.
|
| __mul__(self, value, /)
| Return self*value.n
|
| __ne__(self, value, /)
| Return self!=value.
|
| __new__(*args, **kwargs) from builtins.type
| Create and return a new object. See help(type) for accurate signature.
|
| __repr__(self, /)
| Return repr(self).
|
| __reversed__(...)
| L.__reversed__() -- return a reverse iterator over the list
|
| __rmul__(self, value, /)
| Return self*value.
|
| __setitem__(self, key, value, /)
| Set self[key] to value.
|
| __sizeof__(...)
| L.__sizeof__() -- size of L in memory, in bytes
|
| append(...)
| L.append(object) -> None -- append object to end
|
| clear(...)
| L.clear() -> None -- remove all items from L
|
| copy(...)
| L.copy() -> list -- a shallow copy of L
|
| count(...)
| L.count(value) -> integer -- return number of occurrences of value
|
| extend(...)
| L.extend(iterable) -> None -- extend list by appending elements from the iterable
|
| index(...)
| L.index(value, [start, [stop]]) -> integer -- return first index of value.
| Raises ValueError if the value is not present.
|
| insert(...)
| L.insert(index, object) -- insert object before index
|
| pop(...)
| L.pop([index]) -> item -- remove and return item at index (default last).
| Raises IndexError if list is empty or index is out of range.
|
| remove(...)
| L.remove(value) -> None -- remove first occurrence of value.
| Raises ValueError if the value is not present.
|
| reverse(...)
| L.reverse() -- reverse *IN PLACE*
|
| sort(...)
| L.sort(key=None, reverse=False) -> None -- stable sort *IN PLACE*
|
| ----------------------------------------------------------------------
| Data and other attributes defined here:
|
| __hash__ = None
1 | class Dog: # 类名首字母要大写,驼峰体 |
hello , I am a dog,my type is 金毛
hello , I am a dog,my type is 金毛
金毛
以上代码就是定义好了Dog这个类,相当于先生成了一个模板,接下来生成了2个实例d1, d2,相当于2条有血有肉的狗被创造出来了。
d_type是类变量,是Dog类下所有实例共有的属性,它存在Dog类本身的内存里。你可以查看d1.d_type,d2.d_type的内存地址,指向的是同一处。
除了共有属性,还有私有属性,比如每条狗的名字、年龄都不一样。
1 | class Dog: # 类名首字母要大写,驼峰体 |
初始化这个实例.... 毛毛
初始化这个实例.... 大黄
hello , I am a dog,my type is 金毛
hello , I am a dog,my type is 金毛
大黄 3 Jack
我们并没有调用__init__(self,….),但它会自动执行,因为它叫初始化函数,就是在实例化的时候,用来初始化一些数据的,比如初始化你实例的名字、年龄等属性。
self就是代表实例本身,你实例化时Python会自动把这个实例本身通过self参数传进去。
self在实例化时自动将对象/实例本身传给init的第一个参数,你也可以给它起个别的名字,但是正常人都不会这么做,因为你瞎改别人就不认识。
属性引用
类的公共属性引用(类名.属性)
1 | class Dog: # 类名首字母要大写,驼峰体 |
京巴
<function Dog.say_hi at 0x10f045488>
实例属性引用(实例名.属性)
1 | class Dog: # 类名首字母要大写,驼峰体 |
hello , I am a dog,my type is 京巴
二蛋 3 Jack
京巴
练习
设计一个类Person,生成若干实例,在终端输出如下信息
小明,10岁,男,喜欢打篮球
小红,13岁,女,喜欢跳芭蕾
老李,40岁,男,喜欢吹牛
老张…
1 |
1 | class Point: |
<class '__main__.Point'>
<__main__.Point object at 0x10f0536d8>
1 |