week5 day1-3 面向对象编程
一. 面向对象编程思想
前情回顾:我们之前所学的书写代码的方法叫做面向过程编程。面向过程编程是一种很常用的书写编程的思想,也非常实用。将复杂的问题拆解出来一步一步解决。
所有的程序都是由“数据”和“功能”组成,因而编写程序的本质就是定义出一系列的数据,然后定义出一系列的功能来对数据进行操作。在学习“对象”之前,程序中的数据与功能是分离开的,如下:
# 数据:name、age、sex
name='lili'
age=18
sex='female'
# 功能:tell_info
def tell_info(name,age,sex):
print('<%s:%s:%s>' %(name,age,sex))
# 此时若想执行查看个人信息的功能,需要同时拿来两样东西,一类是功能tell_info,另外一类则是多个数据name、age、sex,然后才能执行,非常麻烦
tell_info(name,age,sex)
而现在我们将介绍一种新的编程思想——面向对象编程。
面向过程编程的核心是过程,是将问题按步骤拆解完逐步实现的过程。而面向对象编程的核心是对象,而“对象”的精髓在于“整合”。对象就是一个盛放数据和功能的容器。基于该思想写程序就是在创造一个个容器。
编程思想 | 优点 | 缺点 |
---|---|---|
面向过程编程 | 将复杂的问题简单化/流水化 | 可扩展性差 |
面向对象编程 | 可扩展性强 | 把简单的问题复杂化 |
在了解了对象的基本概念之后,理解面向对象的编程方式就相对简单很多了,面向对象编程就是要造出一个个的对象,把原本分散开的相关数据与功能整合到一个个的对象里,这么做既方便使用,也可以提高程序的解耦合程度,进而提升了程序的可扩展性(需要强调的是,软件质量属性包含很多方面,面向对象解决的仅仅只是扩展性问题)
二. 类的定义与实例化
我们个人【对象】既具有数据属性(身高体重年龄),又具有功能属性(吃喝拉撒)。而我们每个人都属于人类这个范围,在人类这个范围中我们共有的数据属性是两条腿,两只胳膊,共有的功能属性是吃喝拉撒。这就是我们的共同点,把我们的共同点找出来都扔在一个大的容器里面,这就构成了【类】。
对象是具体的一个个 个体,而类是把个体都具体有的属性取出来扔到一个大的容器里面。这个大的容器叫做类,而一个个个体就是把属性具体化的类。
即类包含对象共有的数据属性和功能属性,对象是把数据属性具体化的类。产生对象的过程就是类的实例化的过程。
综上所述,虽然我们是先介绍对象后介绍类,但是需要强调的是:在程序中,必须要事先定义类,然后再调用类产生对象(调用类拿到的返回值就是对象)。产生对象的类与对象之间存在关联,这种关联指的是:对象可以访问到类中共有的数据与功能,所以类与对象仍然是属于对象的,类只不过是一种节省空间、减少代码冗余的机制,面向对象编程最终的核心仍然是去使用对象。
# 基础语法
class 类名:
(类独有的数据属性)
def __init__(self,传入的参数1,传入的参数2...):
self.对象独有数据属性1=传入的参数1
self.对象独有数据属性2=传入的参数2
...
# 可以有返回值,但是只能返回None,不能返回非None的值
def 数据的独有功能属性(self,可以传参):
功能属性的函数体代码
@类的装饰器
def 功能属性:
...
类名.__dict__来查看类这个容器内盛放的东西
在定义类的时候发生的事情:
- 立刻运行类体代码
- 将运行过程中产生的名字丢到类的名称空间里面
(类在定义时产生名称空间,函数和模块在调用时产生名称空间)
在调用类时发生的事情:
- 先自动创造一个空对象
- 自动触发类体代码中__init__的运行,将空对象当作第一个参数传入
- 返回一个初始化好的对象 stu1
调用类的过程称为类的实例化,拿到的返回值就是程序中的对象,或称为一个实例。
# 例子
class People:
school='oldboy'
def __init__(self,name,age,gender):
self.name=name
self.age=age
self.gender=gender
def tell_info(self):
print('{}{}{}'.format(self.name,self.age,self.gender))
stu1=People('wth',20,'male')
print(stu1.__dict__)-----># {'name': 'egon', 'age': 18, 'gender': 'male'}
我们会发现:
调用类产生对象就是造了一个具有对象都有属性的字典!
三. 属性查找与绑定方法
在上一个标题中我们讨论了类和对象的关系。类中包含了相关对象中所有的共有属性,既有数据属性,又有功能属性。而对象调用类,调用类会发生三件事,①先自动创建一个空对象,②触发类体代码中的__init__
方法,将空对象当作第一个参数传入,同时将其他参数传入__init__
方法,③返回一个初始化完的对象。而无论是类还是对象,都需要可以方便地获取数据属性和功能属性。接下来我们将具体分析如何通过类和对象拿到数据属性和功能属性。
class Student:
school='oldboy' # 类的属性
def __init__(self,name,age,gender):
self.name=name # 对象独有的属性
self.age=age # 对象独有的属性
self.gender=gender # 对象独有的属性
def tell_info(self):
print('{}{}{}'.format(self.name,self.age,self.gender))
obj1=Student('wth',20,'male') # 背后完成:Student.__init__(obj1,'wth',20,'male')
obj2=Student('wth1',20,'female')
我们之前在学模块的时候学过.
的用法。在导入语句中,.
是路径分隔符,.
前必须是文件夹;在调用语句中,.
前是模块名,.
后是模块中的功能,.
起到了调用功能的作用。同样,当我们创建了对象之后,也需要拿到对象独有的数据属性和功能属性。其获取属性的方法与模块(模块其实就是比类更大的容器)一模一样。下面我们分数据属性和功能属性来详细说明。
3.1 获取数据属性
# 使用方法:
对象.对象/类 数据属性的名字
类.类数据属性的名字
3.1.1 类属性和对象属性
在类中定义的名字,都是类的属性,细说的话,类有两种属性:数据属性和函数属性,可以通过__dict__访问属性的值,比如Student.dict[‘school’],但Python提供了专门的属性访问语法。
通过调用类运行类内部的__init__
方法产生的属性是对象的数据属性。对象除了可以访问自己的数据属性,也可以访问类的属性。
3.1.2 属性的查找顺序与绑定方法
-
对象获取数据属性的查找顺序:对象的名称空间里只存放着对象独有的属性,而对象们相似的属性是存放于类中的。对象在访问属性时,会优先从对象本身的__dict__中查找,未找到,则去类的__dict__中查找
-
类获取数据属性的查找顺序:从类的名称空间中找。
类中的数据属性是共享给对象(id相同)使用的。如果需求是修改所有对象的某项数据,直接修改类中对象数据的值即可;如果只是想修改单个对象的属性,该对象的属性存在则修改,不存在则添加。
# 代码演示:
class Student:
count=0
school='oldboy'
student_list={}
def __init__(self,name,age,gender):
# 每次调用类都会运行类中的__init__函数体内的代码。这样就可以实现调用次数累计的效果
Student.count+=1
self.name=name
self.age=age
self.gender=gender
student_list[Student.count]=self
def tell_info(self):
print('{}-{}-{}'.format(self,name,self.age,self.gender))
stu1=Student('wth',20,'male')
print(Student.count)-----># 1
stu2=Student('wth1',20,'female')
print(Student.count)-----># 2
for k,v in Student.student_list.items():
print(k,v.name)-----># 1 wth
# 2 wth1
3.2 获取功能属性(绑定方法)
# 使用方法:
对象.类的内置方法(给除了self以外的参数传参)
类.类的内置方法(给包括self在内的所有参数传参) # 类对于内部方法的调用与普通函数调用无异
类中的功能属性,类自己可以使用,如果类来调用,该怎么传参就怎么传参,和调用普通函数一样。
但类中的函数是给对象用的,对象来调用就是一个绑定方法,绑定方法的特点就是会将调用内置方法的对象(字典)当作第一个参数传入。
# 代码演示:
class Student:
count=0
school='oldboy'
student_list={}
def __init__(self,name,age,gender):
Student.cout+=1
self.name=name
self.age=age
self.gender=gender
student_list[Student.count]=self
def tell_info(self):
print('{}-{}-{}'.format(self,name,self.age,self.gender))
stu1=Student('wth',20,'male')
stu2=Student('wth1',20,'female')
# 类来调用
Student.tell_info(stu1)
# 对象调用
stu1.tell_info() # 参数自动传入调用它的对象
Python中一切皆为对象,且Python3中类与类型是一个概念,因而绑定方法我们早就接触过
#类型list就是类
>>> list
<class 'list'>
#实例化的到3个对象l1,l2,l3
>>> l1=list([1,2,3])
>>> l2=list(['a','b','c'])
>>> l3=list(['x','y'])
#三个对象都有绑定方法append,是相同的功能,但内存地址不同
>>> l1.append
<built-in method append of list object at 0x10b482b48>
>>> l2.append
<built-in method append of list object at 0x10b482b88>
>>> l3.append
<built-in method append of list object at 0x10b482bc8>
#操作绑定方法l1.append(4),就是在往l1添加4,绝对不会将4添加到l2或l3
>>> l1.append(4) #等同于list.append(l1,4)
>>> l1
[1,2,3,4]
>>> l2
['a','b','c']
>>> l3
['x','y']
3.3 小结
在上述介绍类与对象的使用过程中,我们更多的是站在底层原理的角度去介绍类与对象之间的关联关系,如果只是站在使用的角度,我们无需考虑语法“对象.属性"中”属性“到底源自于哪里,只需要知道是通过对象获取到的就可以了,所以说,对象是一个高度整合的产物,有了对象,我们只需要使用”对象.xxx“的语法就可以得到跟这个对象相关的所有数据与功能,十分方便且解耦合程度极高。
四. 类的三大特性之一 —— 封装
关于类的封装的特性其实我们一直在创造类的过程中一直在使用,我们把对象中相似或相同的属性都提取出来放到一个大容器里面,而这个大容器被我们封装成了类。而我们把具象化的属性封装在一个对象里面。我们原来想传递数据需要把所有的数据都传递给别人,需要传多个值:现在我们只需要将封装了这些具体数据的对象扔给别人,需要传一个值,而其他属性都可以通过这个值拿到。除此之外,针对封装到对象或者类中的属性,我们还可以严格控制对他们的访问,分两步实现:隐藏与开放接口。
4.1 隐藏属性
是不是我们可以获得对象里面所有的值呢?是的。但其实有些数据我们不想别人拿到或者我们不想别人随意修改那些数据,我们就需要一种方法把数据藏起来。这也就是python提供给我们的隐藏属性的方法。我们可以把属性隐藏起来,不让外部直接访问修改,我们单独这些数据的删改查开辟接口来进行进一步的操作。
我们只需要在想隐藏起来的属性前面加上__
即将该属性隐藏起来了。那么这个隐藏过程是如何发生的?以及我们可以如何使用隐藏属性这个方法呢?
# 代码演示:
class Student:
def __init__(self,name):
self.__name=name
# 开辟查询接口
def get_name(self):
print(self.__name)
stu1=Student('wth')
print(stu1.__name)-----># 直接报错,说没有这个属性
# 我们创造的学生名字这个数据属性究竟去了哪里?查询一下这个对象的字典
print(stu1.__dict__)----->{...,'_Student__name':'wth',...} # python完成了改名操作
通过上面的代码我们可以得知隐藏属性这个方法并没有真的把我们的数据隐藏起来,而是给它改了名,让我们没法直接访问到。我们没法直接访问,但是可以通过其他接口拿到名字。总结一下隐藏属性这个方法的特点:
-
并没有真的藏起来,只是语法意义上的变形
>>> Foo.__dict__ mappingproxy({..., '_Foo__N': 0, ...}) >>> obj.__dict__ {'_Foo__x': 10} >>> Foo._Foo__N 0 >>> obj._Foo__x 10 >>> obj._Foo__N 0
-
该变形只在类定义阶段,扫描语法的时候执行,此后
__
开头的属性都不会变形>>> Foo.__M=100 >>> Foo.__dict__ mappingproxy({..., '__M': 100,...}) >>> Foo.__M 100 >>> obj.__y=20 >>> obj.__dict__ {'__y': 20, '_Foo__x': 10} >>> obj.__y 20
-
该隐藏对内不对外,还是可以通过开放给外部的接口拿到被隐藏的属性
stu1.get_name()----->'wth'
那么为什么要隐藏属性呢?
- 隐藏数据属性是为了严格控制外部访问者对数据的修改和删除
- 隐藏功能属性是为了隔离复杂度
eg.开电脑的时候我们只需要点亮开机键等待电脑开机,其他的计算机会帮我们处理(其实隐藏属性也是为了提高封装程度)
下面是回忆过去时间:
我们之前学习数据类型的时候,定义一个列表,字典的时候list1=list('string')
,这个状态是不是很像我们把类实例化的操作,list就是类的名字,把一个参数传进去,创建一个空对象,将空对象和参数传入内置的__init__函数,返回一个初始化后的对象,赋值给list1,然后我们可以通过对象加.
调用类的内置方法处理数据。其实,python中一切皆对象
,只不过我们可以拿来就用的是内置好的类,而我们想要完成某种特定的需求所建立的类是自定义的类。
4.2 开放接口
定义属性就是为了使用,所以隐藏并不是目的。
4.2.1 隐藏数据属性
将数据隐藏起来就限制了类外部对数据的直接操作,然后类内部应该提供相应的接口来允许类外部间接地操作数据,接口之上可以附加额外的逻辑对数据进行严格的控制。
>>> class Teacher:
... def __init__(self,name,age): #将名字和年纪都隐藏起来
... self.__name=name
... self.__age=age
... def tell_info(self): #对外提供访问老师信息的接口
... print('姓名:%s,年龄:%s' %(self.__name,self.__age))
... def set_info(self,name,age): #对外提供设置老师信息的接口,并附加类型检查的逻辑
... if not isinstance(name,str):
... raise TypeError('姓名必须是字符串类型')
... if not isinstance(age,int):
... raise TypeError('年龄必须是整型')
... self.__name=name
... self.__age=age
...
>>>
>>> t=Teacher('lili',18)
>>> t.set_info(‘LiLi','19') # 年龄不为整型,抛出异常
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 11, in set_info
TypeError: 年龄必须是整型
>>> t.set_info('LiLi',19) # 名字为字符串类型,年龄为整形,可以正常设置
>>> t.tell_info() # 查看老师的信息
姓名:LiLi,年龄:19
4.2.2 隐藏函数属性
目的是提高隔离度,例如ATM程序的取款功能,该功能有很多其他功能,比如插卡、身份认证、输入金额、打印小票、取钱等,而对于使用者来说,只需要开发取款这个功能即可,其余功能我们都可以隐藏起来。
class ATM:
def __card(self): # 插卡
print('插卡')
def __auth(self): # 用户认证
print('用户认证')
def __input(self): # 输入金额
print('输入存款金额')
def __print_bill(self): # 打印小票
print('打印账单')
def __take_money(self): # 取钱
print('取款')
def withdraw(self): # 对外提供的功能
self.__card()
self.__auth()
self.__input()
self.__print_bill()
self.__take_money()
obj=ATM()
obj.withdraw()
总结隐藏属性与开放接口,本质就是为了明确地区分内外,类内部可以修改封装内的东西而不影响外部调用者的代码;而类外部接口只需拿到一个接口。只要接口名、参数不变,则无论设计者如何改变内部实现代码,使用者均无需改变代码。这就是一个良好的合作基础,只要接口这个基础约定不变,则代码的修改不足为虑。
4.3 类的装饰器property
BMI指数是用来衡量一个人的体重与身高对健康影响的一个指标,计算公式为
体质指数(BMI)=体重(kg)÷身高^2(m)
EX:70kg÷(1.75×1.75)=22.86
身高或体重是不断变化的,因而每次想查看BMI值都需要通过计算才能得到,但很明显BMI听起来更像是一个特征而非功能,为此Python专门提供了一个装饰器property,可以将类中的函数“伪装成”对象的数据属性,对象在访问该特殊属性时会触发功能的执行,然后将返回值作为本次访问的结果,例如
# 代码演示:
class People:
def __init__(self,name,height,weight):
self.name=name
self.height=height
self.weight=weight
# 写一个测BMI的功能
@property
def BMI(self):
print('{}的BMI是{}'.format(self.name,self.weight/(self.height**2)))
peo1=People('wth',178,75)
# 不加装饰器的调用方法
peo1.BMI()
# 添加了装饰器property之后的调用方式
peo1.BMI # 实现了将功能属性像数据属性一样调用的效果
property
是提供给不需要传参的功能属性添加装饰器的方式,直接将功能属性的使用方法转换成了数据属性的使用方法。使用property
有效地保证了属性访问的一致性。另外property
还提供了设置和删除属性的功能,如下:
# 代码演示:添加装饰器方式一
>>> class Foo:
... def __init__(self,val):
... self.__NAME=val #将属性隐藏起来
... @property
... def name(self):
... return 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 PermissionError('Can not delete')
...
>>> f=Foo('lili')
>>> f.name
lili
>>> f.name='LiLi' #触发name.setter装饰器对应的函数name(f,’LiLi')
>>> f.name=123 #触发name.setter对应的的函数name(f,123),抛出异常TypeError
>>> del f.name #触发name.deleter对应的函数name(f),抛出异常PermissionError
# 代码演示:添加装饰器方法二
class People:
def __init__(self,name):
self.__name=name
def get_name(self):
return self.__name
def set_name(self,name):
if type(name) is not str:
raise Exception('名字必须是字符串类型')
self.__name=name
def del_name(self):
raise Exception('不允许删除!')
name=property(get_name,set_name,del_name)
obj=People('wth')
print(obj.name)
obj.name='WTH'
del obj.name
4.4 绑定方法与非绑定方法
类中定义的函数分为两大类:绑定方法和非绑定方法。
其中绑定方法又可以分为绑定到对象的绑定方法和绑定给类的类方法。
绑定方法我们在介绍类和对象关系的时候提到过,类的功能属性只存在于类的名称空间中,对象想要调用类中的功能属性时就是把一个链接放在对象的名称空间中,这个链接可以直接指向类中功能的内存地址,类中的功能是相当于绑定方法拿给对象用的。绑定方法给谁就是由谁来用,谁来调用就把自己当作第一个参数传入。
那么什么是非绑定方法呢?不与任何对象绑定,意味着谁都可以来调用,但无论谁来调用,都是一个普通函数,没有自动传参的效果。
# 类绑定方法的演示
# 配置文件settings.py的内容
HOST='127.0.0.1'
PORT=3306
# 类方法的应用
import settings
class MySQL:
def __init__(self,host,port):
self.host=host
self.port=port
@classmethod
def from_conf(cls): # 从配置文件中读取配置进行初始化
return cls(settings.HOST,settings.PORT)
>>> MySQL.from_conf # 绑定到类的方法
<bound method MySQL.from_conf of <class ‘__main__.MySQL'>>
>>> conn=MySQL.from_conf() # 调用类方法,自动将类MySQL当作第一个参数传给cls
# 非绑定方法的演示
import uuid
class MySQL:
def __init__(self,host,port):
self.id=self.create_id()
self.host=host
self.port=port
@staticmethod
def create_id():
return uuid.uuid1()
>>> conn=MySQL(‘127.0.0.1',3306)
>>> print(conn.id) #100365f6-8ae0-11e7-a51e-0088653ea1ec
# 类或对象来调用create_id发现都是普通函数,而非绑定到谁的方法
>>> MySQL.create_id
<function MySQL.create_id at 0x1025c16a8>
>>> conn.create_id
<function MySQL.create_id at 0x1025c16a8>
# 代码演示:
class People:
def __init__(self,name):
self.name=name
# 但凡在类中定义一个函数,默认就是绑定给对象,应该由对象来调用,对象来调用会把自己当成第一个参数传入
def tell(self):
print(self.name)
# 类中定义的函数被@classmethod装饰过,就绑定给类,应该由类来调用,类来调用就会把自己当成第一个参数自动传入
@classmethod
def f1(cls):
print(cls)
# 类中定义的函数被@staticmethod装饰过,谁也不绑定,谁都可以来调,按照普通函数传参
@staticmethod
def func():
print('this is from func')
peo1=People('wth')
peo1.tell()
People.f1()
peo1.func()
People.func()
五. 类的三大特性之二 —— 继承
5.1 继承基本概念
既然对象之间相似属性可以提取出来放在类里面,那么我们可不可以把类和类的相似数据属性和功能属性拿出来,放在父类里面呢?
# 代码演示:提取前
class Student:
def __init__(self,name,age,gender):
self.name=name
self.age=age
self.gender=gender
def tell_info(self):
print('{}{}{}'.format(self.name,age,gender))
def choose_course(self):
print('{}正在选课'.format(self.name))
class Teacher:
def __init__(self,name,age,gender):
self.name=name
self.age=age
self.gender=gender
def tell_info(self):
print('{}{}{}'.format(self.name,age,gender))
def grade(self):
print('{}正在评分'.format(self.name))
# 代码演示:提取后
class People:
def __init__(self,name,age,gender):
self.name=name
self.age=age
self.gender=gender
def tell_info(self):
print('{}{}{}'.format(self.name,age,gender))
class Student(People):
def choose_course(self):
print('{}正在选课'.format(self.name))
class Teacher(People):
def grade(self):
print('{}正在评分'.format(self.name))
在上面的例子中,把Student类和Teacher类共有的属性提取出来,创建了一个新类,这个新类也叫Student和Teacher的父类/超类/基类,而Student和Teacher称为子类。继承的优点就是,子类可以遗传父类所有的属性and代码简介。缺点是强耦合。可以通过代码print(类.__bases__)
来查看这个类的所有父类。
继承是一种创建新类的方式,在python中,新建的类可以继承一个或多个父类,新建的类可以称为子类或派生类,父类又可以称为基类或者超类。
class ParentClass1: #定义父类
pass
class ParentClass2: #定义父类
pass
class SubClass1(ParentClass1): #单继承
pass
class SubClass2(ParentClass1,ParentClass2): #多继承
pass
>>> SubClass2.__bases__
(<class '__main__.ParentClass1'>, <class '__main__.ParentClass2'>)
新式类:在python3中,但凡是继承了object的子类以及该类的子子孙孙类都是新式类。(python3的所有类都是新式类)
经典类:一般python2中的类都是经典类。
5.1.1 继承与抽象
要找出类与类之间的继承关系,需要先抽象,再继承。抽象即总结相似之处,总结对象之间的相似之处得到类,总结类与类之间的相似之处就可以得到父类,如下图所示
基于抽象的结果,我们就找到了继承关系
基于上图我们可以看出类与类之间的继承指的是什么’是’什么的关系(比如人类,猪类,猴类都是动物类)。子类可以继承/遗传父类所有的属性,因而继承可以用来解决类与类之间的代码重用性问题。比如我们按照定义Student类的方式再定义一个Teacher类。
class Teacher:
school='清华大学'
def __init__(self,name,sex,age):
self.name=name
self.sex=sex
self.age=age
def teach(self):
print('%s is teaching' %self.name)
类Teacher与Student之间存在重复的代码,老师与学生都是人类,所以我们可以得出如下继承关系,实现代码重用
class People:
school='清华大学'
def __init__(self,name,sex,age):
self.name=name
self.sex=sex
self.age=age
class Student(People):
def choose(self):
print('%s is choosing a course' %self.name)
class Teacher(People):
def teach(self):
print('%s is teaching' %self.name)
Teacher类内并没有定义__init__方法,但是会从父类中找到__init__,因而仍然可以正常实例化,如下
>>> teacher1=Teacher('lili','male',18)
>>> teacher1.school,teacher1.name,teacher1.sex,teacher1.age
('清华大学', 'lili', 'male', 18)
5.1.2 属性查找
既然是涉及到继承,那么对象调用的属性是如何查找的呢?见例
# 例一
class Bar:
def f1(self):
print('from Bar.f1')
def f2(self):
print('from Bar.f2')
self.f1()
class Foo(Bar):
def f1(self):
print('from Foo.f1')
obj=Foo()
obj.f2()-----># from Bar.f2
# from Foo.f1
obj是Foo的对象,当它想要调用f2时,先从自己的名称空间找,但发现没有找到f2,就去定义自己的类里面寻找,同样也没有。再向自己类的父类里面寻找,找到了,这时候可以调用。但需要注意一点!对象的功能属性调用的时候,把自己当作第一个参数传入,因此调用f2的时候f2内的代码是这样的:
def f2(obj):
print('from Bar.f2')
obj.f1()
结果一目了然。再考虑下面这种情况:
# 例二:
class Bar:
def __f1(self):
print('from Bar.f1')
def __f2(self):
print('from Bar.f2')
self.f1()
class Foo(Bar):
def __f1(self):
print('from Foo.f1')
obj=Foo()
obj.f2()-----># from Bar.f2
# from Bar.f1
这里为什么会出现不同的结果呢?因为隐藏属性在类的定义阶段会被改名,所以Bar的两个功能属性变成了下面的样子:
class Bar:
def _Bar__f1(self):
print('from Bar.f1')
def _Bar__f2(obj):
print('from Bar.f2')
obj._Bar__f1()
而obj本身是没有_Bar__f1()
这个名字的属性的,所以从定义自己的类里面找,还是没有,再去父类里面找,找到了,只在父类里面有相同名字的属性,成功调用!
5.2 继承的实现原理
5.2.1 菱形问题
大多数面向对象语言都不支持多继承,而在python中,一个子类是可以同时继承多个父类的,这固然可以带来一个子类对多个不同父类加以重用的好处,但也有可能引发著名的“Diamond problem”菱形问题(或者钻石问题,有时候也被称为“死亡钻石”),菱形其实就是对下面这种继承结构的形象比喻。
A类在顶部,B类和C类分别位于其下方,D类在底部将两者连接在一起形成菱形。
这种继承结构下导致的问题称之为菱形问题:如果A中有一个方法,B和/或C都重写了该方法,而D没有重写它,那么D继承的是哪个版本的方法:B的还是C的?如下所示
class A(object):
def test(self):
print('from A')
class B(A):
def test(self):
print('from B')
class C(A):
def test(self):
print('from C')
class D(B,C):
pass
obj = D()
obj.test() # 结果为:from B
要想搞明白obj.test()是如何找到方法test的,需要了解python的继承实现原理
5.2.2 继承原理
python到底是如何实现继承的呢? 对于你定义的每一个类,Python都会计算出一个方法解析顺序(MRO)列表,该MRO列表就是一个简单的所有基类的线性顺序列表,如下
>>> D.mro() # 新式类内置了mro方法可以查看线性列表的内容,经典类没有该内置该方法
[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]
python会在MRO列表上从左到右开始查找基类,直到找到第一个匹配这个属性的类为止。 而这个MRO列表的构造是通过一个C3线性化算法来实现的。我们不去深究这个算法的数学原理,它实际上就是合并所有父类的MRO列表并遵循如下三条准则:
- 子类会先于父类被检查
- 多个父类会根据它们在列表中的顺序被检查
- 如果下一个类存在两个合法选择,选择第一个父类
所以obj.test()的查找顺序是,先从对象obj本身的属性里找方法test,没有找到,则参照属性的发起者(即obj)所处类D的MRO列表来依次检索,首先在类D中未找到,然后在B中找到方法test。
ps:
- 由对象发起的属性查找,会从对象自身的属性里检索,没有则会按照
对象的类.mro()
的顺序依次找下去- 由类发起的属性查找,会按照
当前类.mro()
规定的顺序依次查找下去
5.2.3 深度优先和广度优先
参照下述代码,多继承结构为非菱形结构,此时,会按照先找B这一条分支,然后再找C这一条分支,最后找D这一条分支的顺序直到找到我们想要的属性
class E:
def test(self):
print('from E')
class F:
def test(self):
print('from F')
class B(E):
def test(self):
print('from B')
class C(F):
def test(self):
print('from C')
class D:
def test(self):
print('from D')
class A(B, C, D):
# def test(self):
# print('from A')
pass
print(A.mro())
'''
[<class '__main__.A'>, <class '__main__.B'>, <class '__main__.E'>, <class '__main__.C'>, <class '__main__.F'>, <class '__main__.D'>, <class 'object'>]
'''
obj = A()
obj.test() # 结果为:from B
# 可依次注释上述类中的方法test来进行验证
如果没有关系为菱形结构,那么经典类与新式类会有不同的mro,分别对应属性的两种查找方式:深度优先和广度优先
class G: # 在python2中,未继承object的类及其子类,都是经典类
def test(self):
print('from G')
class E(G):
def test(self):
print('from E')
class F(G):
def test(self):
print('from F')
class B(E):
def test(self):
print('from B')
class C(F):
def test(self):
print('from C')
class D(G):
def test(self):
print('from D')
class A(B,C,D):
# def test(self):
# print('from A')
pass
obj = A()
obj.test() # 如上图,查找顺序为:obj->A->B->E->G->C->F->D->object
# 可依次注释上述类中的方法test来进行验证,注意请在python2.x中进行测试
class G(object):
def test(self):
print('from G')
class E(G):
def test(self):
print('from E')
class F(G):
def test(self):
print('from F')
class B(E):
def test(self):
print('from B')
class C(F):
def test(self):
print('from C')
class D(G):
def test(self):
print('from D')
class A(B,C,D):
# def test(self):
# print('from A')
pass
obj = A()
obj.test() # 如上图,查找顺序为:obj->A->B->E->C->F->D->G->object
# 可依次注释上述类中的方法test来进行验证
5.2.4 Python Mixins机制
一个字类可以同时继承多个父类,这样的设计常被人诟病,一来它有可能导致可恶的菱形问题,二来在人的世界观里面继承应该是个“is-a”的关系。比如轿车类之所以可以继承交通工具类,是因为基于人的世界观,我们可以说:轿车是一个(“is-a”)交通工具,而在人的世界观里面,一个物品不可能是多种不同的东西。因此多重继承在人的世界观里是说不通的,它仅仅是代码层面的逻辑。不过有没有这种情况,一个类的确是需要继承多个类呢?
答案是有,我们还是可以拿交通工具举例子:
民航飞机、直升飞机、轿车都是一个(is-a)交通工具,前两者都有一个功能是飞行fly,但是轿车没有,所以如下所示我们把飞机功能放到交通工具这个父类下是不合理的。
class Vehicle: # 交通工具
def fly(self):
'''
飞行功能相应的代码
'''
print("I am flying")
class CivilAircraft(Vehicle): # 民航飞机
pass
class Helicopter(Vehicle): # 直升飞机
pass
class Car(Vehicle): # 汽车并不会飞,但按照上述继承关系,汽车也能飞了
pass
但是如果民航飞机和直升机都各自写自己的飞行fly方法,又违背了代码尽可能重用的原则(如果以后飞行器越来越多,那么二重复代码会越来越多)。
怎么办???为了尽可能地重用代码,那就只好在定义出一个飞行器的类,然后让民航飞机和直升飞机同时继承交通工具以及飞行器两个父类,这样就出现了多重继承。这时又违背了继承必须是”is-a”关系。这个难题该怎么解决?
不同的语言给出了不同的方法,让我们先来了解Java的处理方法。Java提供了接口interface功能,来实现多重继承:
// 抽象基类:交通工具类
public abstract class Vehicle {
}
// 接口:飞行器
public interface Flyable {
public void fly();
}
// 类:实现了飞行器接口的类,在该类中实现具体的fly方法,这样下面民航飞机与直升飞机在实现fly时直接重用即可
public class FlyableImpl implements Flyable {
public void fly() {
System.out.println("I am flying");
}
}
// 民航飞机,继承自交通工具类,并实现了飞行器接口
public class CivilAircraft extends Vehicle implements Flyable {
private Flyable flyable;
public CivilAircraft() {
flyable = new FlyableImpl();
}
public void fly() {
flyable.fly();
}
}
// 直升飞机,继承自交通工具类,并实现了飞行器接口
public class Helicopter extends Vehicle implements Flyable {
private Flyable flyable;
public Helicopter() {
flyable = new FlyableImpl();
}
public void fly() {
flyable.fly();
}
}
// 汽车,继承自交通工具类,
public class Car extends Vehicle {
}
现在我们的飞机同时具有了交通工具及飞行器两种属性,而且我们不需要重写飞行器中的飞行方法,同时我们没有破坏单一继承的原则。飞机就是一种交通工具,可飞行的能力是飞机的属性,通过继承接口来获取。
回到主题,Python语言可没有接口功能,但Python提供了Mixins机制,简单来说Mixins机制指的是子类混合(mixin)不同类的功能,而这些类采用统一的命名规范(例如Mixin后缀),以此标识这些类只是用来混合功能的,并不是用来标识子类的从属"is-a"关系的,所以Mixins机制本质仍是多继承,但同样遵守”is-a”关系,如下
class Vehicle: # 交通工具
pass
class FlyableMixin:
def fly(self):
'''
飞行功能相应的代码
'''
print("I am flying")
class CivilAircraft(FlyableMixin, Vehicle): # 民航飞机
pass
class Helicopter(FlyableMixin, Vehicle): # 直升飞机
pass
class Car(Vehicle): # 汽车
pass
# ps: 采用某种规范(如命名规范)来解决具体的问题是python惯用的套路
可以看到,上面的CivilAircraft、Helicopter类实现了多继承,不过它继承的第一个类我们起名为FlyableMixin,而不是Flyable,这个并不影响功能,但是会告诉后来读代码的人,这个类是一个Mixin类,表示混入(mix-in),这种命名方式就是用来明确地告诉别人(python语言惯用的手法),这个类是作为功能添加到子类中,而不是作为父类,它的作用同Java中的接口。所以从含义上理解,CivilAircraft、Helicopter类都只是一个Vehicle,而不是一个飞行器。
使用Mixin类实现多重继承要非常小心
- 首先它必须表示某一种功能,而不是某个物品,python 对于mixin类的命名方式一般以 Mixin, able, ible 为后缀
- 其次它必须责任单一,如果有多个功能,那就写多个Mixin类,一个类可以继承多个Mixin,为了保证遵循继承的“is-a”原则,只能继承一个标识其归属含义的父类
- 然后,它不依赖于子类的实现
- 最后,子类即便没有继承这个Mixin类,也照样可以工作,就是缺少了某个功能。(比如飞机照样可以载客,就是不能飞了)
Mixins是从多个类中重用代码的好方法,但是需要付出相应的代价,我们定义的Minx类越多,子类的代码可读性就会越差,并且更恶心的是,在继承的层级变多时,代码阅读者在定位某一个方法到底在何处调用时会晕头转向,如下
class Displayer:
def display(self, message):
print(message)
class LoggerMixin:
def log(self, message, filename='logfile.txt'):
with open(filename, 'a') as fh:
fh.write(message)
def display(self, message):
super().display(message) # super的用法请参考下一小节
self.log(message)
class MySubClass(LoggerMixin, Displayer):
def log(self, message):
super().log(message, filename='subclasslog.txt')
obj = MySubClass()
obj.display("This string will be shown and logged in subclasslog.txt")
# 属性查找的发起者是obj,所以会参照类MySubClass的MRO来检索属性
#[<class '__main__.MySubClass'>, <class '__main__.LoggerMixin'>, <class '__main__.Displayer'>, <class 'object'>]
# 1、首先会去对象obj的类MySubClass找方法display,没有则去类LoggerMixin中找,找到开始执行代码
# 2、执行LoggerMixin的第一行代码:执行super().display(message),参照MySubClass.mro(),super会去下一个类即类Displayer中找,找到display,开始执行代码,打印消息"This string will be shown and logged in subclasslog.txt"
# 3、执行LoggerMixin的第二行代码:self.log(message),self是对象obj,即obj.log(message),属性查找的发起者为obj,所以会按照其类MySubClass.mro(),即MySubClass->LoggerMixin->Displayer->object的顺序查找,在MySubClass中找到方法log,开始执行super().log(message, filename='subclasslog.txt'),super会按照MySubClass.mro()查找下一个类,在类LoggerMixin中找到log方法开始执行,最终将日志写入文件subclasslog.txt
5.3 继承应用
考虑下面这种场景:
class Student:
def __init__(self,name,age,gender):
self.name=name
self.age=age
self.gender=gender
def choose_course(self):
print('{}正在选课'.format(self.name))
class Teacher:
def __inti__(self,name,age,gender,level)
self.name=name
self.age=age
self.gender=gender
self.level=level
def grade(self):
print('{}正在打分'.format(self.name))
我们会发现其中有很多冗余代码,我们第一时间应该想到是可以把它们都提出来,但是我们又发现,其中Student部分的__init__
部分完全重复,但是Teacher部分的__init__
大部分代码重复,只有一个数据属性是需要自己定义的,我们可以把两者都重复的提取出来,放到父类里面,但是不重复的属性怎么办呢?提出来的属性又可以如何定义呢?介绍两种继承的方法。
5.3.1 指名道姓要(与继承无关)
修改后的代码如下:
class People:
def __init__(self,name,age,gender):
self.name=name
self.age=age
self.gender=gender
class Student:
def choose_course(self):
print('{}正在选课'.format(self.name))
class Teacher:
def __init__(self,name,age,gender,level)
People.__init__(self,name,age,gender) # 类调类的功能属性,相当于调普通函数,正常传参
self.level=level
def grade(self):
print('{}正在打分'.format(self.name))
5.3.2 super() ‘找爸爸要’(与继承有关)
在介绍super()
方法之前,先介绍一下继承实现的原理。继承的查找顺序是遵循发起查找的mro列表的次序进心搜索的。拿个例子介绍一下mro列表:
class A:
...
class M:
...
class B(A):
...
class C(M):
...
class D:
...
class E(B,C,D):
...
print(E.mro())----->
# [<class '__main__.E'>, <class '__main__.B'>, <class '__main__.A'>, <class '__main__.C'>, <class '__main__.M'>, <class '__main__.D'>, <class 'object'>]
修改后代码如下:
class People:
def __init__(self,name,age,gender):
self.name=name
self.age=age
self.gender=gender
class Student:
def choose_course(self):
print('{}正在选课'.format(self.name))
class Teacher(People):
def __init__(self,name,age,gender,level)
super().__init__(name,age,gender) # super()会返回一个特殊的对象,该对象会参考发起属性查找的那一个类的mro列表,依次去当前类的父类中查找属性
self.level=level
def grade(self):
print('{}正在打分'.format(self.name))
super()会返回一个特殊的对象,该对象会参考发起属性查找的那一个类的mro列表,依次去当前类的父类中查找属性。
super()会返回一个特殊的对象,该对象会参考发起属性查找的那一个类的mro列表,依次去当前类的父类中查找属性。
super()会返回一个特殊的对象,该对象会参考发起属性查找的那一个类的mro列表,依次去当前类的父类中查找属性。
了解:
继承表达的是 is—a 的关系。如果想要使用多继承,遵循下面的两条规则:
1.用来表示归属关系的类往右放
2.用来添加功能的类往左放
class Vehicle:
...
class FlyAbleMixin:
def fly(self):
print('flying')
class CivilAircraft(FlyAbleMixin,Vehicle):
...
class Helicopter(FlyAbleMixin,Vehicle):
...
class Car(Vehicle):
...
组合(******)
如果类与类,对象与对象之间的关系是 “xx是xx” ,那么我们就可以定义子类与父类,对象与类的关系来表达这个关系。如果类与类,对象与对象之间的关系 “不是xx是xx的关系” ,是其他的关系,就需要用到组合的关系。给某一个对象添加属性,充分利用面向对象编程的优点,扩展性强。
class Course:
def __init__(self,name,period,price):
self.name=name
self.period=period
self.price=price
def tell_info(self):
print('<%s %s %s>' %(self.name,self.period,self.price))
class Date:
def __init__(self,year,mon,day):
self.year=year
self.mon=mon
self.day=day
def tell_birth(self):
print('<%s-%s-%s>' %(self.year,self.mon,self.day))
class People:
school='清华大学'
def __init__(self,name,sex,age):
self.name=name
self.sex=sex
self.age=age
#Teacher类基于继承来重用People的代码,基于组合来重用Date类和Course类的代码
class Teacher(People): #老师是人
def __init__(self,name,sex,age,title,year,mon,day):
super().__init__(name,age,sex)
self.birth=Date(year,mon,day) #老师有生日
self.courses=[] #老师有课程,可以在实例化后,往该列表中添加Course类的对象
def teach(self):
print('%s is teaching' %self.name)
python=Course('python','3mons',3000.0)
linux=Course('linux','5mons',5000.0)
teacher1=Teacher('lili','female',28,'博士生导师',1990,3,23)
# teacher1有两门课程
teacher1.courses.append(python)
teacher1.courses.append(linux)
# 重用Date类的功能
teacher1.birth.tell_birth()
# 重用Course类的功能
for obj in teacher1.courses:
obj.tell_info()
此时对象teacher1集对象独有的属性、Teacher类中的内容、Course类中的内容于一身(都可以访问到),是一个高度整合的产物
六. 类的三大特性之三 —— 多态
多态:同一种事物有多种形态。
多态性:多态性指的是可以在不考虑对象具体类型的情况下直接使用对象。
class Animal:
def say(self):
print('动物是这么叫的',end='')
class People(Animal):
def say(self):
super().say()
print('嘤嘤嘤')
class Dog(Animal):
def say(self):
super().say()
print('汪汪汪')
class Cat(Animal):
def say(self):
super().say()
print('喵喵喵')
obj1=People()
obj2=Dog()
obj3=Cat()
obj1.say()-----># '动物是这么叫的嘤嘤嘤'
obj2.say()-----># '动物是这么叫的汪汪汪'
obj3.say()-----># '动物是这么叫的喵喵喵'
# 甚至可以定制统一的接口接受传入的动物对象
def animal_say(animal):
animal.say()
其实我们之前一直在接触这种多态的思想,不管是字符串,列表,字典,元祖,都可以测量长度。因为这些数据类型都有__len__
方法。
我们可以强制规定子类使用父类的功能(通过定义抽象基类),也可以约定俗成靠程序员自我遵守统一的命名规范,下面我们简单示范一下两种情况。
# 代码示范:强制子类使用父类的方法
import abc
class Animal(metaclass=abc.ABCMeta): # 定义抽象基类,Animal已不可单独调用
@abd.abstractmethod # 强制约束所有子类都得有这些功能
def say(self):
print('动物是这样叫的',end='')
class Dog(Animal):
def say(self):
super().say()
print('汪汪汪')
class Cat(Animal):
def say(self):
super().say()
print('喵喵喵')
class Pig(Animal):
def say(self):
super().say()
print('哼哼哼')
# 定义了抽象基类,给父类的功能添加装饰器后,就规定了子类定义名字相同的这些功能,如果子类没有,就会报错
# 代码示范:靠程序员自觉性实现相同效果
class Dog:
def say(self):
print('汪汪汪')
class Cat:
def say(self):
print('喵喵喵')
class Pig:
def say(self):
print('哼哼哼')
但其实我们完全可以不依赖于继承,只需要制造出外观和行为相同对象,同样可以实现不考虑对象类型而使用对象,这正是Python崇尚的“鸭子类型”(duck typing):“如果看起来像、叫声像而且走起路来像鸭子,那么它就是鸭子”。比起继承的方式,鸭子类型在某种程度上实现了程序的松耦合度。
#二者看起来都像文件,因而就可以当文件一样去用,然而它们并没有直接的关系
class Txt: #Txt类有两个与文件类型同名的方法,即read和write
def read(self):
pass
def write(self):
pass
class Disk: #Disk类也有两个与文件类型同名的方法:read和write
def read(self):
pass
def write(self):
pass