Python之面向对象编程


面向对象编程介绍

软件开发设计常用的两种设计方法:面向过程编程和面向对象编程。

  • 面向过程编程:
    • 根据业务逻辑从上往下,将需要用到的功能代码封装到函数中,着重关注开发的步骤和过程;(关注过程,典型代表语言C)
    • 面向过程核心是“过程”,就是将程序流程化;
    • 过程可以理解为流水线,分步骤解决问题;
  • 面向对象编程:
    • 将函数或所需要的功能进行分类和封装,着重关注谁帮我做这件事;(关注结果,典型代表语言Java、C++等)
    • 面向对象核心是“对象”,就是将程序进行封装;
    • 对象可以理解为是容器,用来盛放数据和功能;

面向对象编程有2个非常重要的概念:对象

面向对象编程有3个重要的特性:封装继承多态

面向对象

对象和类

对象:符合类描述的具体实例,或者说能存放数据和功能的容器;数据即属性,功能即方法;

类:同一类事物的抽象描述(属性和功能),或者说能存放同类对象共有的数据和功能的容器;

例子:

写程序,程序是什么?数据+功能,各式各样的数据,通过各种功能来操作、保存数据。

一个学校的信息系统:

a.学生student

  • 学生的数据

    • stu_name
    • stu_age
    • stu_gender
  • 学生的功能

    • show_info
    • set_info
    • study

b.课程course

  • 课程的数据
    • cou_name
    • cou_cycle
  • 课程的功能
    • show_info

在学习函数知识之前,实现如上功能的代码如下:

# 学生的数据
stu_name = 'laobai'
stu_age = 18
stu_gender = 'male'

# 学生的功能
# 显示学生信息
print('学生姓名:%s 年龄:%s 性别:%s' %(stu_name,stu_age,stu_gender))

# 修改学生数据
stu_name = 'nichengwe'
stu_age = 19
stu_gender = 'female'

print('学生姓名:%s 年龄:%s 性别:%s' %(stu_name,stu_age,stu_gender))

# 课程的数据(course)
cou_name = 'python'
cou_cycle = '6 months'
cou_score = 10

# 课程的功能
# 显示课程的信息
print('课程名:%s 周期:%s 学分:%s' %(cou_name,cou_cycle,cou_score))

执行结果:

学生姓名:laobai 年龄:18 性别:male
学生姓名:nichengwe 年龄:19 性别:female
课程名:python 周期:6 months 学分:10

通过函数的知识实现如下:

# 学生的数据
stu_name = 'laobai'
stu_age = 18
stu_gender = 'male'

# 学生的功能
# 显示学生信息(student)
def show_stu_info():
    print('学生姓名:%s 年龄:%s 性别:%s' %(stu_name,stu_age,stu_gender))

# 修改学生数据
def set_stu_info():
    stu_name = 'nichengwe'
    stu_age = 19
    stu_gender = 'female'

# 课程的数据(course)
cou_name = 'python'
cou_cycle = '6 months'
cou_score = 10

# 课程的功能
# 显示课程的信息
def show_cou_info():
    print('课程名:%s 周期:%s 学分:%s' %(cou_name,cou_cycle,cou_score))

分析:

  1. 使用函数之前的知识实现,代码都写在一个文件里,堆叠在一起,不仅乱也不方便后期的维护及调用

  2. 使用函数来实现,能够提升一些维护和调用,但还是维护起来比较繁琐

  3. 根据上面提到的,程序即数据+功能,我们可以创建一个容器来盛放学生的数据和学生的功能,再创建一个容器盛放课程的数据和课程的功能。即

    • 学生的容器 = 学生的数据 + 学生的功能
    • 课程的容器 = 课程的数据 + 课程的功能

    这样,除了代码功能区分清晰,并且暴露给调用者的只有学生的容器和课程的容器。(模块、列表、字典、集合,均可以是容器)

将上面的例子,用字典进行改写,如下:

# 学生的功能
# 显示学生信息(student)
def show_stu_info():
    print('学生姓名:%s 年龄:%s 性别:%s' %(stu_name,stu_age,stu_gender))

# 修改学生数据
def set_stu_info(x,y,z):
    stu_name = x
    stu_age = y
    stu_gender = z

# 用字典做为容器,盛放学生的数据和功能
stu_obj = {
    'stu_name':'laobai',
    'stu_age':18,
    'stu_gender':'male',
    'show_stu_info':show_stu_info,
    'set_stu_info':set_stu_info
}

上面的程序中,函数show_stu_info和set_stu_info均缺少参数(使字典中的学生数据和函数中的学生数据关联起来)。这里将字典做为一个参数进行传入

# 学生的功能
# 显示学生信息(student)
def show_stu_info(stu_obj):
    print('学生姓名:%s 年龄:%s 性别:%s' %(
        stu_obj['stu_name'],
        stu_obj['stu_age'],
        stu_obj['stu_gender']))

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

# 用字典做为容器,盛放学生的数据和功能
stu_obj = {
    'stu_name':'laobai',
    'stu_age':18,
    'stu_gender':'male',
    'show_stu_info':show_stu_info,
    'set_stu_info':set_stu_info
}

