python面向对象

一、面向对象的思想

面向对象是一种重要的编程思想,不是一种技术,是超脱于编程语言的

面向过程:

核心是“过程”二字

过程的终极奥义是将程序流程化

过程是“流水线”,用来分步骤解决问题

优点:将程序流程化,进而程序的设计会变得简单化

缺点:可扩展性差

面向对象

核心是“对象”二字

所有的程序都是由”数据”与“功能“组成,编写程序的本质就是定义出一系列的数据,然后定义出一系列的功能来对数据进行操作。

对象是“容器”,容器就是用来盛放东西的,在以前我们编写代码,不是变量就是函数,变量就是数据,函数就是功能。所以,对象就是用来存放数据和功能的,所以我们可以称对象为数据和功能的集合体或整合体

对象的终极奥义就是将程序“整合”

优点:提升程序的解耦合程度,进而增强程序的可扩展性

缺点:设计复杂

# 第一步:
stu_name = '小王'
stu_age = 18
stu_sex = '男'

print(f'学生的姓名是{stu_name},学生的年龄是{stu_age},学生的性别是{stu_sex}')

# 修改学生的相关数据
stu_name = '小明'
stu_age = 19
stu_sex = '女'

teacher_name = '老杨'
teacher_age = 29
teacher_sex = '男'

print(f'老师的姓名是{teacher_name},老师的年龄是{teacher_age},老师的性别是{teacher_sex}')

# 修改学生的相关数据
teacher_name = '木易'
teacher_age = 30

# 代码很乱,能不能讲代码优化下,借助于函数
# 第二步
stu_name = '小王'
stu_age = 18
stu_sex = '男'

def stu_info():
    print(f'学生的姓名是{stu_name},学生的年龄是{stu_age},学生的性别是{stu_sex}')

# 修改学生的相关数据
def set_stu_info(x,y,z):
    stu_name = x
    stu_age = y
    stu_sex = z

teacher_name = '老杨'
teacher_age = 29
teacher_sex = '男'

def teacher_info():
    print(f'老师的姓名是{teacher_name},老师的年龄是{teacher_age},老师的性别是{teacher_sex}')

def set_teacher_info(x,y):
    teacher_name = x
    teacher_age = y

# 还是很乱,我们发现有的数据和功能是只和学生有关系,
# 有的数据和功能只和老师有关系,能不能将这些数据和功能整合到一起,就更加方便
# 学生的容器 = 学生的数据+学生的功能
# 老师的容器 = 老师的数据+老师的功能
# 方法1:将学生,老师的数据和功能存在一个单独的文件,但这样空间浪费
# 方法2:列表也可以存放
def stu_info():
    print(f'学生的姓名是{stu_name},学生的年龄是{stu_age},学生的性别是{stu_sex}')

# 修改学生的相关数据
def set_stu_info(x,y,z):
    stu_name = x
    stu_age = y
    stu_sex = z

stu_obj = ['小王',18,'男',stu_info,set_stu_info]

def teacher_info():
    print(f'老师的姓名是{teacher_name},老师的年龄是{teacher_age},老师的性别是{teacher_sex}')

def set_teacher_info(x,y):
    teacher_name = x
    teacher_age = y

teacher_obj = ['木易',29,'男',teacher_info,set_teacher_info]

# 少了一些代码,看起来更清晰了,但是列表无法对数据进行描述

# 第三部分,改用字典,并优化
def stu_info(stu_obj):
    print(f"学生的姓名是{stu_obj['stu_name']},学生的年龄是{stu_obj['stu_age']},学生的性别是{stu_obj['stu_sex']}")

# 修改学生的相关数据
def set_stu_info(x,y,z):
    stu_obj['stu_name'] = x
    stu_obj['stu_age'] = y
    stu_obj['stu_sex'] = z

stu_obj = {
    'stu_name':'小王',
    'stu_age':18,
    'stu_sex':'女',
    'stu_info':stu_info,
    'set_stu_info':set_stu_info
}

def teacher_info(teacher_obj):
    print(f"老师的姓名是{teacher_obj['teacher_name']}")

def set_teacher_info(teacher_obj,x,y):
    teacher_obj['teacher_name'] = x
    teacher_obj['teacher_age'] = y

teacher_obj = {
    'teacher_name': '木易',
    'teacher_age': 29,
    'teacher_sex': '男',
    'teacher_info': teacher_info,
    'set_teacher_info': set_teacher_info
}

