浅谈面向对象
概要:面向对象,顾名思义,面向对象模式中的主体被称为对象(object)。每个对象都是类(class)的实例(instance)。
什么是面向对象
其实在我学了辣么久的编程,直至今日仍然对所谓的“面向对象”的了解不够深刻,大概是“不识庐山真面目,只缘身在此山中”。一直没有什么作对比,所以只能不自量力做一个简单的分析:
在了解面向对象之前我们先要了解面向过程编程,这样才能更方便地了解面向对象。面向过程类似于工厂的流水线,每一步的操作都基于上一步的运行结果,如果其中一步错了,那么后面的程序就都错了(一步错、步步错)。
-
优点 : 复杂问题流程化、进而简单化
-
确定 : 扩展性差
那么面向对象就类似于创建了很多对象给你干活,以上帝的视角去定义对象,赋予他们属性及技能。
- 优点 :可扩展性强
- 缺点 : 编程的复杂性要高于面向过程
类与对象
类的概念就像猫科、犬科,即类别的意思,这是面向对象重要的概念,对象是特征与技能的结合体,而类就是一系列对象相似的结合体。(在编程中先有类再有对象)
类的定义语法:
class 类名:
类的属性 = 属性值
def __init__(self,初始化属性值):
self.属性 = 属性值
def 方法(self):
pass
举例:
class Student:
school = "蓝翔" # 这是类的属性
def __init__(self,name): # 类的实例化方法(构造函数或初始化方法)
self.name = name
def study(self): # 类的方法
print(self.name, "在学习!")
那么实例化一个类的对象的方法就是:
stu = Student("张三") # 创建对象的方式,实际走的是类的__init__方法
print(stu.school) # 蓝翔
print(stu.name) # 张三
stu.study() # 张三 在学习!
在python中所有的属性都可以通过.
来调用或者赋值,如果没有这个属性就等于新建了这样一个属性。比如:stu.age = 18
就是给这个对象追加一个age
属性,赋值为18
。
类中内置方法预热:
#python为类内置的特殊属性
类名.__name__# 类的名字(字符串)
类名.__doc__# 类的文档字符串
类名.__base__# 类的第一个父类(在讲继承时会讲)
类名.__bases__# 类所有父类构成的元组(在讲继承时会讲)
类名.__dict__# 类的字典属性
类名.__module__# 类定义所在的模块
类名.__class__# 实例对应的类(仅新式类中)
属性查找
类有两种属性:数据属性 和 函数属性
- 类的数据属性是所有对象共享的(id都是一样的)
- 类的函数属性是绑定给对象的 (对象调用就是对象的绑定方法)
以上面的例子来说,stu.name
首先会从自己的名称空间中找name
,找不到就去类中找,类找不到就找父类,父类也没有就去object类中找,最后找不到就会抛出异常。
对象的绑定方法
类中定义的函数(没有被任何装饰器装饰的)是类的函数属性,类可以使用,但必须遵循函数的参数规则,有几个参数需要传几个参数。
Student.study(stu) # 传入一个对象参数
类中定义的函数(没有被任何装饰器装饰的),其实主要是给对象使用的,而且是绑定到对象的,虽然所有对象指向的都是相同的功能,但是绑定到不同的对象就是不同的绑定方法。
#### 重点:对象的绑定方法特殊之处就在于,对象来调用就会把自身当做第一个参数(self)传给方法
stu.study() # 相当于 Student.study(stu)
了解了对象的一些基本知识后,我们就正式进入面向对象的大门吧!
面向对象的三大特性
面向对象有最重要的三大特性:继承、封装、多态
继承
什么是继承
继承是一种创建新类的方式,新建的类可以继承一个或多个父类(python支持多继承),父类又可称为基类或超类,新建的类称为派生类或子类。
子类会“”遗传”父类的属性(拥有父类所有的属性和方法),从而解决代码重用问题
单继承和多继承
class A: #定义父类
pass
class B: #定义父类
pass
class C(A): #单继承,基类是A,派生类是C
pass
class D(A,B): #python支持多继承,用逗号分隔开多个继承的类
pass
查看继承
>>> C.__bases__ #__base__只查看从左到右继承的第一个子类,__bases__则是查看所有继承的父类
(<class '__main__.A'>,)
>>> D.__bases__
(<class '__main__.A'>, <class '__main__.B'>)
经典类与新式类
1.继承object类的类就是新式类。(python3中所有的类都默认继承object)
2.没有继承object类的类就是经典类。(只存在于python2中)
属性查找
按照类的mro()
返回的查找顺序来查找(返回类的查找顺序列表)。
派生
子类也可以添加自己新的属性或者在自己这里重新定义这些属性(不会影响到父类),需要注意的是,一旦重新定义了自己的属性且与父类重名,那么调用新增的属性时,就以自己为准了。
组合与重用
软件重用的重要方式除了继承之外还有另外一种方式,即:组合
组合指的是,在一个类中以另外一个类的对象作为数据属性,称为类的组合
class Student:
pass
class Taacher:
def __init__(self):
self.stu_list = []
def add_stu(self,stu):
self.stu_list.append(stu)
stu = Student()
teacher1 = Teacher()
teacher1.add_stu(stu)
菱形问题
在python中的子类可以继承多个父类,如F(D, E),如果继承关系为非菱形的,那么就会先从D这个分支开始找(包括D的所有父类),没找到就会 去E这条分支去找,如果这次也没有找到,就会报错。
如果继承关系为菱形结构,那么属性查找的方式有两种,分别是深度优先和广度优先。
在python3中的菱形查找顺序都是广度优先,图示如下:
深度优先只存在于python2中才有,它会在第一个分支上就走到底。
我们还可以直接用__mro__
或mro()
方法查看继承顺序。不过只有新式类才有这个属性来查看先行列表,经典类没有这个属性。
子类调用父类方法
方式一:指名道姓(父类名.父类方法)
class Person:
def __init__(self,name,age):
self.name = name
self.age = age
class Teacher(Person):
def __init__(self,name,age,sex):
Person(self,name,age)
self.sex = sex
方式二:super()
class Person:
def __init__(self,name,age):
self.name = name
self.age = age
class Teacher(Person):
def __init__(self,name,age,sex):
super().__init__(name,age)
self.sex = sex
使用哪一种都可以,但是最好不要混用
了解:
- 继承关系也按照
mro
顺序进行! super(父类名,self).父类方法名
多态以及多态性
-
多态指的是一类事物有多种形态
比如猫科动物的多种形态:老虎、狮子、狸猫
class Felid: def talk(self): pass class Tiger(Felid): def talk(self): print("吼吼吼~") class Lion(Felid): def talk(self): print("嗷嗷嗷~") class Cat(Felid): def talk(self): print("喵喵喵~")
-
多态性是指在不考虑实例类型的情况下使用实例
多态性分为静态多态性和动态多态性
具体如下:
tiger = Tiger() lion = Lion() cat = Cat() # 只要是猫科动物都会有talk()方法,所以我们也不用管具体是什么品种而直接调用 tiger.talk() lion.talk() cat.talk() # 我们可以定义一个统一的接口来使用 def func(obj): obj.talk()
多态性的好处:
-
增加了程序的灵活性:
无论对象怎么变,使用方式都是一样,如 func(obj)
-
增加了程序的可扩展性
使用者无需更改自己的代码,直接用func(obj)去调用
-
-
鸭子类型
python是十分崇尚鸭子类型的。鸭子类型的含义就是:如果看起来像鸭子、走路和叫声都想鸭子,那么它就是鸭子。
如果我们想编写先有对象的自定义版本,可以继承该对象,也可以创建一个外观和行为相像的,但是与它无任何关系的全新对象,后者通常用于保存程序组件的松耦合度。
封装
封装的概念就是把类的一些属性隐藏起来,封装=隐藏。
在python中,使用双下划綫开头的方式可以将属性隐藏起来(私有属性)
#其实这仅仅这是一种变形操作且仅仅只在类定义阶段发生变形
#类中所有双下划线开头的名称如__x都会在类定义时自动变形成:_类名__x的形式:
class A:
__N=0 #类的数据属性就应该是共享的,但是语法上是可以把类的数据属性设置成私有的如__N,会变形为_A__N
def __init__(self):
self.__X=10 #变形为self._A__X
def __foo(self): #变形为_A__foo
print('from A')
def bar(self):
self.__foo() #只有在类内部才可以通过__foo的形式访问到.
#A._A__N是可以访问到的,
#这种,在外部是无法通过__x这个名字访问到。
这里有两点我们需要注意:
- 这种机制实际就是将属性变形,知道了类名和属性名就可以拼出名字:_类名__属性,然后就可以访问了,如a._A__N,即这种操作并不是严格意义上的限制外部访问,主要用来限制外部的直接访问。
- 变形的过程只在类的定义时发生一次,在定义后的赋值操作,不会变形
如果在继承中,父类不想让子类覆盖自己的方法,也可以将方法定义为私有的
#正常情况
class A:
def fa(self):
print('from A')
def test(self):
self.fa()
class B(A):
def fa(self):
print('from B')
b=B()
b.test()
# from B
#把fa定义成私有的,即__fa
class A:
def __fa(self): #在定义时就变形为_A__fa
print('from A')
def test(self):
self.__fa() #只会与自己所在的类为准,即调用_A__fa
class B(A):
def __fa(self):
print('from B')
b=B()
b.test()
# from A
封装的属性对外不对内:封装的属性可以在内部直接使用,而不能在外部直接使用,外部如果要使用就需要我们开辟接口。
特性(property)
property 是一种特殊的属性,访问它时会将函数的返回值包装成属性。
示例 : 计算圆的面积和周长
import math
class Circle:
def __init__(self,radius): #圆的半径radius
self.radius=radius
@property
def area(self):
return math.pi * self.radius**2 #计算面积
@property
def perimeter(self):
return 2*math.pi*self.radius #计算周长
c=Circle(10)
print(c.radius)
print(c.area) #可以向访问数据属性一样去访问area,会触发一个函数的执行,动态计算出一个值
print(c.perimeter) #同上
'''
输出结果:
314.1592653589793
62.83185307179586
'''
为什么要使用property: 对象获取这个值时无法察觉到是调用了一个函数,这种特性的使用方式遵循了统一访问原则。
property的扩展:
class Person:
def __init__(self,val):
self.__NAME=val #将所有的数据属性都隐藏起来
@property
def name(self):
return self.__NAME #obj.name访问的是self.__NAME(这也是真实值的存放位置)
@name.setter
def name(self,value):
if not isinstance(value,str): #在设定值之前进行类型检查
raise TypeError('%s must be str' %value)
self.__NAME=value #通过类型检查后,将值value存放到真实的位置self.__NAME
@name.deleter
def name(self):
raise TypeError('Can not delete')
f=Person('Du')
print(f.name)
# f.name=10 #抛出异常'TypeError: 10 must be str'
del f.name #抛出异常'TypeError: Can not delete'