在上面的基础上,增加一个学生属性,stu_school:pls

# 学生的功能
# 显示学生信息(student)
def show_stu_info(stu_obj):
    print('学生姓名:%s 年龄:%s 性别:%s' %(
        stu_obj['stu_name'],
        stu_obj['stu_age'],
        stu_obj['stu_gender']))

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

# 用字典做为容器,盛放学生的数据和功能
stu_obj = {
    'stu_school':'pls',
    'stu_name':'laobai',
    'stu_age':18,
    'stu_gender':'male',
    'show_stu_info':show_stu_info,
    'set_stu_info':set_stu_info
}

上面的程序中,stu_obj是一个对象,表示一名学生,会有很多个stu_obj,这样既繁琐又会造成存储空间的浪费。

# 学生的功能
# 显示学生信息(student)
def show_stu_info(stu_obj):
    print('学生姓名:%s 年龄:%s 性别:%s' %(
        stu_obj['stu_name'],
        stu_obj['stu_age'],
        stu_obj['stu_gender']))

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

# 用字典做为容器,盛放学生的数据和功能
stu_obj = {
    'stu_school':'pls',
    'stu_name':'laobai',
    'stu_age':18,
    'stu_gender':'male',
    'show_stu_info':show_stu_info,
    'set_stu_info':set_stu_info
}

stu_obj1 = {
    'stu_school':'pls',
    'stu_name':'nichegnwe',
    'stu_age':19,
    'stu_gender':'female',
    'show_stu_info':show_stu_info,
    'set_stu_info':set_stu_info
}

stu_obj2 = {
    'stu_school':'pls',
    'stu_name':'baby2016的天空',
    'stu_age':20,
    'stu_gender':'male',
    'show_stu_info':show_stu_info,
    'set_stu_info':set_stu_info
}

上面的代码中,stu_obj,stu_obj1,stu_obj2等,拥有相同的属性和方法,只是部分的值不一样。这里要引入类,因为上面讲到类是存放同类对象共有的数据和功能的容器,而我们需要一个容器去存放学生的数据和学生的功能。

类的定义与实例化

定义类

定义类的语法:

class 类名:
  代码
  ...

注意:类名要满足标识符命名规则,同时遵循大驼峰命名习惯。

实例化的语法(对象又称为实例,创建对象即为实例化):

对象名 = 类名()

将上面的学生信息处理程序用类进行改写:

class Student:

    # 1、变量的定义
    # 学生的通用数据
    stu_school='pls'

    # 2、功能的定义
    # 学生的功能
    # 显示学生信息(student)
    def show_stu_info(stu_obj):
        print('学生姓名:%s 年龄:%s 性别:%s' %(
            stu_obj['stu_name'],
            stu_obj['stu_age'],
            stu_obj['stu_gender']))

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

每个学生的姓名、年龄和性别都是不一样的,这三个变量不定义在类中,通过方法的参数进行传值。

这个学生类中每个方法都有个参数stu_obj,是在上一段程序中提供的调用时的参数,在python的类中,这个参数有个固定的写法selfself表示对象本身。

class Student:

    # 1、变量的定义
    # 学生的通用数据
    stu_school='pls'

    # 2、功能的定义
    # 学生的功能
    # 显示学生信息(student)
    def show_stu_info(self):
        print('学生姓名:%s 年龄:%s 性别:%s' %(
            self['stu_name'],
            self['stu_age'],
            self['stu_gender']))

    # 修改学生数据
    def set_stu_info(self,x,y,z):
        self['stu_name'] = x
        self['stu_age'] = y
        self['stu_gender'] = z

a. 可以同类的__dict__查看对应类的名称空间

print(Student.__dict__)


{'__module__': '__main__', 'stu_school': 'pls', 'show_stu_info': <function Student.show_stu_info at 0x10d52aa60>, 'set_stu_info': <function Student.set_stu_info at 0x10d554940>, '__dict__': <attribute '__dict__' of 'Student' objects>, '__weakref__': <attribute '__weakref__' of 'Student' objects>, '__doc__': None}

b. 访问类的属性值

print(Student.stu_school) # 相当于Student.__dict__['stu_school']


pls

c. 访问类的方法

print(Student.show_stu_info) # 相当于Student.__dict__['show_stu_info']


<function Student.show_stu_info at 0x107451a60>

实例化

调用类的过程称为实例化。这一过程会发生3件事:

  1. 先产生一个空对象
# 通过类名加括号生成一个对象
stu_obj1 = Student()
# 通过对象的__dict__查看对象的名称空间
print(stu_obj1.__dict__)


{}

为空对象赋值

# 通过访问类属性的方式,访问到对象属性为其赋值
stu_obj1.stu_name = 'laobai'
stu_obj1.stu_age = 18
stu_obj1.stu_gender = 'male'

print(stu_obj1.__dict__)


{'stu_name': 'laobai', 'stu_age': 18, 'stu_gender': 'male'}

上面是创建了类的1个实例对象,若创建多个,代码就会大量重复,将上面的内容写成函数:

