面向对象编程(OOP,Object Oriented Programming)是每一个高级编程语言都支持的编程方法,比如JAVA/C++/C#等等。学习面向对象编程是每一个程序员都绕不开的重点内容。
一、面向对象的概念
面向对象是在面向过程的设计方法出现很多问题的情况下应运而生的。面向过程设计方法求解问题的基本策略是从功能的角度审视问题域。它将应用程序看成实现某些特定任务的功能模块,其中子过程是实现某项具体操作的底层功能模块。在每个功能模块中,用数据结构描述待处理数据的组织形式,用算法描述具体的操作过程。面对日趋复杂的应用系统,这种设计思路在不同方面逐渐暴露出了缺点:一是系统性较差;二是抽象级别较低;三是封装程度不够;四是重用性较差。在软件理论和硬件提升的过程中,出现了函数式编程(严格来说也算面向过程的一种),它更高级一点。没多久,面向对象编程出现了,实现了跨越式的发展。Java和C#来说只支持面向对象编程,而python比较灵活即支持面向对象编程也支持函数式编程。
- 面向过程:根据业务逻辑从上到下写垒代码
- 函数式:将某功能代码封装到函数中,日后便无需重复编写,仅调用函数即可
- 面向对象:对函数进行分类和封装,让开发“更快更好更强...”
二、类和对象
面向对象编程是一种编程方式,它需要使用 “类” 和 “对象” 来实现,所以,面向对象编程其实就是对 “类” 和 “对象” 的使用。在这里请一定要忘记“面向对象”,“类”和“对象”这三个词的中文意思,不要按他们的字面意思去理解,请用代码组织和利用的形式来理解他们。
类就是一个模板,就像word模板一样,可以在它的基础之上填充内容。模板里可以包含多个函数和变量,函数里实现一些功能,变量里保存一些值。
对象则是根据模板创建的实例,也就是填充了东西之后,对象可以执行类中的函数,可以调用字段。
本质上,类是对事物的抽象,对象(也叫实例)是将抽象的类具体化成一个有血有肉的活体。例如:
定义一个类叫做“人”,他能吃能喝能睡能工作,这些是“人”这个类内部的函数(方法);他有身高有体重有性别,这是“人”这个类的字段。
然后我们可以实例化“人”这个类,比如“张三”、“李四"、”王五”,他们都是“人”这个类的对象,有各自的身高体重性别,但都能吃喝拉撒睡。
那么在程序中是如果创建类和对象的呢?
- class是关键字,表示类
- 创建对象,类名称后加括号即可
# 创建类 class Foo: def Bar(self): print 'Bar' def Hello(self, name): print 'i am %s' %name # 根据类Foo创建对象obj obj = Foo() obj.Bar() #执行Bar方法 obj.Hello('wupeiqi') #执行Hello方法
类和对象在内存中是如何保存的?
类以及类中的方法在内存中只有一份,而根据类创建的每一个对象都在内存中需要存一份,大致如下图:
如上图所示,根据类创建对象时,对象中除了封装 name 和 age 的值之外,还会保存一个类对象指针,该值指向当前对象的类。因此,对象可以寻找到自己的类,并进行某些调用,而类是无法寻找自己的某个对象,然后调用它的。
当通过 obj1 执行方法时,过程如下:
- 根据当前对象中的 类对象指针 找到类中的方法
- 将对象 obj1 当作参数传给 方法的第一个参数 self
三、面向对象三大特性
面向对象的三大特性是指:封装、继承和多态。
一、封装
封装,顾名思义就是将内容封装到某个地方,以后再去调用被封装在某处的内容。
所以,在使用面向对象的封装特性时,需要:
- 将内容封装到某处
- 从某处调用被封装的内容
第一步:将内容封装到某处
self 是一个形式参数,当执行 obj1 = Foo('wupeiqi', 18 ) 时,self 等于 obj1
当执行 obj2 = Foo('alex', 78 ) 时,self 等于 obj2
所以,内容其实被封装到了对象 obj1 和 obj2 中,每个对象中都有 name 和 age 属性,在内存里类似于下图来保存。
第二步:从某处调用被封装的内容
调用被封装的内容时,有两种情况:
- 通过对象直接调用
- 通过self间接调用
1、通过对象直接调用被封装的内容
上图展示了对象 obj1 和 obj2 在内存中保存的方式,根据保存格式可以如此调用被封装的内容:对象.属性名
class Foo: def __init__(self, name, age): self.name = name self.age = age obj1 = Foo('jack', 18) print obj1.name # 直接调用obj1对象的name属性 print obj1.age # 直接调用obj1对象的age属性 obj2 = Foo('andy', 73) print obj2.name # 直接调用obj2对象的name属性 print obj2.age # 直接调用obj2对象的age属性
2、通过self间接调用被封装的内容
执行类中的方法时,需要通过self间接调用被封装的内容
class Foo: def __init__(self, name, age): self.name = name self.age = age def detail(self): print self.name print self.age obj1 = Foo('jack', 18) obj1.detail() # Python默认会将obj1传给self参数,即:obj1.detail(obj1),所以,此时方法内部的 self = obj1,即:self.name 是 jack ;self.age 是 18 obj2 = Foo('andy', 73) obj2.detail() # Python默认会将obj2传给self参数,即:obj1.detail(obj2),所以,此时方法内部的 self = obj2,即:self.name 是 andy ; self.age 是 78
综上所述,上面对于面向对象封装特性的解释是使用构造方法将内容封装到对象中,然后通过对象直接或者self间接获取被封装的内容。然而,本人认为这只是一种低层次的理解。更高层面的理解是:面向对象封装的不仅仅是对象中的普通内容(字段),同时也封装了类本身包含的其他字段、方法和属性,封装也不简单的只是封闭、访问限制的意思,更有一种集成、打包、模块化的概念。
下面的例子中,Foo类中定义了一个私有方法,这是一种封装,它使得外部无法直接调用该方法;Foo还定义了一个静态方法,它可以在不实例化对象的情况下直接由类调用,这也是一种封装。
class Foo: def __init__(self, name): self.name = name def __show(self): print(self.name) @staticmethod def welcome(): print("welcome!") Foo.welcome() obj = Foo("jack") obj.__show()
究其本质,封装作为面向对象三大特性之首,从结构和使用上实现了类和对象两大元素,将事物抽象出来的本质特性封装成为类,又将每一个类的实现封装为一个对象。
二、继承
继承,面向对象中的继承和现实生活中的继承相同,即:子可以继承父的内容。但是,父亲不能继承儿子的内容。
对于面向对象的继承来说,其实就是将多个类共有的方法提取到父类中,子类仅需继承父类而不必一一实现每个方法。
注:除了子类和父类的称谓,还有派生类 和 基类 的说法,他们与子类和父类只是叫法不同而已。
那么问题又来了,多继承呢?
- 是否可以继承多个类
- 如果继承的多个类每个类中都定了相同的函数,那么那一个会被使用呢?
Python的类可以继承多个类,Java和C#中则只能继承一个类。注意,是同时继承多个类,而不是多层类的意思。
python2和python3在多继承策略中有很大不同!
注意:以下是python2中继承的方法!
在Python2版本中的类如果继承了多个类,那么其寻找方法的方式有两种,分别是:深度优先和广度优先
- 当类是经典类时,多继承情况下,会按照深度优先方式查找
- 当类是新式类时,多继承情况下,会按照广度优先方式查找
经典类和新式类,从字面上可以看出一个老一个新,新的必然包含了跟多的功能,也是之后推荐的写法,从写法上区分的话,如果 当前类或者父类继承了object类,那么该类便是新式类,否则便是经典类。
class D: def bar(self): print 'D.bar' class C(D): def bar(self): print 'C.bar' class B(D): def bar(self): print 'B.bar' class A(B, C): def bar(self): print 'A.bar' a = A() # 执行bar方法时 # 首先去A类中查找,如果A类中没有,则继续去B类中找,如果B类中么有,则继续去D类中找,如果D类中么有,则继续去C类中找,如果还是未找到,则报错 # 所以,查找顺序:A --> B --> D --> C # 在上述查找bar方法的过程中,一旦找到,则寻找过程立即中断,便不会再继续找了 a.bar()
经典类多继承
class D(object): def bar(self): print 'D.bar' class C(D): def bar(self): print 'C.bar' class B(D): def bar(self): print 'B.bar' class A(B, C): def bar(self): print 'A.bar' a = A() # 执行bar方法时 # 首先去A类中查找,如果A类中没有,则继续去B类中找,如果B类中么有,则继续去C类中找,如果C类中么有,则继续去D类中找,如果还是未找到,则报错 # 所以,查找顺序:A --> B --> C --> D # 在上述查找bar方法的过程中,一旦找到,则寻找过程立即中断,便不会再继续找了 a.bar()
新式类多继承
经典类:首先去A类中查找,如果A类中没有,则继续去B类中找,如果B类中么有,则继续去D类中找,如果D类中么有,则继续去C类中找,如果还是未找到,则报错
新式类:首先去A类中查找,如果A类中没有,则继续去B类中找,如果B类中么有,则继续去C类中找,如果C类中么有,则继续去D类中找,如果还是未找到,则报错
注意:在上述查找过程中,一旦找到,则寻找过程立即中断,便不会再继续找了
注意:以上是python2.x版本中多继承的问题!!!
下面,让我们看看在python3.x版本中是如何多继承的。需要说明的是,在python3.x之后,没有新式和经典类的概念了。
例子一:
我们设想了一个如下图一的继承关系:
图一
A同时继承B和C,B继承C,C继承D,其他类似不赘述。
class D: # def show(self): # print("i am D") pass class C(D): pass class B(C): def show(self): print("i am B") pass class G: pass class F(G): pass class E(F): def show(self): print("i am E") pass class A(B, E): pass a = A() a.show()
运行结果是i am B。在类A中,没有show这个方法,于是它只能去它的父类里查找,最后它在B类中找到并执行了show方法。可见,在A的定义中,继承参数的书写有先后顺序,写在前面的被优先继承。那如果B没有show方法,而是D有呢?
class D: def show(self): print("i am D") pass class C(D): pass class B(C): pass class G: pass class F(G): pass class E(F): def show(self): print("i am E") pass class A(B, E): pass a = A() a.show()
运行结果是i am D,左边具有深度优先权,当一条路走到黑也没找到的时候,就换一条路。可见,在图一的这种继承结构关系中,搜索顺序是这样的:
例子二:
如果继承结构图是这样的呢?
类D和类G又同时继承了类H。
当只有B和E有show方法的时候,无疑和上面的例子一样,找到B就不找了,直接打印i am B。但如果是只有H和E有show方法呢?如下面的代码:
class H: def show(self): print("i am H") pass class D(H): # def show(self): # print("i am D") pass class C(D): pass class B(C): # def show(self): # print("i am B") pass class G(H): pass class F(G): pass class E(F): def show(self): print("i am E") pass class A(B, E): pass a = A() a.show()
我们想当然地以为会打印i am H。但是,打印的却是i am E!为什么?因为在这种情况下,python的继承搜索路劲是这样的:
它会在搜索左边,直到快到定点H的前一个点D,然后转身搜索另一边的E,再一直往E这条线搜索到最后!很绕是吧?
其它类型的继承结构可以在这个的基础上进行修改和扩展,大家可以自己尝试一下,基本都是这个套路。
三、多态
多态就是多种形态、多种类型意思,表示同一件事物,在不同的情况下可以表现出不同的特性或实现不同的功能或产生不同的效果。