# 问题:还是有函数的代码不能省略,但是代码清晰了很多
# 整合程度越高,代码看起来更清晰,解耦合越高,扩展和维护更方便

如果把”数据“比喻为”睫毛膏“、”眼影“、”唇彩“等化妆所需要的原材料;把”功能“比喻为眼线笔、眉笔等化妆所需要的工具,那么”对象“就是一个彩妆盒,彩妆盒可以把”原材料“与”工具“都装到一起

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

二、 类与对象

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

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

类也是“容器”,该容器用来存放同类对象共有的数据和功能

三、 面向对象编程

3.1 类的定义与实例化

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

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

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

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

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

class Student: # 类的命名应该使用“驼峰体”

    school='清华大学' # 数据

    def choose(self): # 功能
        print('%s is choosing a course' %self.name)

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

print(Student.__dict__)

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

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__查看

print(stu1.__dict__)

至此,我们造出了三个对象与一个类,对象存放各自独有的数据,类中存放对象们共有的内容

存的目的是为了用,那么如何访问对象或者类中存放的内容呢?

3.2 属性访问

3.2.1 类属性与对象属性

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

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

操作对象的属性也是一样

print(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是约定俗成的。

在上述介绍类与对象的使用过程中,我们更多的是站在底层原理的角度去介绍类与对象之间的关联关系,如果只是站在使用的角度,我们无需考虑语法“对象.属性"中”属性“到底源自于哪里,只需要知道是通过对象获取到的就可以了,所以说,对象是一个高度整合的产物,有了对象,我们只需要使用”对象.xxx“的语法就可以得到跟这个对象相关的所有数据与功能,十分方便且解耦合程度极高

四、封装

面向对象编程有三大特性:封装、继承、多态,其中最重要的一个特性就是封装。

封装指的就是把数据与功能都整合到一起,听起来是不是很熟悉,没错,我们之前所说的”整合“二字其实就是封装的通俗说法。除此之外,针对封装到对象或者类中的属性,我们还可以严格控制对它们的访问,分两步实现:隐藏与开放接口

4.1 隐藏属性

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,所以说这种操作并没有严格意义上地限制外部访问,仅仅只是一种语法意义上的变形
  2. 在类内部是可以直接访问双下滑线开头的属性的,比如self.__f1(),因为在类定义阶段类内部双下滑线开头的属性统一发生了变形
  3. 变形操作只在类定义阶段发生一次,在类定义之后的赋值操作,不会变形

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

4.2 隐藏数据属性

将数据隐藏起来就限制了类外部对数据的直接操作,然后类内应该提供相应的接口来允许类外部间接地操作数据,接口之上可以附加额外的逻辑来对数据的操作进行严格地控制

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') # 年龄不为整型,抛出异常
# TypeError: 年龄必须是整型
t.set_info('LiLi',19) # 名字为字符串类型,年龄为整形,可以正常设置
t.tell_info() # 查看老师的信息
# 姓名:LiLi,年龄:19

4.3 隐藏函数属性

目的的是为了隔离复杂度,例如ATM程序的取款功能,该功能有很多其他功能组成,比如插卡、身份认证、输入金额、打印小票、取钱等,而对使用者来说,只需要开发取款这个功能接口即可,其余功能我们都可以隐藏起来

class ATM:
    def __card(self): #插卡
        print('插卡')

    def __auth(self): #身份认证
        print('用户认证')

    def withdraw(self): #取款功能
        self.__card()
        self.__auth()
obj=ATM()
obj.withdraw()

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

4.4 property

我们一直在用“类对象.属性”的方式访问类中定义的属性,其实这种做法是欠妥的,因为它破坏了类的封装原则。正常情况下,类包含的属性应该是隐藏的,只允许通过类提供的方法来间接实现对类属性的访问和操作

因此,在不破坏类封装原则的基础上,为了能够有效操作类中的属性,类中应包含读(或写)类属性的多个 getter(或 setter)方法,这样就可以通过“类对象.方法(参数)”的方式操作属性,例如:

class CLanguage:
    # 构造函数
    def __init__(self, name):
        self.name = name

    # 设置 name 属性值的函数
    def setname(self, name):
        self.name = name

    # 访问nema属性值的函数
    def getname(self):
        return self.name

    # 删除name属性值的函数
    def delname(self):
        self.name = "xxx"


clang = CLanguage("python")
# 获取name属性值
print(clang.getname())
# 设置name属性值
clang.setname("Python教程")
print(clang.getname())
# 删除name属性值
clang.delname()
print(clang.getname())

Python 中提供了 property() 函数,可以实现在不破坏类封装原则的前提下,让开发者依旧使用“类对象.属性”的方式操作类中的属性

property() 函数的基本使用格式如下:

属性名=property(fget=None, fset=None, fdel=None, doc=None)

其中,fget 参数用于指定获取该属性值的类方法,fset 参数用于指定设置该属性值的方法,fdel 参数用于指定删除该属性值的方法,最后的 doc 是一个文档字符串,用于说明此函数的作用

注意,在使用 property() 函数时,以上 4 个参数可以仅指定第 1 个、或者前 2 个、或者前 3 个,当前也可以全部指定。也就是说,property() 函数中参数的指定并不是完全随意的

例如,修改上面的程序,为 name 属性配置 property() 函数:

class CLanguage:
    # 构造函数
    def __init__(self, n):
        self.__name = n

    # 设置 name 属性值的函数
    def setname(self, n):
        self.__name = n

    # 访问nema属性值的函数
    def getname(self):
        return self.__name

    # 删除name属性值的函数
    def delname(self):
        self.__name = "xxx"

    # 为name 属性配置 property() 函数
    name = property(getname, setname, delname, '指明出处')


# 调取说明文档的 2 种方式
# print(CLanguage.name.__doc__)
help(CLanguage.name)
clang = CLanguage("python")
# 调用 getname() 方法
print(clang.name)
# 调用 setname() 方法
clang.name = "Python教程"
print(clang.name)
# 调用 delname() 方法
del clang.name
print(clang.name)

注意,在此程序中,由于 getname() 方法中需要返回 name 属性,如果使用 self.name 的话,其本身又被调用 getname(),这将会先入无限死循环。为了避免这种情况的出现,程序中的 name 属性必须设置为私有属性,即使用 __name(前面有 2 个下划线)

当然,property() 函数也可以少传入几个参数。以上面的程序为例,我们可以修改 property() 函数如下所示

name = property(getname, setname)

这意味着,name 是一个可读写的属性,但不能删除,因为 property() 函数中并没有为 name 配置用于函数该属性的方法。也就是说,即便 CLanguage 类中设计有 delname() 函数,这种情况下也不能用来删除 name 属性

同理,还可以像如下这样使用 property() 函数:

name = property(getname)    
# name 属性可读,不可写,也不能删除
name = property(getname, setname,delname)    
#name属性可读、可写、也可删除,就是没有说明文档

4.6 @property装饰器详解

既要保护类的封装特性,又要让开发者可以使用“对象.属性”的方式操作操作类属性,除了使用 property() 函数,Python 还提供了 @property 装饰器。通过 @property 装饰器,可以直接通过方法名来访问方法,不需要在方法名后添加一对“()”小括号

@property 的语法格式如下:

@property
def 方法名(self)
    代码块

例如,定义一个矩形类,并定义用 @property 修饰的方法操作类中的 area 私有属性,代码如下:

class Rect:
    def __init__(self,area):
        self.__area = area
    @property
    def area(self):
        return self.__area
rect = Rect(30)
#直接通过方法名来访问 area 方法
print("矩形的面积是:",rect.area)

上面程序中,使用 @property 修饰了 area() 方法,这样就使得该方法变成了 area 属性的 getter 方法。需要注意的是,如果类中只包含该方法,那么 area 属性将是一个只读属性。

也就是说,在使用 Rect 类时,无法对 area 属性重新赋值,即运行如下代码会报错:

rect.area = 90
print("修改后的面积:",rect.area)

而要想实现修改 area 属性的值,还需要为 area 属性添加 setter 方法,就需要用到 setter 装饰器,它的语法格式如下:

@方法名.setter
def 方法名(self, value):
    代码块

例如,为 Rect 类中的 area 方法添加 setter 方法,代码如下:

@area.setter
def area(self, value):
    self.__area = value

rect.area = 90
print("修改后的面积:",rect.area)

这样,area 属性就有了 getter 和 setter 方法,该属性就变成了具有读写功能的属性

除此之外,还可以使用 deleter 装饰器来删除指定属性,其语法格式为:

@方法名.deleter
def 方法名(self):
    代码块

例如,在 Rect 类中,给 area() 方法添加 deleter 方法,实现代码如下:

@area.deleter
def area(self):
    self.__area = 0

del rect.area
print("删除后的area值为:",rect.area)
class People:
    def __init__(self,name,weight,height):
        self.name=name
        self.weight=weight
        self.height=height

    @property
    def bmi(self):
        print(self.weight / (self.height**2))

obj=People('lili',75,1.85)
obj.bmi 
#触发方法bmi的执行,将obj自动传给self,执行后返回值作为本次引用的结果

五、继承

5.1 继承介绍

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

class ParentClass1: #定义父类
    pass

class ParentClass2: #定义父类
    pass

class SubClass1(ParentClass1): #单继承
    pass

class SubClass2(ParentClass1,ParentClass2): #多继承
    pass

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

print(SubClass2.__bases__)

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

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

5.2 继承与抽象

要找出类与类之间的继承关系,需要先抽象,再继承。抽象即总结相似之处,总结对象之间的相似之处得到类,总结类与类之间的相似之处就可以得到父类

类与类之间的继承指的是什么’是’什么的关系(比如人类,猪类,猴类都是动物类)子类可以继承/遗传父类所有的属性,因而继承可以用来解决类与类之间的代码重用性问题

比如我们按照定义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)
print(teacher1.school,teacher1.name,teacher1.sex,teacher1.age)