def base_info(x,y,z):
    stu_obj1.stu_name = x
    stu_obj1.stu_age = y
    stu_obj1.stu_gender = z

但这个函数中stu_obj1被写死了,需要再传一个参数,对哪个对象进行赋值

def base_info(obj,x,y,z):
    obj.stu_name = x
    obj.stu_age = y
    obj.stu_gender = z

base_info(stu_obj1,'laobai',18,'male')

由于新建的base_info的函数与Student类并没有整合,这里需要将base_info函数定义在Student类中

class Student:

    def base_info(self, x, y, z):
        self.stu_name = x
        self.stu_age = y
        self.stu_gender = z   
    # 1、变量的定义
    # 学生的通用数据
    stu_school='pls'

    # 2、功能的定义
    # 学生的功能
    # 显示学生信息(student)
    def show_stu_info(self):
        print('学生姓名:%s 年龄:%s 性别:%s' %(
            self['stu_name'],
            self['stu_age'],
            self['stu_gender']))

    # 修改学生数据
    def set_stu_info(self,x,y,z):
        self['stu_name'] = x
        self['stu_age'] = y
        self['stu_gender'] = z

这样写,调用类实例对象时并不会自动赋值,而是需要手动调用base_info方法才行。

  1. python会自动调用类中的__init__方法,然后将空对象以及调用类时括号内传入的参数一同传给__init__方法
class Student:

    def __init__(self, x, y, z):
        self.stu_name = x
        self.stu_age = y
        self.stu_gender = z
    # 1、变量的定义
    # 学生的通用数据
    stu_school='pls'

    # 2、功能的定义
    # 学生的功能
    # 显示学生信息(student)
    def show_stu_info(self):
        print('学生姓名:%s 年龄:%s 性别:%s' %(
            self['stu_name'],
            self['stu_age'],
            self['stu_gender']))

    # 修改学生数据
    def set_stu_info(self,x,y,z):
        self['stu_name'] = x
        self['stu_age'] = y
        self['stu_gender'] = z

注:上面程序中的类变量stu_school可以写在__init__方法中

实例化一个对象

stu_obj1 = Student()

执行程序

TypeError: __init__() missing 3 required positional arguments: 'x', 'y', and 'z'

__init__方法需要传4个参数,但上面的提示是缺少3个参数'x','y','z'。这里是python语言自己为我们传了第一参数,传值是生成对象的本身,即上面代码中stu_obj1=Student()中的stu_obj1。所以我们只需要传'x','y','z' 3个参数即可。

stu_obj1 = Student('laobai',18,'male')
  1. 返回初始化后的对象
stu_obj1 = Student('laobai',18,'male')
print(stu_obj1.__dict__)


{'stu_name': 'laobai', 'stu_age': 18, 'stu_gender': 'male'}

返回初始化好的对象stu_obj1

__init__方法总结:

  • 会在调用类时自动触发执行,用来为对象初始化自己独有的数据
  • __init__内应该存放的是为对象初始化属性的功能,同时也是可以存放其他任意代码的,想要在类调用时就立刻执行的代码都可以放到该方法内
  • __init__方法必须返回None

Student类开始时是用字典来存放数据和功能的,现在通过__init__初始化了数据,可以将类中的函数都改写成类的方式

class Student:

    def __init__(self, x, y, z):
        self.stu_name = x
        self.stu_age = y
        self.stu_gender = z
    # 1、变量的定义
    # 学生的通用数据
    stu_school='pls'

    # 2、功能的定义
    # 学生的功能
    # 显示学生信息(student)
    def show_stu_info(self):
        print('学生姓名:%s 年龄:%s 性别:%s' %(
            self.stu_name,
            self.stu_age,
            self.stu_gender))

    # 修改学生数据
    def set_stu_info(self,x,y,z):
        self.stu_name = x
        self.stu_age = y
        self.stu_gender = z

属性的访问与操作

