一 对象的概念

“面向对象”的核心是“对象”二字,而对象的精髓在于“整合”,什么意思?

所有的程序都是由”数据”与“功能“组成,因而编写程序的本质就是定义出一系列的数据,然后定义出一系列的功能来对数据进行操作。在学习”对象“之前,程序中的数据与功能是分离开的,如下

  1. # 数据:name、age、sex
  2. name='lili'
  3. age=18
  4. sex='female'
  5. # 功能:tell_info
  6. def tell_info(name,age,sex):
  7. print('<%s:%s:%s>' %(name,age,sex))
  8. # 此时若想执行查看个人信息的功能,需要同时拿来两样东西,一类是功能tell_info,另外一类则是多个数据name、age、sex,然后才能执行,非常麻烦
  9. tell_info(name,age,sex)

在学习了“对象”之后,我们就有了一个容器,该容器可以盛放数据与功能,所以我们可以说:对象是把数据与功能整合到一起的产物,或者说”对象“就是一个盛放数据与功能的容器/箱子/盒子。
如果把”数据“比喻为”睫毛膏“、”眼影“、”唇彩“等化妆所需要的原材料;把”功能“比喻为眼线笔、眉笔等化妆所需要的工具,那么”对象“就是一个彩妆盒,彩妆盒可以把”原材料“与”工具“都装到一起

如果我们把”化妆“比喻为要执行的业务逻辑,此时只需要拿来一样东西即可,那就是彩妆盒,因为彩妆盒里整合了化妆所需的所有原材料与功能,这比起你分别拿来原材料与功能才能执行,要方便的多。

在了解了对象的基本概念之后,理解面向对象的编程方式就相对简单很多了,面向对象编程就是要造出一个个的对象,把原本分散开的相关数据与功能整合到一个个的对象里,这么做既方便使用,也可以提高程序的解耦合程度,进而提升了程序的可扩展性(需要强调的是,软件质量属性包含很多方面,面向对象解决的仅仅只是扩展性问题)

二 类与对象

类即类别/种类,是面向对象分析和设计的基石,如果多个对象有相似的数据与功能,那么该多个对象就属于同一种类。有了类的好处是:我们可以把同一类对象相同的数据与功能存放到类里,而无需每个对象都重复存一份,这样每个对象里只需存自己独有的数据即可,极大地节省了空间。所以,如果说对象是用来存放数据与功能的容器,那么类则是用来存放多个对象相同的数据与功能的容器。

综上所述,虽然我们是先介绍对象后介绍类,但是需要强调的是:在程序中,必须要事先定义类,然后再调用类产生对象(调用类拿到的返回值就是对象)。产生对象的类与对象之间存在关联,这种关联指的是:对象可以访问到类中共有的数据与功能,所以类中的内容仍然是属于对象的,类只不过是一种节省空间、减少代码冗余的机制,面向对象编程最终的核心仍然是去使用对象。
在了解了类与对象这两大核心概念之后,我们就可以来介绍一下面向对象编程啦。

三 面向对象编程

3.1 类的定义与实例化

我们以开发一个清华大学的选课系统为例,来简单介绍基于面向对象的思想如何编写程序

面向对象的基本思路就是把程序中要用到的、相关联的数据与功能整合到对象里,然后再去使用,但程序中要用到的数据以及功能那么多,如何找到相关连的呢?我需要先提取选课系统里的角色:学生、老师、课程等,然后显而易见的是:学生有学生相关的数据于功能,老师有老师相关的数据与功能,我们单以学生为例,

# 学生的数据有
学校
名字
年龄
性别
# 学生的功能有
选课

详细的

# 学生1:
    数据:
        学校=清华大学
        姓名=李建刚
        性别=男
        年龄=28
    功能:
        选课
# 学生2:
    数据:
        学校=清华大学
        姓名=王大力
        性别=女
        年龄=18
    功能:
        选课
# 学生3:
    数据:
        学校=清华大学
        姓名=牛嗷嗷
        性别=男
        年龄=38
    功能:
        选课

我们可以总结出一个学生类,用来存放学生们相同的数据与功能

# 学生类
    相同的特征:
        学校=清华大学
    相同的功能:
        选课

基于上述分析的结果,我们接下来需要做的就是在程序中定义出类,然后调用类产生对象

class Student: # 类的命名应该使用“驼峰体”
    school='清华大学' # 数据
    def choose(self): # 功能
        print('%s is choosing a course' %self.name)

类体最常见的是变量的定义和函数的定义,但其实类体可以包含任意Python代码,类体的代码在类定义阶段就会执行,因而会产生新的名称空间用来存放类中定义的名字,可以打印Student.dict来查看类这个容器内盛放的东西

>>> print(Student.__dict__)
{..., 'school': '清华大学', 'choose': <function Student.choose at 0x1018a2950>, ...}

调用类的过程称为将类实例化,拿到的返回值就是程序中的对象,或称为一个实例

>>> stu1=Student() # 每实例化一次Student类就得到一个学生对象
>>> stu2=Student()
>>> stu3=Student()

如此stu1、stu2、stu3全都一样了(只有类中共有的内容,而没有各自独有的数据),想在实例化的过程中就为三位学生定制各自独有的数据:姓名,性别,年龄,需要我们在类内部新增一个init方法,如下

class Student:
    school='清华大学'
    #该方法会在对象产生之后自动执行,专门为对象进行初始化操作,可以有任意代码,但一定不能返回非None的值
    def __init__(self,name,sex,age):
        self.name=name
        self.sex=sex
        self.age=age
    def choose(self): 
        print('%s is choosing a course' %self.name)

然后我们重新实例出三位学生

>>> stu1=Student('李建刚','男',28)
>>> stu2=Student('王大力','女',18)
>>> stu3=Student('牛嗷嗷','男',38)

单拿stu1的产生过程来分析,调用类会先产生一个空对象stu1,然后将stu1连同调用类时括号内的参数一起传给Student.init(stu1,’李建刚’,’男’,28)