5.3 属性查找

有了继承关系,对象在查找属性时,先从对象自己的__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__f1,因而只会调用自己所在的类中的方法

class Bar(Foo):
    def __f1(self):  # 变形为_Bar__f1
        print('Foo.f1')

b = Bar()
b.f2()  # 在父类中找到f2方法,进而调用b._Foo__f1()方法,同样是在父类中找到该方法
Foo.f2
Foo.f1

5.4 继承的实现原理

5.4.1 菱形问题

大多数面向对象语言都不支持多继承,而在Python中,一个子类是可以同时继承多个父类的,这固然可以带来一个子类可以对多个不同父类加以重用的好处,但也有可能引发著名的菱形问题(或称钻石问题,有时候也被称为“死亡钻石”),菱形其实就是对下面这种继承结构的形象比喻

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.4.2 继承原理

python到底是如何实现继承的呢? 对于你定义的每一个类,Python都会计算出一个方法解析顺序(MRO)列表,该MRO列表就是一个简单的所有基类的线性顺序列表,如下

print(D.mro()) 
# 新式类内置了mro方法可以查看线性列表的内容,经典类没有该内置该方法

python会在MRO列表上从左到右开始查找基类,直到找到第一个匹配这个属性的类为止。 而这个MRO列表的构造是通过一个C3线性化算法来实现的。我们不去深究这个算法的数学原理,它实际上就是合并所有父类的MRO列表并遵循如下三条准则:

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

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

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