类中存放的是对象共有的数据与功能

  1. 类可以访问

    a. 类的数据属性

    print(Student.stu_school)

    执行结果:

    pls

    b. 类的函数属性

    print(Student.show_stu_info)
    print(Student.set_stu_info)

    执行结果:

    <function Student.show_stu_info at 0x107f64940>
    <function Student.set_stu_info at 0x107f649d0>
  2. 类的实例对象也可以访问。其实类中的东西是给对象用的

    a. 类的数据属性是共享给所有对象用的,大家访问的地址都是一样的

    stu_obj1 = Student('laobai',18,'male')
    stu_obj2 = Student('nichengwe',19,'female')
    stu_obj3 = Student('baby2016的天空',20,'male')
    
    print(id(Student.stu_school))
    
    print(id(stu_obj1.stu_school))
    print(id(stu_obj2.stu_school))
    print(id(stu_obj3.stu_school))
    
    
    4531942704
    4531942704
    4531942704
    4531942704

    b. 类的函数属性是绑定给对象用的,谁来调用绑定方法就会将谁当做第一个参数自动传入(即类中定义的函数都会将第一个参数设置参数self)

  • ​ α. 用类调用类的函数

    class Student:
    
        def __init__(self, x, y, z):
            self.stu_name = x
            self.stu_age = y
            self.stu_gender = z
        # 1、变量的定义
        # 学生的通用数据
        stu_school='pls'
    
        # 2、功能的定义
        # 学生的功能
        # 显示学生信息(student)
        def show_stu_info(self):
            print('学生姓名:%s 年龄:%s 性别:%s' %(
                self.stu_name,
                self.stu_age,
                self.stu_gender))
    
        # 修改学生数据
        def set_stu_info(self,x,y,z):
            self.stu_name = x
            self.stu_age = y
            self.stu_gender = z
            
    # 通过类名加括号生成一个对象
    stu_obj1 = Student('laobai',18,'male')
    stu_obj2 = Student('nichengwe',19,'female')
    stu_obj3 = Student('baby2016的天空',20,'male')
    
    Student.show_stu_info()
    
    
    TypeError: show_stu_info() missing 1 required positional argument: 'stu_obj'

    提示缺少一个参数,stu_obj表示对象

    Student.show_stu_info(stu_obj1)
    Student.set_stu_info(stu_obj1,'pls',21,'male')
    Student.show_stu_info(stu_obj1)
    Student.show_stu_info(stu_obj2)
    Student.show_stu_info(stu_obj3)
    
    
    学生姓名:laobai 年龄:18 性别:male
    学生姓名:pls 年龄:21 性别:male
    学生姓名:nichengwe 年龄:19 性别:female
    学生姓名:baby2016的天空 年龄:20 性别:male

    打印出类调用方法和对象调用方法的内存地址

    print(Student.show_stu_info)
    print(stu_obj1.show_stu_info)
    print(stu_obj2.show_stu_info)
    print(stu_obj3.show_stu_info)

    输出结果:

    <function Student.show_stu_info at 0x102b87940>
    <bound method Student.show_stu_info of <__main__.Student object at 0x102b8bd60>>
    <bound method Student.show_stu_info of <__main__.Student object at 0x102b8bd00>>
    <bound method Student.show_stu_info of <__main__.Student object at 0x102b8bc70>>

    可以根据输出结果看出,对象调用方法是bound method绑定方法,绑定的是类Student

  • ​ β. 用对象调用类的函数

    stu_obj1.show_stu_info()
    stu_obj1.set_stu_info('pls', 11, 'female')
    stu_obj1.show_stu_info()
    stu_obj2.show_stu_info()
    stu_obj3.show_stu_info()
    
    
    学生姓名:laobai 年龄:18 性别:male
    学生姓名:pls 年龄:11 性别:female
    学生姓名:nichengwe 年龄:19 性别:female
    学生姓名:baby2016的天空 年龄:20 性别:male

    用对象调用类方法时,会将调用的对象本身作为第一个参数传给函数,就是类方法中的第一个参数self

类的封装

封装是面向对象三大特性最核心的一个特性。

封装 (encapsulation)的定义:隐藏对象的属性和实现细节,仅对外公开接口,控制在程序中属性的读取和修改的访问级别。

前面的章节中我们一直在使用类对象.属性的方式访问类中定义的属性,这种方法破坏了类的封装原则。正常情况下,类包含的属性应该是对外隐藏的,只允许通过类提供的方法间接实现对类属性的访问和操作。类中应该针对属性设计操作属性的gettersetter方法,这样就可以通过类对象.方法(参数)的方式操作属性。

隐藏属性

将类的属性进行隐藏

class Func:
    x = 1

    def f1(self):
        print('from f1()')

Func.x
Func.f1

通过在属性名和函数名前加__前缀,就会实现隐藏属性和函数的效果

class Func:
    __x = 1

    def __f1(self):
        print('from f1()')

Func.x
Func.f1

执行结果:

AttributeError: type object 'Func' has no attribute 'x'
class Func:
    __x = 1

    def __f1(self):
        print('from f1()')

Func.__x
Func.__f1

执行结果:

AttributeError: type object 'Func' has no attribute '__x'

通过查看类Func的命名空间,查看属性和函数

class Func:
    __x = 1

    def __f1(self):
        print('from f1()')

print(Func.__dict__)

执行结果:

{'__module__': '__main__', '_Func__x': 1, '_Func__f1': <function Func.__f1 at 0x10285bb00>, '__dict__': <attribute '__dict__' of 'Func' objects>, '__weakref__': <attribute '__weakref__' of 'Func' objects>, '__doc__': None}

查看Func命名空间中,x和f1是以_Func__x_Func__f1的形式存在。即_类名__属性名_类名__函数名,可以通过如下方式访问到

class Func:
    __x = 1

    def __f1(self):
        print('from f1()')

Func._Func__x
Func._Func__f1

所以这种隐藏操作并没有严格意义上的限制外部访问,仅仅只是一种语法意义上的变形。

这种隐藏对类内部的操作,可以使用__属性名__函数名的形式被访问到

class Func:
    __x = 1

    def __f1(self):
        print('from f1()')

    def f2(self):
        print(self.__x)
        print(self.__f1)