def __init__(self, name, sex, age):
    self.name = name  # stu1.name = '李建刚'
    self.sex = sex    # stu1.sex = '男'
    self.age = age    # stu1.age = 28

会产生对象的名称空间,同样可以用dict查看

>>> stu1.__dict__
{'name': '李建刚', 'sex': '男', 'age': 28}

至此,我们造出了三个对象与一个类,对象存放各自独有的数据,类中存放对象们共有的内容
存的目的是为了用,那么如何访问对象或者类中存放的内容呢?

3.2 属性访问

3.2.1 类属性与对象属性

在类中定义的名字,都是类的属性,细说的话,类有两种属性:数据属性和函数属性,可以通过dict访问属性的值,比如Student.dict[‘school’],但Python提供了专门的属性访问语法
插图:恶搞图

>>> Student.school # 访问数据属性,等同于Student.__dict__['school']
'清华大学'
>>> Student.choose # 访问函数属性,等同于Student.__dict__['choose']
<function Student.choose at 0x1018a2950>
# 除了查看属性外,我们还可以使用Student.attrib=value(修改或新增属性),用del Student.attrib删除属性。

操作对象的属性也是一样

>>> stu1.name # 查看,等同于obj1.__dict__[‘name']
'李建刚'
>>> stu1.course=’python’ # 新增,等同于obj1.__dict__[‘course']='python'
>>> stu1.age=38 # 修改,等同于obj1.__dict__[‘age']=38
>>> del obj1.course # 删除,等同于del obj1.__dict__['course']

3.2.2 属性查找顺序与绑定方法

对象的名称空间里只存放着对象独有的属性,而对象们相似的属性是存放于类中的。对象在访问属性时,会优先从对象本身的dict中查找,未找到,则去类的dict中查找

1、类中定义的变量是类的数据属性,是共享给所有对象用的,指向相同的内存地址

# id都一样
print(id(Student.school)) # 4301108704
print(id(stu1.school)) # 4301108704
print(id(stu2.school)) # 4301108704
print(id(stu3.school)) # 4301108704

2、类中定义的函数是类的函数属性,类可以使用,但必须遵循函数的参数规则,有几个参数需要传几个参数

Student.choose(stu1) # 李建刚 is choosing a course
Student.choose(stu2) # 王大力 is choosing a course
Student.choose(stu3) # 牛嗷嗷 is choosing a course

但其实类中定义的函数主要是给对象使用的,而且是绑定给对象的,虽然所有对象指向的都是相同的功能,但是绑定到不同的对象就是不同的绑定方法,内存地址各不相同

print(id(Student.choose)) # 4335426280
print(id(stu1.choose)) # 4300433608
print(id(stu2.choose)) # 4300433608
print(id(stu3.choose)) # 4300433608

绑定到对象的方法特殊之处在于,绑定给谁就应该由谁来调用,谁来调用,就会将’谁’本身当做第一个参数自动传入(方法init也是一样的道理)

stu1.choose()  # 等同于Student.choose(stu1)
stu2.choose()  # 等同于Student.choose(stu2)
stu3.choose()  # 等同于Student.choose(stu3)

绑定到不同对象的choose技能,虽然都是选课,但李建刚选的课,不会选给王大力,这正是”绑定“二字的精髓所在。

#注意:绑定到对象方法的这种自动传值的特征,决定了在类中定义的函数都要默认写一个参数self,self可以是任意名字,但命名为self是约定俗成的。

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']

四 隐藏属性

Python的Class机制采用双下划线开头的方式将属性隐藏起来(设置成私有的),但其实这仅仅只是一种变形操作,类中所有双下滑线开头的属性都会在类定义阶段、检测语法时自动变成“类名_属性名”的形式:

class Foo:
    __N=0 # 变形为_Foo__N
    def __init__(self): # 定义函数时,会检测函数语法,所以__开头的属性也会变形
        self.__x=10 # 变形为self._Foo__x
    def __f1(self): # 变形为_Foo__f1
        print('__f1 run')
    def f2(self):  # 定义函数时,会检测函数语法,所以__开头的属性也会变形
        self.__f1() #变形为self._Foo__f1()
print(Foo.__N) # 报错AttributeError:类Foo没有属性__N
obj = Foo()
print(obbj.__x) # 报错AttributeError:对象obj没有属性__x

这种变形需要注意的问题是:
1、在类外部无法直接访问双下滑线开头的属性,但知道了类名和属性名就可以拼出名字:类名_属性,然后就可以访问了,如Foo._A__N,所以说这种操作并没有严格意义上地限制外部访问,仅仅只是一种语法意义上的变形。

>>> Foo.__dict__
mappingproxy({..., '_Foo__N': 0, ...})
>>> obj.__dict__
{'_Foo__x': 10}
>>> Foo._Foo__N
0
>>> obj._Foo__x
10
>>> obj._Foo__N
0

2、在类内部是可以直接访问双下滑线开头的属性的,比如self.__f1(),因为在类定义阶段类内部双下滑线开头的属性统一发生了变形。

>>> obj.f2()
__f1 run

3、变形操作只在类定义阶段发生一次,在类定义之后的赋值操作,不会变形。

>>> Foo.__M=100
>>> Foo.__dict__
mappingproxy({..., '__M': 100,...})
>>> Foo.__M
100
>>> obj.__y=20
>>> obj.__dict__
{'__y': 20, '_Foo__x': 10}
>>> obj.__y
20

五 开放接口

定义属性就是为了使用,所以隐藏并不是目的

5.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

5.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()

总结隐藏属性与开放接口,本质就是为了明确地区分内外,类内部可以修改封装内的东西而不影响外部调用者的代码;而类外部只需拿到一个接口,只要接口名、参数不变,则无论设计者如何改变内部实现代码,使用者均无需改变代码。这就提供一个良好的合作基础,只要接口这个基础约定不变,则代码的修改不足为虑。

六 property

BMI指数是用来衡量一个人的体重与身高对健康影响的一个指标,计算公式为