5.4.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,分别对应属性的两种查找方式:深度优先和广度优先

当类是经典类时,多继承情况下,在要查找属性不存在时,会按照深度优先的方式查找下去

当类是新式类时,多继承情况下,再要查找属性不存在时,会按照广度优先的方式查找下去

5.4.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”关系。这个难题该怎么解决?

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语言惯用的手法),这个类是作为功能添加到子类中,而不是作为父类

使用Mixin类实现多重继承要非常小心

  • 首先它必须表示某一种功能,而不是某个物品,python 对于mixin类的命名方式一般以 Mixin, able, ible 为后缀
  • 其次它必须责任单一,如果有多个功能,那就写多个Mixin类,一个类可以继承多个Mixin,为了保证遵循继承的“is-a”原则,只能继承一个标识其归属含义的父类
  • 然后,它不依赖于子类的实现
  • 最后,子类即便没有继承这个Mixin类,也照样可以工作,就是缺少了某个功能。(比如飞机照样可以载客,就是不能飞了)

Mixins是从多个类中重用代码的好方法,但是需要付出相应的代价,我们定义的Minx类越多,子类的代码可读性就会越差,并且更恶心的是,在继承的层级变多时,代码阅读者在定位某一个方法到底在何处调用时会晕头转向

5.5.5 派生与方法重用

子类可以派生出自己新的属性,在进行属性查找时,子类中的属性名会优先于父类被查找,例如每个老师还有职称这一属性,我们就需要在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__,并不会自动调用父类的
print(obj.name, obj.sex, obj.age, obj.title)

很明显子类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继续往后查找