obj = Func()
obj.f2()

执行结果:

1
<bound method Func.__f1 of <__main__.Func object at 0x102dfcb10>>

即这种隐藏对外不对内,且只在检查类体语法时发生一次。

隐藏属性,不是不让外部调用,而是不让外部直接调用,需要通过类中实现好的接口来调用。

开放接口

通过在类中实现对属性调用的接口对外开放,来达到让外部调用属性。

将数据隐藏起来就限制了类外部对数据的直接操作,然后类内应该提供相应的接口来允许类外部间接地操作数据,接口之上可以附件额外的逻辑来对数据的操作进行严格地控制。而隐藏函数属性是为了隔离复杂度,类内部使用或调用者不会用到的功能不必暴露给调用者

class People:

    def __init__(self, name):
        self.__name = name

    def get_name(self):
        print(self.__name)

    def set_name(self, name):
        self.__name = name

obj = People('laobai')

obj.get_name()
obj.set_name('nichengwe')
obj.get_name()

执行结果:

laobai
nichengwe

property

property是一个装饰器。(装饰器是在不修改被装饰对象源代码以及调用方式的前提下为被装饰对象添加新功能的可调用对象)

下面以计算成年人BMI指数为例进行说明

成年人的BMI数值:

公式:体质指数(BMI) = 体重(kg) ÷ 身高^2^ (m)

偏瘦:低于18.4
正常:18.5 ~ 23.9
过重:24.0 ~ 27.9
肥胖:高于28.0

class People:

    def __init__(self, name, weight, height):
        self.name = name
        self.weight = weight
        self.height = height

    def bmi(self):
        return self.weight / (self.height ** 2)


obj1 = People('laobai', 65, 1.70)
print(obj1.bmi())


22.49134948096886

上面的程序已经实现了获取BMI值的功能,但BMI更像是人的一个数据属性值,而非功能。接下来使用装饰器property将函数bmi伪装成数据属性bmi

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)

obj1 = People('laobai', 65, 1.70)
print(obj1.bmi)


22.49134948096886

可以看到上面被property修饰的函数bmi,被调用时是以属性的形式进行调用的obj1.bmi。若再用调用函数的方式调用会出错obj1.bmi()

obj1 = People('laobai', 65, 1.70)
print(obj1.bmi())


TypeError: 'float' object is not callable

下面的是之前隐藏属性的例子:

class People:

    def __init__(self, name):
        self.__name = name

    def get_name(self):
        return self.__name

    def set_name(self, name):
        self.__name = name

obj = People('laobai')

obj.get_name()
obj.set_name('nichengwe')
obj.get_name()

添加property()函数进行伪装

property()作为函数时的语法:
属性名=property(fget=None, fset=None, fdel=None, doc=None)

属性名,即要隐藏或伪装的属性名,上面的例子为name
property的四个参数,对应的是get_name,set_name,del_name,及文档字符串。而这四个参数,可仅指定第1个,或仅指定前2个,或仅指定前3个,即可。

name = property(get_name) -> name属性只可以读,不可以改,也不能删除
name = property(get_name, set_name,del_name) -> name属性可以读,可以改,也可以删除

class People:

    def __init__(self, name):
        self.__name = name

    def get_name(self):
        return self.__name

    def set_name(self, name):
        self.__name = name

    name = property(get_name, set_name)

obj = People('laobai')

print(obj.name)
obj.name = 'nichengwe'
print(obj.name)

执行结果:

laobai
nichengwe

添加property装饰器进行伪装

class People:

    def __init__(self, name):
        self.__name = name

    # 利用property装饰器将获取name方法转换为获取对象的属性
    @property
    def name(self):
        return self.__name

    # 利用property装饰器将设置name方法转换为为属性赋值
    @name.setter
    def name(self, name):
        self.__name = name

obj = People('laobai')

print(obj.name)
obj.name = 'nichengwe'
print(obj.name)

执行结果:

laobai
nichengwe

上面的程序,将操作属性的方法名都改成外部调用时要操作的名称,这里是name,有几个操作方法就改几个位置;然后为读取属性的方法增加@property装饰器,其他操作方法增加对应的属性名.方法name.setter,name.deleter等;类外部调用类中的属性时,即可以使用类对象.属性名的方式调用隐藏后的属性了。

类的继承

继承介绍

继承是一种创建新类的方式,新建的类可称为子类或派生类;被继承的类,称为父类,父类又可以称为基类或超类。Python语言支持多继承。子类可以使用父类的属性。
类是解决对象间的代码冗余问题;继承是解决类与类间代码冗余问题。

示例:

class Parent1:
    pass

class Parent2:
    pass

# 继承,单继承,只有一个父类
class func1(Parent1):
    pass

# 继承,多继承,有多于1个父类
class func2(Parent1,Parent2):
    pass
# python 新建的类都会默认继承object类;python自带的很多类都继承于object类;
# 由于是默认继承,所以object可以省略,即下面两种写法的含义是一样的。
class Parent1(object):
    pass

class Parent1:
    pass

继承与抽象

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