体质指数(BMI)=体重(kg)÷身高^2(m)
EX:70kg÷(1.75×1.75)=22.86

身高或体重是不断变化的,因而每次想查看BMI值都需要通过计算才能得到,但很明显BMI听起来更像是一个特征而非功能,为此Python专门提供了一个装饰器property,可以将类中的函数“伪装成”对象的数据属性,对象在访问该特殊属性时会触发功能的执行,然后将返回值作为本次访问的结果,例如

>>> class People:
...     def __init__(self,name,weight,height):
...         self.name=name
...         self.weight=weight
...         self.height=height
...     @property
...     def bmi(self):
...         return self.weight / (self.height**2)
...
>>> obj=People('lili',75,1.85)
>>> obj.bmi #触发方法bmi的执行,将obj自动传给self,执行后返回值作为本次引用的结果
21.913805697589478

使用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,’Egon')
>>> f.name=123 #触发name.setter对应的的函数name(f,123),抛出异常TypeError
>>> del f.name #触发name.deleter对应的函数name(f),抛出异常PermissionError

七 继承介绍

继承是一种创建新类的方式,在Python中,新建的类可以继承一个或多个父类,新建的类可称为子类或派生类,父类又可称为基类或超类

class ParentClass1: #定义父类
    pass
class ParentClass2: #定义父类
    pass
class SubClass1(ParentClass1): #单继承
    pass
class SubClass2(ParentClass1,ParentClass2): #多继承
    pass

通过类的内置属性bases可以查看类继承的所有父类

>>> SubClass2.__bases__
(<class '__main__.ParentClass1'>, <class '__main__.ParentClass2'>)

在Python2中有经典类与新式类之分,没有显式地继承object类的类,以及该类的子类,都是经典类,显式地继承object的类,以及该类的子类,都是新式类。而在Python3中,即使没有显式地继承object,也会默认继承该类,如下

>>> ParentClass1.__bases__
(<class ‘object'>,)
>>> ParentClass2.__bases__
(<class 'object'>,)

因而在Python3中统一都是新式类,关于经典类与新式类的区别,我们稍后讨论

提示:object类提供了一些常用内置方法的实现,如用来在打印对象时返回字符串的内置方法__str__

八 继承与抽象

要找出类与类之间的继承关系,需要先抽象,再继承。抽象即总结相似之处,总结对象之间的相似之处得到类,总结类与类之间的相似之处就可以得到父类,如下图所示
基于抽象的结果,我们就找到了继承关系
基于上图我们可以看出类与类之间的继承指的是什么’是’什么的关系(比如人类,猪类,猴类都是动物类)。子类可以继承/遗传父类所有的属性,因而继承可以用来解决类与类之间的代码重用性问题。比如我们按照定义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)

九 属性查找

有了继承关系,对象在查找属性时,先从对象自己的dict中找,如果没有则去子类中找,然后再去父类中找……

>>> class Foo:
...     def f1(self):
...         print('Foo.f1')
...     def f2(self):
...         print('Foo.f2')
...         self.f1()
... 
>>> class Bar(Foo):
...     def f1(self):
...         print('Foo.f1')
... 
>>> b=Bar()
>>> b.f2()
Foo.f2
Foo.f1

b.f2()会在父类Foo中找到f2,先打印Foo.f2,然后执行到self.f1(),即b.f1(),仍会按照:对象本身->类Bar->父类Foo的顺序依次找下去,在类Bar中找到f1,因而打印结果为Foo.f1

父类如果不想让子类覆盖自己的方法,可以采用双下划线开头的方式将方法设置为私有的

>>> class Foo:
...     def __f1(self): # 变形为_Foo__fa
...         print('Foo.f1') 
...     def f2(self):
...         print('Foo.f2')
...         self.__f1() # 变形为self._Foo__fa,因而只会调用自己所在的类中的方法
... 
>>> class Bar(Foo):
...     def __f1(self): # 变形为_Bar__f1
...         print('Foo.f1')
... 
>>> 
>>> b=Bar()
>>> b.f2() #在父类中找到f2方法,进而调用b._Foo__f1()方法,同样是在父类中找到该方法
Foo.f2
Foo.f1

十 继承的实现原理

10.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的继承实现原理

10.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列表并遵循如下三条准则:

1.子类会先于父类被检查
2.多个父类会根据它们在列表中的顺序被检查
3.如果对下一个类存在两个合法的选择,选择第一个父类

所以obj.test()的查找顺序是,先从对象obj本身的属性里找方法test,没有找到,则参照属性查找的发起者(即obj)所处类D的MRO列表来依次检索,首先在类D中未找到,然后再B中找到方法test
ps:

1.由对象发起的属性查找,会从对象自身的属性里检索,没有则会按照对象的类.mro()规定的顺序依次找下去,
2.由类发起的属性查找,会按照当前类.mro()规定的顺序依次找下去,

10.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来进行验证

10.4 Pyton 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
    

    ps:课外了解小知识

    Java只允许接口的多重继承。接口本质上是抽象基类,具有所有抽象方法,没有数据成员。
    与java一样,python也有抽象类的概念但是同样需要借助模块实现,抽象类是一个特殊的类,它的特殊之处在于只能被继承,不能被实例化,继承的子类必须实现抽象基类规定的方法,这样便可保证始终只有一个特定方法或属性的实现,并且不会产生歧义,因而也可以起到避免菱形问题的作用
    java的interface:https://www.cnblogs.com/linhaifeng/articles/7340153.html#_label6
    python的抽象基类:https://www.cnblogs.com/linhaifeng/articles/7340153.html#_label7
    

    十一 派生与方法重用

    子类可以派生出自己新的属性,在进行属性查找时,子类中的属性名会优先于父类被查找,例如每个老师还有职称这一属性,我们就需要在Teacher类中定义该类自己的init覆盖父类的

    >>> class People:
    ...     school='清华大学'
    ...     
    ...     def __init__(self,name,sex,age):
    ...         self.name=name
    ...         self.sex=sex
    ...         self.age=age
    ... 
    >>> class Teacher(People):
    ...     def __init__(self,name,sex,age,title): # 派生
    ...         self.name=name
    ...         self.sex=sex
    ...         self.age=age
    ...         self.title=title
    ...     def teach(self):
    ...         print('%s is teaching' %self.name)
    ... 
    >>> obj=Teacher('lili','female',28,'高级讲师') #只会找自己类中的__init__,并不会自动调用父类的
    >>> obj.name,obj.sex,obj.age,obj.title
    ('lili', 'female', 28, '高级讲师')
    

    很明显子类Teacher中init内的前三行又是在写重复代码,若想在子类派生出的方法内重用父类的功能,有两种实现方式
    方法一:“指名道姓”地调用某一个类的函数

    >>> class Teacher(People):
    ...     def __init__(self,name,sex,age,title):
    ...         People.__init__(self,name,age,sex) #调用的是函数,因而需要传入self
    ...         self.title=title
    ...     def teach(self):
    ...         print('%s is teaching' %self.name)
    ...
    