关于在子类中重用父类功能的这两种方式,使用任何一种都可以,但是在最新的代码中还是推荐使用super()

5.5.6 组合

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

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类中的内容于一身(都可以访问到),是一个高度整合的产物

六、多态

6.1 多态与多态性

多态指的是一类事物有多种形态,比如动物有多种形态:猫、狗、猪

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

七、绑定方法与非绑定方法

7.1 绑定方法与非绑定方法

类中定义的函数分为两大类:绑定方法和非绑定方法

其中绑定方法又分为绑定到对象的对象方法和绑定到类的类方法。

在类中正常定义的函数默认是绑定到对象的,而为某个函数加上装饰器@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中,绑定到类的方法与绑定到对象的方法是严格区分开的

7.2 非绑定方法

为类中某个函数加上装饰器@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>

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

八、反射,内置方法

8.1 反射

python是动态语言,而反射(reflection)机制被视为动态语言的关键。

反射机制指的是在程序的运行状态中

对于任意一个类,都可以知道这个类的所有属性和方法;

对于任意一个对象,都能够调用他的任意方法和属性。

这种动态获取程序信息以及动态调用对象的功能称为反射机制。

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

class People:
	def __init__(self,name,age,gender):
		self.name=name
		self.age=age
		self.gender=gender

obj=People('木易',18,'male')
print(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...

8.2 内置方法

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

__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__()

九、 type()函数:动态创建类

我们知道,type() 函数属于python内置函数,通常用来查看某个变量的具体类型。其实,type() 函数还有一个更高级的用法,即创建一个自定义类型(也就是创建一个类)

type() 函数的语法格式有 2 种,分别如下:

type(obj) 
type(name, bases, dict)

以上这 2 种语法格式,各参数的含义及功能分别是:

  • 第一种语法格式用来查看某个变量(类对象)的具体类型,obj 表示某个变量或者类对象。
  • 第二种语法格式用来创建类,其中 name 表示类的名称;bases 表示一个元组,其中存储的是该类的父类;dict 表示一个字典,用于表示类内定义的属性或者方法。

对于使用 type() 函数查看某个变量或类对象的类型,由于很简单,这里不再做过多解释,直接给出一个样例:

#查看 3.4 的类型
print(type(3.4))
#查看类对象的类型
class CLanguage:
    pass
clangs = CLanguage()
print(type(clangs))
# <class 'float'>
# <class '__main__.CLanguage'>

这里重点介绍 type() 函数的另一种用法,即创建一个新类,先来分析一个样例:

#定义一个实例方法
def say(self):
    print("我要学 Python!")
#使用 type() 函数创建类
CLanguage = type("CLanguage",(object,),dict(say = say, name = "Python"))
#创建一个 CLanguage 实例对象
clangs = CLanguage()
#调用 say() 方法和 name 属性
clangs.say()
print(clangs.name)

注意,Python 元组语法规定,当 (object,) 元组中只有一个元素时,最后的逗号(,)不能省略

可以看到,此程序中通过 type() 创建了类,其类名为 CLanguage,继承自 objects 类,且该类中还包含一个 say() 方法和一个 name 属性。

如何判断 dict 字典中添加的是方法还是属性?很简单,如果该键值对中,值为普通变量(如 “Python”),则表示为类添加了一个类属性;反之,如果值为外部定义的函数(如 say() ),则表示为类添加了一个实例方法

可以看到,使用 type() 函数创建的类,和直接使用 class 定义的类并无差别。事实上,我们在使用 class 定义类时,Python 解释器底层依然是用 type() 来创建这个类。

十、MetaClass元类详解

MetaClass元类,本质也是一个类,但和普通类的用法不同,它可以对类内部的定义(包括类属性和类方法)进行动态的修改。可以这么说,使用元类的主要目的就是为了实现在创建类时,能够动态地改变类中定义的属性或者方法

举个例子,根据实际场景的需要,我们要为多个类添加一个 name 属性和一个 say() 方法。显然有多种方法可以实现,但其中一种方法就是使用 MetaClass 元类。

如果在创建类时,想用 MetaClass 元类动态地修改内部的属性或者方法,则类的创建过程将变得复杂:先创建 MetaClass 元类,然后用元类去创建类,最后使用该类的实例化对象实现功能

如果想把一个类设计成 MetaClass 元类,其必须符合以下条件:

  1. 必须显式继承自 type 类
  2. 类中需要定义并实现 __new__() 方法,该方法一定要返回该类的一个实例对象,因为在使用元类创建类时,该 __new__() 方法会自动被执行,用来修改新建的类
#定义一个元类
class FirstMetaClass(type):
    # cls代表动态修改的类
    # name代表动态修改的类名
    # bases代表被动态修改的类的所有父类
    # attr代表被动态修改的类的所有属性、方法组成的字典
    def __new__(cls, name, bases, attrs):
        # 动态为该类添加一个name属性
        attrs['name'] = "python"
        attrs['say'] = lambda self: print("调用 say() 实例方法")
        return super().__new__(cls,name,bases,attrs)

可以看到,在这个元类的 __new__()方法中,手动添加了一个 name 属性和 say() 方法。这意味着,通过 FirstMetaClass 元类创建的类,会额外添加 name 属性和 say() 方法

#定义类时,指定元类
class CLanguage(object,metaclass=FirstMetaClass):
    pass
clangs = CLanguage()
print(clangs.name)
clangs.say()

可以看到,在创建类时,通过在标注父类的同时指定元类(格式为metaclass=元类名),则当python解释器在创建这该类时,FirstMetaClass 元类中__new__方法就会被调用,从而实现动态修改类属性或者类方法的目的

显然,FirstMetaClass 元类的 __new__() 方法动态地为 Clanguage 类添加了 name 属性和 say() 方法,因此,即便该类在定义时是空类,它也依然有 name 属性和 say() 方法

对于 MetaClass 元类,它多用于创建 API,因此我们几乎不会使用到它

class Base(type):
    def __init__(self,class_name,class_bases,class_dic):
        super(Base, self).__init__(class_name, class_bases, class_dic)  # 重用父类的功能
        print('我是一个元类')
        self.name = '我是一个元类'

class Son(object,metaclass=Base):
    def __init__(self):
        print('我是木易')

s = Son()
print(s.name)

10.1 MetaClass元类实现的底层原理

要理解 MetaClass 的底层原理,首先要深入理解 Python 类型模型

1) 所有的 Python 的用户定义类,都是 type 这个类的实例