基于抽象的结果,我们就找到了继承关系,如下图:

基于上图可以看出,类与类之间的继承制的是:“什么‘是’什么”的关系(学生类、老师类、工人类都是人类)。子类可以继承父类所有的属性,因而继承可以用来解决类与类之间的代码重用性问题。

下面以学生类和老师类为例:

class Student:
    school = '漂亮学院'

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

    def study(self):
        print('%s正在学习!' % self.name)


class Teacher:
    school = '漂亮学院'

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

    def Teach(self):
        print('%s正在讲课!' % self.name)

查看上面的代码可以发现,学生类和老师类中有部分代码是重复的,可以将重复的代码抽象出来新写一个类

class People:
    school = '漂亮学院'

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

上面的代码是学生类和老师类所共有的。将学生类继承人类

class Student(People):

    def study(self):
        print('%s正在学习!' % self.name)


obj1 = Student('小光', 18, 'male')
print(obj1.school)
print(obj1.name)
obj1.study()


漂亮学院
小光
小光正在学习!

Student类继承People类,将Student类中与父类重复的代码删除。可以看到学生类已经精简了很多,并且调用原来的schoolname等属性及study方法均能正确执行。

接下来将老师类继承人类,其中schoolnameagegender4个属性父类People中都有,可以删除掉,但Teacher类中的__init__初始化方法有个独有的salary参数需要保留,这样Teacher类中的__init__方法要调用父类People__init__方法补全Teacher类的初始化方法

class Teacher(People):

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

    def teach(self):
        print('%s正在讲课!' % self.name)


obj2 = Teacher('张老师', 30, 'female', 3000)
print(obj2.school)
print(obj2.name)
print(obj2.salary)
obj2.teach()


漂亮学院
张老师
3000
张老师正在讲课!

单继承属性查找

单继承的属性查找,先从对象内部找,再到对象所在的类中找,再到所在类的父类中找,再向上找父类。

class Func:
    def f1(self):
        print('Func.f1')

    def f2(self):
        print('Func.f2')
        self.f1()


class Sub(Func):
    def f1(self):
        print('Sub.f1')


obj = Sub()
obj.f2()


Func.f2
Sub.f1

注意上面的程序中,self.f1()等价于obj.f1()objSub类的实例,所以要先在Sub类中找

菱形继承

(这个差异是因为python2和python3两个版本的差异,但python2已经停止维护多年,所以这个问题不再是问题,仅作为了解。)

MRO

  • class D继承了class B和Class C,但class D没有没有重写父类中的方法
  • class B继承了class A,class B重写了父类方法
  • class C继承了class A,class C重写了父类方法
  • class A不是object
class A:
    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()

上面代码中,结果是什么?

python中提供了mro方法,获取当前执行类的对象访问属性的查找顺序列表

class A:
    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()
print(D.mro())


from B
[<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>]

class D实例的mro列表为<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>,即先从D自身查找,然后是B->C->A这样的顺序。

python会在MRO列表中从左到右开始查找基类,直到找到第一个匹配这个属性的类为止。实际上就是合并所有父类的MRO列表并遵循以下三个准则:

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

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

深度优先与广度优先

深度优先遍历:对每一个可能的分支路径深入到不能再深入为止,而且每个节点只能访问一次;

广度优先遍历:又叫层次遍历,从下往上对每一层依次访问,在每一层中,从左往右访问节点,访问完一层就进入到上一层,直到没有节点可以访问为止;

深度优先适用于python2的经典类,广度优先适用于python2的新式类和python3的所有类(python3不区分新式类和经典类,默认都继承object类)。python2已停止维护多年。所以一般情况下,可以理解为Python默认的多继承查找属性的顺序是广度优先。

Mixins机制

mix-in原意是一种冰淇淋,提供一些基础口味(草莓、巧克力等),在这种基础口味上可以添加其他食品(坚果、小饼干等)。Mixin术语由此而来。

Mixins是一种语言概念,允许程序员讲一些代码注入到一个类中。Mixin在编程常被译为“混入”,是一种软件开发风格,其中的功能单元在一个类中创建,然后与其他类混合。

在面向对象编程中,Mixin是一种类,这种类包含了其他类要使用的方法,但不必充当其他类的父类。其他类是如何获取Mixin中的方法因语言的不同而不同。所以有时候Mixin被描述为’include’(包含)而不是 inheritance(继承)。Mixins鼓励代码重用,并且可以用于避免多重继承可能导致(“如钻石问题”)的继承歧义,或者解决一个缺乏对一种语言的多重继承的支持。mixin也可以被看作 实现方法 的接口。

Mixins机制的核心:在多继承背景下尽可能提升多继承的可读性。

简单理解,就是在多继承的关系中,最直接的继承关系只有一个,不变。其他的功能扩展性的继承可以通过定义mixins类的形式进行,这样能够方便确认类的继承关系及功能扩展关系。子类可以继承不同功能的Mixin类,按需动态组合使用。

使用Mixin类实现多重继承的注意事项:

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

Mixins是从多个类中重用代码的好方法,但是需要付出相应的代价,我们定义的Minx类越多,子类的代码可读性就会越差。