方法二:super()
调用super()会得到一个特殊的对象,该对象专门用来引用父类的属性,且严格按照MRO规定的顺序向后查找

>>> class Teacher(People):
...     def __init__(self,name,sex,age,title):
...         super().__init__(name,age,sex) #调用的是绑定方法,自动传入self
...         self.title=title
...     def teach(self):
...         print('%s is teaching' %self.name)
...

提示:在Python2中super的使用需要完整地写成super(自己的类名,self) ,而在python3中可以简写为super()。

这两种方式的区别是:方式一是跟继承没有关系的,而方式二的super()是依赖于继承的,并且即使没有直接继承关系,super()仍然会按照MRO继续往后查找

>>> #A没有继承B
... class A:
...     def test(self):
...         super().test()
... 
>>> class B:
...     def test(self):
...         print('from B')
... 
>>> class C(A,B):
...     pass
... 
>>> C.mro() # 在代码层面A并不是B的子类,但从MRO列表来看,属性查找时,就是按照顺序C->A->B->object,B就相当于A的“父类”
[<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>,<class ‘object'>]
>>> obj=C()
>>> obj.test() # 属性查找的发起者是类C的对象obj,所以中途发生的属性查找都是参照C.mro()
from B

obj.test()首先找到A下的test方法,执行super().test()会基于MRO列表(以C.mro()为准)当前所处的位置继续往后查找(),然后在B中找到了test方法并执行。
关于在子类中重用父类功能的这两种方式,使用任何一种都可以,但是在最新的代码中还是推荐使用super()

十二 组合

在一个类中以另外一个类的对象作为数据属性,称为类的组合。组合与继承都是用来解决代码的重用性问题。不同的是:继承是一种“是”的关系,比如老师是人、学生是人,当类之间有很多相同的之处,应该使用继承;而组合则是一种“有”的关系,比如老师有生日,老师有多门课程,当类之间有显著不同,并且较小的类是较大的类所需要的组件时,应该使用组合,如下示例

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 talk(self):
        pass
class Cat(Animal): #动物的形态之一:猫
    def talk(self):
        print('喵喵喵')
class Dog(Animal): #动物的形态之二:狗
    def talk(self):
        print('汪汪汪')
class Pig(Animal): #动物的形态之三:猪
    def talk(self):
        print('哼哼哼')
#实例化得到三个对象
>>> cat=Cat()
>>> dog=Dog()
>>> pig=Pig()

多态性指的是可以在不用考虑对象具体类型的情况下而直接使用对象,这就需要在设计时,把对象的使用方法统一成一种:例如cat、dog、pig都是动物,但凡是动物肯定有talk方法,于是我们可以不用考虑它们三者的具体是什么类型的动物,而直接使用

>>> cat.talk()
喵喵喵
>>> dog.talk()
汪汪汪
>>> pig.talk()
哼哼哼

更进一步,我们可以定义一个统一的接口来使用

>>> def Talk(animal):
...     animal.talk()
... 
>>> Talk(cat)
喵喵喵
>>> Talk(dog)
汪汪汪
>>> Talk(pig)
哼哼哼

Python中一切皆对象,本身就支持多态性

# 我们可以在不考虑三者类型的情况下直接使用统计三个对象的长度
s.__len__()
l.__len__()
t.__len__()
# Python内置了一个统一的接口
len(s)
len(l)
len(t)

多态性的好处在于增强了程序的灵活性和可扩展性,比如通过继承Animal类创建了一个新的类,实例化得到的对象obj,可以使用相同的方式使用obj.talk()

>>> class Wolf(Animal): #动物的另外一种形态:狼
...     def talk(self):
...         print('嗷...')
... 
>>> wolf=Wolf() # 实例出一头狼
>>> wolf.talk() # 使用者根本无需关心wolf是什么类型而调用talk
嗷...

综上我们得知,多态性的本质在于不同的类中定义有相同的方法名,这样我们就可以不考虑类而统一用一种方式去使用对象,可以通过在父类引入抽象类的概念来硬性限制子类必须有某些方法名

import abc
# 指定metaclass属性将类设置为抽象类,抽象类本身只是用来约束子类的,不能被实例化
class Animal(metaclass=abc.ABCMeta):
    @abc.abstractmethod # 该装饰器限制子类必须定义有一个名为talk的方法
    def talk(self): # 抽象方法中无需实现具体的功能
        pass
class Cat(Animal): # 但凡继承Animal的子类都必须遵循Animal规定的标准
    def talk(self):
        pass
cat=Cat() # 若子类中没有一个名为talk的方法则会抛出异常TypeError,无法实例化

但其实我们完全可以不依赖于继承,只需要制造出外观和行为相同对象,同样可以实现不考虑对象类型而使用对象,这正是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

十四 绑定方法与非绑定方法

类中定义的函数分为两大类:绑定方法和非绑定方法
其中绑定方法又分为绑定到对象的对象方法和绑定到类的类方法。
在类中正常定义的函数默认是绑定到对象的,而为某个函数加上装饰器@classmethod后,该函数就绑定到了类。

我们在之前的章节中已经介绍过对象方法了,本节我们主要介绍类方法。类方法通常用来在init的基础上提供额外的初始化实例的方式

# 配置文件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

绑定到类的方法就是专门给类用的,但其实对象也可以调用,只不过自动传入的第一个参数仍然是类,也就是说这种调用是没有意义的,并且容易引起混淆,这也是Python的对象系统与其他面向对象语言对象系统的区别之一,比如Smalltalk和Ruby中,绑定到类的方法与绑定到对象的方法是严格区分开的。

十五 非绑定方法

为类中某个函数加上装饰器@staticmethod后,该函数就变成了非绑定方法,也称为静态方法。该方法不与类或对象绑定,类与对象都可以来调用它,但它就是一个普通函数而已,因而没有自动传值那么一说

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>

总结绑定方法与非绑定方法的使用:若类中需要一个功能,该功能的实现代码中需要引用对象则将其定义成对象方法、需要引用类则将其定义成类方法、无需引用类或对象则将其定义成静态方法。

十六 反射

python是动态语言,而反射(reflection)机制被视为动态语言的关键。
反射机制指的是在程序的运行状态中
对于任意一个类,都可以知道这个类的所有属性和方法;
对于任意一个对象,都能够调用他的任意方法和属性。
这种动态获取程序信息以及动态调用对象的功能称为反射机制。

在python中实现反射非常简单,在程序运行过程中,如果我们获取一个不知道存有何种属性的对象,若想操作其内部属性,可以先通过内置函数dir来获取任意一个类或者对象的属性列表,列表中全为字符串格式

>>> class People:
...     def __init__(self,name,age,gender):
...         self.name=name
...         self.age=age
...         self.gender=gender
... 
>>> obj=People('egon',18,'male')
>>> dir(obj) # 列表中查看到的属性全为字符串
[......,'age', 'gender', 'name']

接下来就是想办法通过字符串来操作对象的属性了,这就涉及到内置函数hasattr、getattr、setattr、delattr的使用了(Python中一切皆对象,类和对象都可以被这四个函数操作,用法一样)

class Teacher:
    def __init__(self,full_name):
        self.full_name =full_name
t=Teacher('Egon Lin')
# hasattr(object,'name')
hasattr(t,'full_name') # 按字符串'full_name'判断有无属性t.full_name
# getattr(object, 'name', default=None)
getattr(t,'full_name',None) # 等同于t.full_name,不存在该属性则返回默认值None
# setattr(x, 'y', v)
setattr(t,'age',18) # 等同于t.age=18
# delattr(x, 'y')
delattr(t,'age') # 等同于del t.age

基于反射可以十分灵活地操作对象的属性,比如将用户交互的结果反射到具体的功能执行

>>> class FtpServer:
...     def serve_forever(self):
...         while True:
...             inp=input('input your cmd>>: ').strip()
...             cmd,file=inp.split()
...             if hasattr(self,cmd): # 根据用户输入的cmd,判断对象self有无对应的方法属性
...                 func=getattr(self,cmd) # 根据字符串cmd,获取对象self对应的方法属性
...                 func(file)
...     def get(self,file):
...         print('Downloading %s...' %file)
...     def put(self,file):
...         print('Uploading %s...' %file)
... 
>>> server=FtpServer()
>>> server.serve_forever()
input your cmd>>: get a.txt
Downloading a.txt...
input your cmd>>: put a.txt
Uploading a.txt...

十七 内置方法

Python的Class机制内置了很多特殊的方法来帮助使用者高度定制自己的类,这些内置方法都是以双下划线开头和结尾的,会在满足某种条件时自动触发,我们以常用的strdel为例来简单介绍它们的使用。

str方法会在对象被打印时自动触发,print功能打印的就是它的返回值,我们通常基于方法来定制对象的打印信息,该方法必须返回字符串类型

>>> class People:
...     def __init__(self,name,age):
...         self.name=name
...         self.age=age
...     def __str__(self):
...         return '<Name:%s Age:%s>' %(self.name,self.age) #返回类型必须是字符串
... 
>>> p=People('lili',18)
>>> print(p) #触发p.__str__(),拿到返回值后进行打印
<Name:lili Age:18>

del会在对象被删除时自动触发。由于Python自带的垃圾回收机制会自动清理Python程序的资源,所以当一个对象只占用应用程序级资源时,完全没必要为对象定制del方法,但在产生一个对象的同时涉及到申请系统资源(比如系统打开的文件、网络连接等)的情况下,关于系统资源的回收,Python的垃圾回收机制便派不上用场了,需要我们为对象定制该方法,用来在对象被删除时自动触发回收系统资源的操作

class MySQL:
    def __init__(self,ip,port):
        self.conn=connect(ip,port) # 伪代码,发起网络连接,需要占用系统资源
    def __del__(self):
        self.conn.close() # 关闭网络连接,回收系统资源
obj=MySQL('127.0.0.1',3306) # 在对象obj被删除时,自动触发obj.__del__()

十八 元类介绍

什么是元类呢?一切源自于一句话:python中一切皆为对象。让我们先定义一个类,然后逐步分析

class StanfordTeacher(object):
    school='Stanford'
    def __init__(self,name,age):
        self.name=name
        self.age=age
    def say(self):
        print('%s says welcome to the Stanford to learn Python' %self.name)

所有的对象都是实例化或者说调用类而得到的(调用类的过程称为类的实例化),比如对象t1是调用类StanfordTeacher得到的

t1=StanfordTeacher('lili',18)
print(type(t1)) #查看对象t1的类是<class '__main__.StanfordTeacher'>

如果一切皆为对象,那么类StanfordTeacher本质也是一个对象,既然所有的对象都是调用类得到的,那么StanfordTeacher必然也是调用了一个类得到的,这个类称为元类
于是我们可以推导出===>产生StanfordTeacher的过程一定发生了:StanfordTeacher=元类(…)

print(type(StanfordTeacher)) # 结果为<class 'type'>,证明是调用了type这个元类而产生的StanfordTeacher,即默认的元类为type

十九 class关键字创建类的流程分析

上文我们基于python中一切皆为对象的概念分析出:我们用class关键字定义的类本身也是一个对象,负责产生该对象的类称之为元类(元类可以简称为类的类),内置的元类为type
class关键字在帮我们创建类时,必然帮我们调用了元类StanfordTeacher=type(…),那调用type时传入的参数是什么呢?必然是类的关键组成部分,一个类有三大组成部分,分别是
1、类名class_name=’StanfordTeacher’
2、基类们class_bases=(object,)
3、类的名称空间class_dic,类的名称空间是执行类体代码而得到的
调用type时会依次传入以上三个参数
综上,class关键字帮我们创建一个类应该细分为以下四个过程

补充:exec的用法

#exec:三个参数
#参数一:包含一系列python代码的字符串
#参数二:全局作用域(字典形式),如果不指定,默认为globals()
#参数三:局部作用域(字典形式),如果不指定,默认为locals()
#可以把exec命令的执行当成是一个函数的执行,会将执行期间产生的名字存放于局部名称空间中
g={
    'x':1,
    'y':2
}
l={}
exec('''
global x,z
x=100
z=200
m=300
''',g,l)
print(g) #{'x': 100, 'y': 2,'z':200,......}
print(l) #{'m': 300}

二十 自定义元类控制类StanfordTeacher的创建

一个类没有声明自己的元类,默认他的元类就是type,除了使用内置元类type,我们也可以通过继承type来自定义元类,然后使用metaclass关键字参数为一个类指定元类

class Mymeta(type): #只有继承了type类才能称之为一个元类,否则就是一个普通的自定义类
    pass
# StanfordTeacher=Mymeta('StanfordTeacher',(object),{...})
class StanfordTeacher(object,metaclass=Mymeta): 
    school='Stanford'
    def __init__(self,name,age):
        self.name=name
        self.age=age
    def say(self):
        print('%s says welcome to the Stanford to learn Python' %self.name)

自定义元类可以控制类的产生过程,类的产生过程其实就是元类的调用过程,即StanfordTeacher=Mymeta(‘StanfordTeacher’,(object),{…}),调用Mymeta会先产生一个空对象StanfordTeacher,然后连同调用Mymeta括号内的参数一同传给Mymeta下的init方法,完成初始化,于是我们可以

class Mymeta(type): #只有继承了type类才能称之为一个元类,否则就是一个普通的自定义类
    def __init__(self,class_name,class_bases,class_dic):
        # print(self) #<class '__main__.StanfordTeacher'>
        # print(class_bases) #(<class 'object'>,)
        # print(class_dic) #{'__module__': '__main__', '__qualname__': 'StanfordTeacher', 'school': 'Stanford', '__init__': <function StanfordTeacher.__init__ at 0x102b95ae8>, 'say': <function StanfordTeacher.say at 0x10621c6a8>}
        super(Mymeta, self).__init__(class_name, class_bases, class_dic)  # 重用父类的功能
        if class_name.islower():
            raise TypeError('类名%s请修改为驼峰体' %class_name)
        if '__doc__' not in class_dic or len(class_dic['__doc__'].strip(' \n')) == 0:
            raise TypeError('类中必须有文档注释,并且文档注释不能为空')
# StanfordTeacher=Mymeta('StanfordTeacher',(object),{...})
class StanfordTeacher(object,metaclass=Mymeta): 
    """
    类StanfordTeacher的文档注释
    """
    school='Stanford'
    def __init__(self,name,age):
        self.name=name
        self.age=age
    def say(self):
        print('%s says welcome to the Stanford to learn Python' %self.name)

二十一 自定义元类控制类StanfordTeacher的调用

储备知识:call

class Foo:
    def __call__(self, *args, **kwargs):
        print(self)
        print(args)
        print(kwargs)
obj=Foo()
#1、要想让obj这个对象变成一个可调用的对象,需要在该对象的类中定义一个方法__call__方法,该方法会在调用对象时自动触发
#2、调用obj的返回值就是__call__方法的返回值
res=obj(1,2,3,x=1,y=2)

由上例得知,调用一个对象,就是触发对象所在类中的call方法的执行,如果把StanfordTeacher也当做一个对象,那么在StanfordTeacher这个对象的类中也必然存在一个call方法

class Mymeta(type): #只有继承了type类才能称之为一个元类,否则就是一个普通的自定义类
    def __call__(self, *args, **kwargs):
        print(self) #<class '__main__.StanfordTeacher'>
        print(args) #('lili', 18)
        print(kwargs) #{}
        return 123
class StanfordTeacher(object,metaclass=Mymeta):
    school='Stanford'
    def __init__(self,name,age):
        self.name=name
        self.age=age
    def say(self):
        print('%s says welcome to the Stanford to learn Python' %self.name)
# 调用StanfordTeacher就是在调用StanfordTeacher类中的__call__方法
# 然后将StanfordTeacher传给self,溢出的位置参数传给*,溢出的关键字参数传给**
# 调用StanfordTeacher的返回值就是调用__call__的返回值
t1=StanfordTeacher('lili',18)
print(t1) #123

默认地,调用t1=StanfordTeacher(‘lili’,18)会做三件事
1、产生一个空对象obj
2、调用init方法初始化对象obj
3、返回初始化好的obj
对应着,StanfordTeacher类中的call方法也应该做这三件事

class Mymeta(type): #只有继承了type类才能称之为一个元类,否则就是一个普通的自定义类
    def __call__(self, *args, **kwargs): #self=<class '__main__.StanfordTeacher'>
        #1、调用__new__产生一个空对象obj
        obj=self.__new__(self) # 此处的self是类OldoyTeacher,必须传参,代表创建一个StanfordTeacher的对象obj
        #2、调用__init__初始化空对象obj
        self.__init__(obj,*args,**kwargs)
        #3、返回初始化好的对象obj
        return obj
class StanfordTeacher(object,metaclass=Mymeta):
    school='Stanford'
    def __init__(self,name,age):
        self.name=name
        self.age=age
    def say(self):
        print('%s says welcome to the Stanford to learn Python' %self.name)
t1=StanfordTeacher('lili',18)
print(t1.__dict__) #{'name': 'lili', 'age': 18}

上例的call相当于一个模板,我们可以在该基础上改写call的逻辑从而控制调用StanfordTeacher的过程,比如将StanfordTeacher的对象的所有属性都变成私有的

class Mymeta(type): #只有继承了type类才能称之为一个元类,否则就是一个普通的自定义类
    def __call__(self, *args, **kwargs): #self=<class '__main__.StanfordTeacher'>
        #1、调用__new__产生一个空对象obj
        obj=self.__new__(self) # 此处的self是类StanfordTeacher,必须传参,代表创建一个StanfordTeacher的对象obj
        #2、调用__init__初始化空对象obj
        self.__init__(obj,*args,**kwargs)
        # 在初始化之后,obj.__dict__里就有值了
        obj.__dict__={'_%s__%s' %(self.__name__,k):v for k,v in obj.__dict__.items()}
        #3、返回初始化好的对象obj
        return obj
class StanfordTeacher(object,metaclass=Mymeta):
    school='Stanford'
    def __init__(self,name,age):
        self.name=name
        self.age=age
    def say(self):
        print('%s says welcome to the Stanford to learn Python' %self.name)
t1=StanfordTeacher('lili',18)
print(t1.__dict__) #{'_StanfordTeacher__name': 'lili', '_StanfordTeacher__age': 18}

上例中涉及到查找属性的问题,比如self.new,请看下一小节

二十二 再看属性查找

结合python继承的实现原理+元类重新看属性的查找应该是什么样子呢???

在学习完元类后,其实我们用class自定义的类也全都是对象(包括object类本身也是元类type的 一个实例,可以用type(object)查看),我们学习过继承的实现原理,如果把类当成对象去看,将下述继承应该说成是:对象StanfordTeacher继承对象Foo,对象Foo继承对象Bar,对象Bar继承对象object

class Mymeta(type): #只有继承了type类才能称之为一个元类,否则就是一个普通的自定义类
    n=444
    def __call__(self, *args, **kwargs): #self=<class '__main__.StanfordTeacher'>
        obj=self.__new__(self)
        self.__init__(obj,*args,**kwargs)
        return obj
class Bar(object):
    n=333
class Foo(Bar):
    n=222
class StanfordTeacher(Foo,metaclass=Mymeta):
    n=111
    school='Stanford'
    def __init__(self,name,age):
        self.name=name
        self.age=age
    def say(self):
        print('%s says welcome to the Stanford to learn Python' %self.name)
print(StanfordTeacher.n) #自下而上依次注释各个类中的n=xxx,然后重新运行程序,发现n的查找顺序为StanfordTeacher->Foo->Bar->object->Mymeta->type

于是属性查找应该分成两层,一层是对象层(基于c3算法的MRO)的查找,另外一个层则是类层(即元类层)的查找

#查找顺序:
#1、先对象层:StanfordTeacher->Foo->Bar->object
#2、然后元类层:Mymeta->type

依据上述总结,我们来分析下元类Mymeta中call里的self.new的查找

class Mymeta(type): 
    n=444
    def __call__(self, *args, **kwargs): #self=<class '__main__.StanfordTeacher'>
        obj=self.__new__(self)
        print(self.__new__ is object.__new__) #True
class Bar(object):
    n=333
    # def __new__(cls, *args, **kwargs):
    #     print('Bar.__new__')
class Foo(Bar):
    n=222
    # def __new__(cls, *args, **kwargs):
    #     print('Foo.__new__')
class StanfordTeacher(Foo,metaclass=Mymeta):
    n=111
    school='Stanford'
    def __init__(self,name,age):
        self.name=name
        self.age=age
    def say(self):
        print('%s says welcome to the Stanford to learn Python' %self.name)
    # def __new__(cls, *args, **kwargs):
    #     print('StanfordTeacher.__new__')
StanfordTeacher('lili',18) #触发StanfordTeacher的类中的__call__方法的执行,进而执行self.__new__开始查找

总结,Mymeta下的call里的self.new在StanfordTeacher、Foo、Bar里都没有找到new的情况下,会去找object里的new,而object下默认就有一个new,所以即便是之前的类均未实现new,也一定会在object中找到一个,根本不会、也根本没必要再去找元类Mymeta->type中查找new

我们在元类的call中也可以用object.new(self)去造对象

但我们还是推荐在call中使用self.new(self)去创造空对象,因为这种方式会检索三个类StanfordTeacher->Foo->Bar,而object.new则是直接跨过了他们三个
最后说明一点

class Mymeta(type): #只有继承了type类才能称之为一个元类,否则就是一个普通的自定义类
    n=444
    def __new__(cls, *args, **kwargs):
        obj=type.__new__(cls,*args,**kwargs) # 必须按照这种传值方式
        print(obj.__dict__)
        # return obj # 只有在返回值是type的对象时,才会触发下面的__init__
        return 123
    def __init__(self,class_name,class_bases,class_dic):
        print('run。。。')
class StanfordTeacher(object,metaclass=Mymeta): #StanfordTeacher=Mymeta('StanfordTeacher',(object),{...})
    n=111
    school='Stanford'
    def __init__(self,name,age):
        self.name=name
        self.age=age
    def say(self):
        print('%s says welcome to the Stanford to learn Python' %self.name)
print(type(Mymeta)) #<class 'type'>
# 产生类StanfordTeacher的过程就是在调用Mymeta,而Mymeta也是type类的一个对象,那么Mymeta之所以可以调用,一定是在元类type中有一个__call__方法
# 该方法中同样需要做至少三件事:
# class type:
#     def __call__(self, *args, **kwargs): #self=<class '__main__.Mymeta'>
#         obj=self.__new__(self,*args,**kwargs) # 产生Mymeta的一个对象
#         self.__init__(obj,*args,**kwargs) 
#         return obj

六 作业

1、在元类中控制把自定义类的数据属性都变成大写

class Mymetaclass(type):
    def __new__(cls,name,bases,attrs):
        update_attrs={}
        for k,v in attrs.items():
            if not callable(v) and not k.startswith('__'):
                update_attrs[k.upper()]=v
            else:
                update_attrs[k]=v
        return type.__new__(cls,name,bases,update_attrs)
class Chinese(metaclass=Mymetaclass):
    country='China'
    tag='Legend of the Dragon' #龙的传人
    def walk(self):
        print('%s is walking' %self.name)
print(Chinese.__dict__)
'''
{'__module__': '__main__',
 'COUNTRY': 'China', 
 'TAG': 'Legend of the Dragon',
 'walk': <function Chinese.walk at 0x0000000001E7B950>,
 '__dict__': <attribute '__dict__' of 'Chinese' objects>,                                         
 '__weakref__': <attribute '__weakref__' of 'Chinese' objects>,
 '__doc__': None}
'''

2、在元类中控制自定义的类无需init方法
1.元类帮其完成创建对象,以及初始化操作;
2.要求实例化时传参必须为关键字形式,否则抛出异常TypeError: must use keyword argument
3.key作为用户自定义类产生对象的属性,且所有属性变成大写

class Mymetaclass(type):
    # def __new__(cls,name,bases,attrs):
    #     update_attrs={}
    #     for k,v in attrs.items():
    #         if not callable(v) and not k.startswith('__'):
    #             update_attrs[k.upper()]=v
    #         else:
    #             update_attrs[k]=v
    #     return type.__new__(cls,name,bases,update_attrs)
    def __call__(self, *args, **kwargs):
        if args:
            raise TypeError('must use keyword argument for key function')
        obj = object.__new__(self) #创建对象,self为类Foo
        for k,v in kwargs.items():
            obj.__dict__[k.upper()]=v
        return obj
class Chinese(metaclass=Mymetaclass):
    country='China'
    tag='Legend of the Dragon' #龙的传人
    def walk(self):
        print('%s is walking' %self.name)
p=Chinese(name='lili',age=18,sex='male')
print(p.__dict__)

3、在元类中控制自定义的类产生的对象相关的属性全部为隐藏属性

class Mymeta(type):
    def __init__(self,class_name,class_bases,class_dic):
        #控制类Foo的创建
        super(Mymeta,self).__init__(class_name,class_bases,class_dic)
    def __call__(self, *args, **kwargs):
        #控制Foo的调用过程,即Foo对象的产生过程
        obj = self.__new__(self)
        self.__init__(obj, *args, **kwargs)
        obj.__dict__={'_%s__%s' %(self.__name__,k):v for k,v in obj.__dict__.items()}
        return obj
class Foo(object,metaclass=Mymeta):  # Foo=Mymeta(...)
    def __init__(self, name, age,sex):
        self.name=name
        self.age=age
        self.sex=sex
obj=Foo('lili',18,'male')
print(obj.__dict__)

4、基于元类实现单例模式

#步骤五:基于元类实现单例模式
# 单例:即单个实例,指的是同一个类实例化多次的结果指向同一个对象,用于节省内存空间
# 如果我们从配置文件中读取配置来进行实例化,在配置相同的情况下,就没必要重复产生对象浪费内存了
#settings.py文件内容如下
HOST='1.1.1.1'
PORT=3306
#方式一:定义一个类方法实现单例模式
import settings
class Mysql:
    __instance=None
    def __init__(self,host,port):
        self.host=host
        self.port=port
    @classmethod
    def singleton(cls):
        if not cls.__instance:
            cls.__instance=cls(settings.HOST,settings.PORT)
        return cls.__instance
obj1=Mysql('1.1.1.2',3306)
obj2=Mysql('1.1.1.3',3307)
print(obj1 is obj2) #False
obj3=Mysql.singleton()
obj4=Mysql.singleton()
print(obj3 is obj4) #True
#方式二:定制元类实现单例模式
import settings
class Mymeta(type):
    def __init__(self,name,bases,dic): #定义类Mysql时就触发
        # 事先先从配置文件中取配置来造一个Mysql的实例出来
        self.__instance = object.__new__(self)  # 产生对象
        self.__init__(self.__instance, settings.HOST, settings.PORT)  # 初始化对象
        # 上述两步可以合成下面一步
        # self.__instance=super().__call__(*args,**kwargs)
        super().__init__(name,bases,dic)
    def __call__(self, *args, **kwargs): #Mysql(...)时触发
        if args or kwargs: # args或kwargs内有值
            obj=object.__new__(self)
            self.__init__(obj,*args,**kwargs)
            return obj
        return self.__instance
class Mysql(metaclass=Mymeta):
    def __init__(self,host,port):
        self.host=host
        self.port=port
obj1=Mysql() # 没有传值则默认从配置文件中读配置来实例化,所有的实例应该指向一个内存地址
obj2=Mysql()
obj3=Mysql()
print(obj1 is obj2 is obj3)
obj4=Mysql('1.1.1.4',3307)
#方式三:定义一个装饰器实现单例模式
import settings
def singleton(cls): #cls=Mysql
    _instance=cls(settings.HOST,settings.PORT)
    def wrapper(*args,**kwargs):
        if args or kwargs:
            obj=cls(*args,**kwargs)
            return obj
        return _instance
    return wrapper
@singleton # Mysql=singleton(Mysql)
class Mysql:
    def __init__(self,host,port):
        self.host=host
        self.port=port
obj1=Mysql()
obj2=Mysql()
obj3=Mysql()
print(obj1 is obj2 is obj3) #True
obj4=Mysql('1.1.1.3',3307)
obj5=Mysql('1.1.1.4',3308)
print(obj3 is obj4) #False