事实上,类本身不过是一个名为 type 类的实例,可以通过如下代码进行验证

class MyClass:
  pass
instance = MyClass()
print(type(instance))
print(type(MyClass))
# <class '__main__.MyClass'>
# <class 'type'>

可以看到,instance 是 MyClass 的实例,而 MyClass 是 type 的实例

2) 用户自定义类,只不过是 type 类的 __call__运算符重载

当定义完成一个类时,真正发生的情况是 Python 会调用 type 类的 __call__ 运算符

简单来说,当定义一个类时,例如下面语句:

class MyClass:
  data = 1

Python 底层执行的是下面这段代码:

class = type(classname, superclasses, attributedict)

其中等号右边的 type(classname, superclasses, attributedict) 就是 type 的 __call__ 运算符重载,它会进一步调用下面这 2 个函数:

type.__new__(typeclass, classname, superclasses, attributedict)
type.__init__(class, classname, superclasses, attributedict)

以上整个过程,可以通过如下代码进行论证:

class MyClass:
  data = 1
instance = MyClass()
print(MyClass,instance)
print(instance.data)
MyClass = type('MyClass', (), {'data': 1})
instance = MyClass()
print(MyClass,instance)
print(instance.data)
# <class '__main__.MyClass'> <__main__.MyClass object at 0x000001CB469F7400>
# 1
# <class '__main__.MyClass'> <__main__.MyClass object at 0x000001CB46A50828>
# 1

由此可见,正常的 MyClass 定义,和手工调用 type 运算符的结果是完全一样的

总之,正是 Python 的类创建机制,给了 metaclass 大展身手的机会,即一旦把一个类型 MyClass 设置成元类 MyMeta,那么它就不再由原生的 type 创建,而是会调用 MyMeta 的 __call__ 运算符重载

class = type(classname, superclasses, attributedict)
# 变为了
class = MyMeta(classname, superclasses, attributedict)

使用 metaclass 的风险

正如上面所看到的那样,metaclass 这样“逆天”的存在,会"扭曲变形"正常的 Python 类型模型,所以,如果使用不慎,对于整个代码库造成的风险是不可估量的。

换句话说,metaclass 仅仅是给小部分 Python 开发者,在开发框架层面的 Python 库时使用的。而在应用层,metaclass 往往不是很好的选择

  • 9
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值