class Func:
    pass

class FooMixin:
    pass

class Func1(FooMixin, Func):
    pass

class Func2(Func):
    pass

注意:在被继承时,Mixin类通常写在左侧Func1(FooMixin, Func)FooMixin写在Func的左侧。

派生

在子类派生的新方法中如何重用父类的功能

  • 通过写明具体调用的某个类下的某个函数

    上面的例子中的Teacher类就是直接调取父类的类名+函数名的方式实现的

    class People:
        school = '漂亮学院'
    
        def __init__(self, name, age, gender):
            self.name = name
            self.age = age
            self.gender = gender
    
    
    class Student(People):
    
        def study(self):
            print('%s正在学习!' % self.name)
    
    
    class Teacher(People):
    
        def __init__(self, name, age, gender, salary):
            People.__init__(self, name, age, gender)
            self.salary = salary
    
        def teach(self):
            print('%s正在讲课!' % self.name)

  • 使用super()调用父类提供给自己的方法

    调用super()会得到一个特殊的对象,这个对象会去当前类的父类找属性。

    在上面代码的基础上改用super()来实现

    class People:
        school = '漂亮学院'
    
        def __init__(self, name, age, gender):
            self.name = name
            self.age = age
            self.gender = gender
    
    
    class Student(People):
    
        def study(self):
            print('%s正在学习!' % self.name)
    
    
    class Teacher(People):
    
        def __init__(self, name, age, gender, salary):
            super().__init__(name, age, gender)
            self.salary = salary
    
        def teach(self):
            print('%s正在讲课!' % self.name)
    
    
    obj2 = Teacher('张老师', 30, 'female', 3000)
    print(obj2.school)
    print(obj2.name)
    print(obj2.salary)
    obj2.teach()

    将实例化及测试代码写到__main__中,如下

    class People:
        school = '漂亮学院'
    
        def __init__(self, name, age, gender):
            self.name = name
            self.age = age
            self.gender = gender
    
    
    class Student(People):
    
        def study(self):
            print('%s正在学习!' % self.name)
    
    
    class Teacher(People):
    
        def __init__(self, name, age, gender, salary):
            super().__init__(name, age, gender)
            self.salary = salary
    
        def teach(self):
            print('%s正在讲课!' % self.name)
    
    
    if __name__ == '__main__':
        obj2 = Teacher('张老师', 30, 'female', 3000)
        print(obj2.school)
        print(obj2.name)
        print(obj2.salary)
        obj2.teach()
        
        
    漂亮学院
    张老师
    3000
    张老师正在讲课!

类的多态

多态介绍

多态,就是同一事物有多种形态。比如,猫、狗,都属于动物,是动物的一种体现形式。

类的多态要满足以下两个前提:

  1. 继承:多态一定是发生在子类和父类之间;
  2. 重写:子类重写了父类的方法;
class Animal:
    pass

class Cat(Animal):
    pass

class Dog(Animal):
    pass

class Pig(Animal):
    pass

上面代码中的Cat、Dog、Pig都是Animal,都是Animal的一个形态。

Cat、Dog、Pig三种动物都会叫,那在父类Animal中实现say()方法,但三种动物的叫声又不一样,需要在每个动物的类中重写父类Animal的say()方法

class Animal:
    def say(self):
        print('动物基本的发声动作。。。',end=' ')

class Cat(Animal):
    def say(self):
        super().say()
        print('喵喵喵')

class Dog(Animal):
    def say(self):
        super().say()
        print('汪汪汪')

class Pig(Animal):
    def say(self):
        super().say()
        print('哼哼哼')

obj1 = Cat()
obj2 = Dog()
obj3 = Pig()

obj1.say()
obj2.say()
obj3.say()


动物基本的发声动作。。。 喵喵喵
动物基本的发声动作。。。 汪汪汪
动物基本的发声动作。。。 哼哼哼

继续改写上面的例子:

class Animal:
    def say(self):
        print('动物基本的发声动作。。。',end=' ')

class Cat(Animal):
    def say(self):
        super().say()
        print('喵喵喵')

class Dog(Animal):
    def say(self):
        super().say()
        print('汪汪汪')

class Pig(Animal):
    def say(self):
        super().say()
        print('哼哼哼')

def animal_say(animal):
    animal.say()

animal_say(Cat())
animal_say(Dog())
animal_say(Pig())


动物基本的发声动作。。。 喵喵喵
动物基本的发声动作。。。 汪汪汪
动物基本的发声动作。。。 哼哼哼

使用多态,是可以在不考虑对象具体类型的情况下,而直接使用对象。

上面的例子,是鸭子类型的一种表示形式。

鸭子类型

鸭子类型(duck typing),可以这样表述:“当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子。” 在鸭子类型中,关注点在于对象的行为,能作什么;而不是关注对象所属的类型。即对象是否具有相同的属性,而不是关注是否属于同一个类或者是否有继承关系。

class Dog:
    def say(self):
        print('汪汪汪')

class Cat:
    def say(self):
        print('喵喵喵')

class Person:
    def say(self):
        print('嘤嘤嘤')
        
if __name__ == '__main__':
    obj1 = Cat()
    obj2 = Dog()
    obj3 = Person()
    obj1.say()
    obj2.say()
    obj3.say()
    
    
喵喵喵
汪汪汪
嘤嘤嘤

上面的例子,3个类dog、cat、person,是3个独立的类,没有任务继承关系。而这3个类,有这相同的say()方法,在刨除他们分属于不同的类以外,他们有着相同的方法,这就是典型的鸭子类型。

这种典型的鸭子类型,不是在语法层面进行约束,而是在规矩层面进行实现,程序设计者遵循某种规矩将符合条件的类设计成鸭子类型。

抽象类

抽象类的作用就是控制子类的方法的,要求子类必须按照父类的要求实现指定的方法,且方法名要和父类保存一致。

同一类事物,比如动物类,可以抽取出动物的共有属性作为一个抽象类,类中可以仅定义共有的方法,无需具体实现。而继承这个抽象类的子类,必须要实现或者说重写父类中的方法。

定义抽象类需要引入两个模块

from abc import ABCMeta
from abc import abstractmethod
from abc import ABCMeta
from abc import abstractmethod

# 通过传入metaclass=ABCMeta表示当前类为抽象类
class Animal(metaclass=ABCMeta):

    # 通过装饰器@abstractmethod表示当前方法为抽象方法,子类必须进行实现
    @abstractmethod
    def say(self):
        pass

class Cat(Animal):
    pass

class Dog(Animal):
    pass

class Pig(Animal):
    pass

# Cat类中没有实现抽象父类中的say方法,实例化时,将会报错
obj1 = Cat()

执行结果:

TypeError: Can't instantiate abstract class Cat with abstract methods say

下面将子类实现父类的say方法

from abc import ABCMeta
from abc import abstractmethod

# 通过传入metaclass=ABCMeta表示当前类为抽象类
class Animal(metaclass=ABCMeta):

    # 通过装饰器@abstractmethod表示当前方法为抽象方法,子类必须进行实现
    @abstractmethod
    def say(self):
        pass

class Cat(Animal):
    def say(self):
        pass

class Dog(Animal):
    def say(self):
        pass

class Pig(Animal):
    def say(self):
        pass

obj1 = Cat()
obj2 = Dog()
obj3 = Pig()

使用抽象类的注意事项:

  • 抽象类不能被实例化;
  • 子类需要严格遵守父类的抽象类规则,而孙类不需要遵守这个规则;

类方法和静态方法

绑定方法与非绑定方法

  • 绑定方法,特殊之处在于将调用者本身当做第一个参数自动传入

    • 绑定给对象的方法:调用者是对象,自动传入的是对象 (实例方法,self)

    • 绑定给类的对象:调用者是类,自动传入的是类 (类方法,cls)

  • 非绑定方法,没有绑定给任何类或对象,调用者可以是类、对象,没有自动传调用者的效果。(静态方法)

类方法(classmethod)

装饰器 @classmethod 修饰的方法称为:类方法,在使用的时候,会将类本身作为第一个参数 cls 传递给类方法

class Test1():

    # 类方法,第一个参数为cls,代表类本身
    @classmethod
    def func(cls):
        pass

if __name__ == '__main__':
    # 直接使用类名+方法名调用
    Test1.func()

静态方法(staticmethod)

装饰器 @staticmethod 修饰的方法称为:静态方法,在使用的时候,不会自动传递任何参数。和普通的函数没有什么区别

class Test2():
    @staticmethod
    def func1():
        """静态方法"""
        pass


if __name__ == '__main__':
    # 直接使用类名+方法名调用
    Test2.func1()

总结一下实例方法、静态方法、类方法的区别:

实例方法:第一个参数 self 代表实例对象本身,可以使用 self 直接引用定义的实例属性和实例方法;如果需要调用静态方法和类方法,通过「 类名.方法名() 」调用即可;
静态方法:使用「 类名.静态变量 」引用静态变量,利用「 类名.方法名() 」调用其他静态方法和类方法;如果需要调用实例方法,需要先实例化一个对象,然后利用对象去调用实例方法;
类方法:第一个参数 cls 代表类本身,通过「 cls.静态变量 」或「 类名.静态变量 」引用静态变量,利用「 cls.方法名() 」或「 类名.方法名() 」去调用静态方法和类方法;如果需要调用实例方法,需要先实例化一个对象,然后利用对象去调用实例方法。


文章作者: 老百
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 老百 !
 上一篇
Python之反射 Python之反射
在执行对象的某个方法或调用对象中的某个变量,在无法确定这个方法或变量是否存在是,需要用一个特殊的方法或机制来访问和操作这个位置的方法或变量,称之为反射。
2022-10-20
下一篇 
Python匿名函数和高阶函数 Python匿名函数和高阶函数
本文结合各种实际的例子详细讲解了Python中匿名函数和5个内建高阶函数的使用,能够帮助理解Python的数据结构和提高数据处理的效率。
2022-10-13
  目录