面向对象

面向对象

1 面向过程与面向对象

1.1 面向过程

面向过程的编程是面向流程的,就是一步一步的按照过程来进行,把完成某一个需求的所有步骤从头到尾逐步实现;

根据开发需求,将某些功能独立的代码块封装成一个又一个函数。面向过程最重要的特点就是函数,即把执行一件事的过程拆分成一个又一个步骤,然后将这些步骤变为一个又一个的子函数,再通过主函数按照顺序来调用这些子函数以完成整个事件。

这些子函数需要按照事件的发展顺序,或者程序中代码的编写顺序一步一步的执行下去。

面向过程的特点:

  1. 注重步骤与过程,不注重职责分工
  2. 如果需求复杂,代码会变得很复杂
  3. 开发复杂项目时,没有固定的套路,开发难度很大

在这里插入图片描述

1.2 面向对象

人们在认识世界时,会将对象简单处理为两个部分——属性和行为。

对象具有属性,也可以称为变量;正如每个人都有姓名、年龄、身高、体重等属性,我们可以用这些数据来描述对象的属性。

对象具有行为,也可以称为方法;就如同每个人都要吃饭、睡觉、运动一样;面向对象编程将完成某个功能的代码块定义为方法,方法可以被其它程序调用,也可以被对象自身调用。

面向对象的编程,在完成某一个需求前,首先要确定都有哪些职责,即要做的事情(方法);然后根据职责确定不同的对象,在对象内部封装多个不同的方法;最后完成的代码,就是顺序的让不同的对象调用不同的方法。

面向对象的主要特点是类,类是独立个体。即先将所有需要执行的功能或者函数拆分,然后进行分类,封装到不同的类中;程序执行的过程中,通过不同的类或类的对象来调用不同的函数以完成程序的运行和执行;把一个事件运行的执行过程变为了类和类中功能的交互,也就是不同的对象之间的交互,因此我们也把这种编程称为面向对象程序设计。

面向对象的特点:

  1. 注重对象和职责,不同的对象承担不同的职责
  2. 更加适合应对复杂的需求变化,是专门应对复杂项目开发提供的固定套路
  3. 需要在面向过程的基础上,再学习一些面向对象的语法

在这里插入图片描述

1.3 两者之间的对比

比如对"把大象放进冰箱"进行分析。

面向过程:

  1. 打开冰箱门
  2. 把🐘装入冰箱
  3. 关好冰箱门

针对这三个步骤分别编写三个函数,从而实现整个过程。

面向对象:

需要分析出其中的对象以及这种对象具有的动作。

  • 对象1:大象。动作,被装。
  • 对象2:冰箱。动作,开门,关门。

分别设计两种类,以及两种类所具有的动作。

此时,面向对象设计看起来似乎要比面向过程设计复杂,后者只需要三个步骤就完成了,但是前者还要设计类,有点麻烦。
但是如果事件不止这三个步骤,冰箱有冰箱的体积、温度,大象也有大象的体积、重量,不同的大象要放在不同的冰箱中还要有对应的温度,或者编程过程中突然有了新的需求,那么面向过程的程序可能就需要重新设计;但是面向对象的程序因为对象和类已经明确,因此只需要在变化的类中添加相应的属性以及需要做的动作就行,此时,面向对象编程的优势就体现出来了。

再来看一个"下五子棋"的实现例子。

面向过程的设计思路:

首先分析事件的步骤:1、开始游戏 2、黑子先走 3、绘制画面 4、判断输赢 5、轮到白子 6、绘制画面 7、判断输赢 8、返回步骤2 9、输出最后结果。然后把上面每个步骤分别用一个函数来实现。

面向对象的设计思路:

整个五子棋可以分为:1、黑白双方,这两方的行为是一模一样的 2、棋盘系统,负责绘制画面 3、规则系统,负责判定诸如犯规、输赢等。第一类对象(玩家对象)负责接受用户输入,并告知第二类对象(棋盘对象)棋子布局的变化,棋盘对象接收到了棋子的变化就要负责在屏幕上显示出这种变化,同时利用第三类对象(规则系统)来对棋局进行判定。

两者对比:

  1. 两者都可以实现代码重用和模块化编程的功能,但是面向对象使用的是类进行封装,因此封装性更强,也更加安全。
  2. 面向过程的编程是根据事情的发展顺序来进行的,注重于编程的步骤,而面向对象的编程注重于对象之间的交互。
  3. 从前期角度来看,面对对象远比面向过程要复杂,但是从维护和扩展功能的角度来看,面对对象远比面向过程要简单!比如上面"把大象放进冰箱"的事件。
  4. 面向对象是以数据为主导,面向过程是以动作为主导。
  5. 面向对象的编程,是关注如何把相关的功能,包括函数和数据有组织地捆绑到一个对象身上,它强调"封装"、"继承"、"多态",把一些数据和相关的操作封装起来包装成对象,使所有的模块达到更好的内聚性和更低的耦合性,使不同的对象之间减少依赖性,从而提高代码的可复用性,提高编程效率。

2 类与对象

类和对象是面向对象编程的两个核心概念。

2.1 类

类是一群具有相同或相似的特征或行为的对象的统称。类是抽象的,不能直接使用;特征被称为属性,行为被称为方法。

人就是一种类,每个人——即人这种类的对象,都有姓名、年龄、身高、体重等属性,每个人也有吃饭、睡觉、运动等行为。类是对象的抽象,对象则是类的实例化、具体化,每个对象都包括了类中定义的属性和行为。

类就相当于是制造飞机时的图纸,是一个模板,是负责创建对象的。

在这里插入图片描述

2.2 对象

在Python中,对象几乎是无处不在的,我们之前学习的变量、数据、函数等都是对象。

对象是由类创建出来的一个实例化、具体化的存在。对象是具体的,可以直接使用。

由哪一个类创建出来的对象,就拥有在哪一个类中定义的属性和方法。

在程序开发中,应该先有类,再有对象。

对象就相当于是根据图纸制造出来的飞机。

在这里插入图片描述

2.3 类和对象的关系

类是模板,对象是根据类这个模板创建出来的,所有应该先有类,再有对象。

类只有一个,而一个类中的对象可以有很多个,不同的对象可能拥有不同的属性。

类中定义了什么属性和方法,对象中就有什么属性和方法,不可能多,也不可能少。

2.4 类的设计

在使用面向对象开发前,应该首先分析事件的需求,确定程序中需要包含哪些类。

在程序开发中,要设计一个类,通常需要满足三个要素

  1. 类名,这类事物的名字,应满足大驼峰命名法;
  2. 属性,这类事物具有什么样的特征;
  3. 方法,这类事物具有什么样的行为。

大驼峰命名法:每一个单词的首字母大写;单词与单词之间没有下划线。

2.4.1 类名的确定

用名词提炼法分析整个业务流程,流程中出现的名词,通常就是找到的类。

2.4.2 属性和方法的确定

对对象的特征描述,通常可以定义成属性。

对象具有的行为(动词),通常可以定义成方法。

提示:需求中没有涉及到的属性或方法,在设计类时不需要考虑。

3 类的创建与运用

3.1 类的相关概念的介绍

◇ 类:用来描述具有相同属性和方法的对象的集合。它定义了该集合中每个对象所共有的属性和方法。对象是类的实例。

◇ 对象:通过类定义的数据结构实例。对象包括两个数据成员(类变量和实例变量)和方法。

◇ 方法:类中定义的函数。

◇ 实例化:创建一个类的实例,一个类的具体对象。

◇ 类变量:类变量在整个实例化的对象中是公用的。类变量定义在类中且在函数体之外,通常不作为实例变量使用。

◇ 实例变量:定义在方法中的变量,只作用于当前实例的类。

◇ 数据成员:类变量或者实例变量,用于处理类及其实例对象的相关数据。

◇ 方法重写:如果从父类继承的方法不能满足子类的需求,可以对其进行改写,这个改写的过程叫做方法的覆盖,也称作方法的重写。

◇ 继承:指一个派生类继承基类的字段和方法。继承允许把一个派生类的对象作为一个基类对象对待。

3.2 类的定义

面向对象是更大的封装,在一个类中封装多个方法,这样通过这个类创建出来的对象,就可以直接调用这些方法了。

Python中使用class关键字来声明一个类,class中有成员属性和成员方法。

类名和变量名一样区分大小写,字母相同但大小写不同的类是不相同的类。

在Python中定义一个只包含方法的类,格式如下:

class 类名:
	def 方法1(self, 参数列表):
		代码块
	def 方法2(self, 参数列表):
		代码块
    ...

方法的定义格式和之前学习过的函数几乎是一样的,主要的区别在于方法的第一个参数必须是self。

注意,类名的命名规则要符合大驼峰命名法。

定义完类之后,要使用这个类来创建对象,格式如下:

对象变量 = 类名()

创建对象和创建变量类似,需要先声明对象属于哪个类,同时指明对象名称。

类的定义
print('定义类和对象')

class EmptyClass:  # 创建类
    pass

empty = EmptyClass()  # 创建对象变量
print(type(empty))
执行结果如下:
定义类和对象
<class '__main__.EmptyClass'>
3.2.1 第一个面向对象程序

需求:小猫爱吃鱼,小猫要喝水

分析:

  1. 定义一个猫类Cat
  2. 定义两个方法,eat和drink
  3. 按照需求,不需要定义属性

在这里插入图片描述

程序
print('第一个面向对象程序')

class Cat:
    """这是一个猫类"""

    def eat(self):
        print('小猫爱吃鱼')  # 定义eat方法

    def drink(self):
        print('小猫要喝水')  # 定义drink方法

tom = Cat()  # 定义对象变量tom

tom.eat()  # 调用eat方法
tom.drink()  # 调用drink方法
执行结果如下:
第一个面向对象程序
小猫爱吃鱼
小猫要喝水
3.2.2 设置对象属性

在Python中,要给对象设置属性,非常的容易,只需要在类的外部的代码中直接通过 "对象变量名.属性名 = 赋值" 格式,即可设置属性;但是不推荐使用,因为对象属性的封装应该在类的内部实现。

注意,这种设置属性的方式虽然简单,但是在程序开发中并不推荐使用。

设置对象属性
class Cat:
    """这是一个猫类"""

    def eat(self):
        print('小猫爱吃鱼')  # 定义eat方法

    def drink(self):
        print('小猫在喝水')  # 定义drink方法

tom = Cat()  # 定义对象变量tom

tom.eat()  # 调用eat方法
tom.drink()  # 调用drink方法

tom.name = 'Tom'  # 设置名字属性
tom.age = 5  # 设置年龄属性
print('tom的名字叫:%s' % tom.name)
print('%s的年龄为:%d岁' % (tom.name, tom.age))
执行结果如下:
小猫爱吃鱼
小猫在喝水
tom的名字叫:Tom
Tom的年龄为:5岁
3.2.3 self的使用

由哪一个对象调用的方法,方法内的self就是哪一个对象的引用。

在类封装的方法内部,self就表示当前调用方法的对象本身。

调用方法时,程序员不需要传递self参数;在方法内部,可以通过 "self." 访问对象的属性,也可以通过 "self." 调用其它的对象方法。

在类的外部,通过 "变量名." 访问对象的属性和方法。

在类封装的方法中,通过 "self." 访问对象的属性和方法。

self的使用
print('self的使用')

class Cat:

    def eat(self):
        print('%s爱吃鱼' % self.name)  # 用self来调用属性

tom = Cat()
tom.name = 'Tom'
tom.eat()  # 对象为tom,因此调用方法eat时,self指的是tom

lazy_cat = Cat()
lazy_cat.name = '大懒猫'
lazy_cat.eat()  # 对象为lazy_cat,因此调用方法eat时,self指的是lazy_cat
执行结果如下:
self的使用
Tom爱吃鱼
大懒猫爱吃鱼
3.3 类的初始化方法
3.3.1 定义和使用初始化方法

当使用 "类名()" 创建对象时,会自动执行以下操作:

  1. 为对象变量分配内存空间——创建对象
  2. 为对象的属性设置初始值——初始化方法(__ init __)

这个初始化方法就是__ init __方法(init前后都有两个连续的短下划线), 它是对象的内置方法。

__ init __方法是专门用来定义一个类具有哪些属性的方法!

定义和使用初始化方法
print('定义和使用初始化方法')

class Dog:

    def __init__(self):  # 定义初始化方法
        print('汪汪汪!')

dog = Dog()  # 创建对象变量dog
执行结果如下:
定义和使用初始化方法
汪汪汪!

当一个类定义了 __ init __ 方法,类在实例化时会自动调用 __ init __ 方法,用于创建新的类实例。在上面的例子中,新的实例dog被创建,同时执行了初始化方法,运行了print函数。

注意,初始化方法的返回值必须是 "None" 。

3.3.2 在初始化方法中定义属性

在初始化方法中,我们可以初始化一些属性。

属性(或者叫成员变量、类变量)必须使用 "self." 的方式赋值,不能直接定义变量;直接定义的变量的生命周期只会在函数内体现,函数执行完变量就会被销毁。

在 __ init __ 方法中使用 "self.属性名 = 属性的初始值" 格式就可以定义属性。

定义属性之后,再使用类创建对象,那么被创建的对象都会拥有该属性。

初始化属性
print('初始化属性')

class Dog:

    def __init__(self, name):  # 定义初始化方法,并定义name形参
        self.name = name  # 初始化名字属性
        self.age = 5  # 初始化年龄属性

dog = Dog('旺财')  # 创建对象变量dog,并传入name实参

print(dog.name)  # 输出名字属性
print(dog.age)  # 输出年龄属性
执行结果如下:
初始化属性
旺财
5

其实函数 __ init __ 的第一个参数 "self" 指的就是实例本身,在C++等语言中对应的就是 "this" 指针,可以理解为对实例的属性进行赋值。Python在调用 __ init __ 函数时会自动地添加实例作为函数的第一个参数。

我们可以使用参数设置属性初始值来对 __ init __ 方法进行进一步的改造:

  1. 把想要设置的属性值,定义成 __ init __ 方法的参数
  2. 在方法内部使用 "self.属性名 = 形参" 接收外部传递的参数
  3. 在创建对象时,使用 "对象变量名 = 类名(实参)" 来调用属性
3.3.3 类中的方法

在类中定义的函数我们称之为方法。类中的方法和函数定义的方式基本相同,主要区别就在于方法一定要定义在类里面,并且第一个参数必须是 "self" ,其它方面和函数没什么差异。

类中的方法
print('类中的方法')

class Dog:

    def __init__(self, name):  # 定义初始化方法
        self.name = name  # 定义name属性

    def shout(self):  # 定义shout方法
        print('汪汪汪!我是%s' % self.name)

dog = Dog('旺财')  # 创建对象变量dog
dog.shout()  # 调用shout方法
执行结果如下:
类中的方法
汪汪汪!我是旺财
3.4 Python的内置方法和属性
3.4.1 __ del __ 方法

定义了 __ del __ 方法之后,当一个对象要从内存中销毁,销毁之前, __ del __ 方法会被自动调用。

如果想在对象被销毁前,再做一些事情,就可以使用 __ del __ 方法。

__ del __ 方法
print('__del__方法')

class Cat:

    def __init__(self, name):
        self.name = name
        print('%s来了!' % self.name)

    def __del__(self):
        print('%s走了!' % self.name)  # 执行完变量的所有相关代码,然后才执行此句

tom = Cat('汤姆')
print(tom.name)
执行结果如下:
__del__方法
汤姆来了!
汤姆
汤姆走了!
生命周期:
  • 一个对象从类的实例化创建,生命周期开始
  • 一个对象的 __ del __ 方法一旦被调用,生命周期结束
  • 在对象的生命周期内,可以访问对象属性,或者让对象调用方法
3.4.2 __ str __ 方法

在Python中,使用print函数输出对象变量时,默认情况下,会输出这个变量引用的对象是由哪一个类创建的对象,以及这个变量在内存中的地址(用十六进制表示)。

如果在开发中,希望在使用print函数输出对象变量时,能够打印自定义的内容,那么就可以使用 __ str __ 这个内置方法。

注意, __ str __ 方法必须返回一个字符串

__ str __ 方法
print('__str__方法')

class Cat:

    def __init__(self, name):
        self.name = name
        print('%s来了!' % self.name)

    def __del__(self):
        print('%s走了!' % self.name)  # 执行完变量的所有相关代码,然后才执行此句

    def __str__(self):
        return '我是小猫:%s' % self.name  # 必须返回一个字符串

tom = Cat('汤姆')
print(tom)  # 因为定义了__str__方法,所以这里打印对象变量tom,只会输出方法里自定义的内容
执行结果如下:
__str__方法
汤姆来了!
我是小猫:汤姆
汤姆走了!
3.5 私有属性和私有方法
3.5.1 私有属性

实例可以轻松地获取和修改方法中定义的属性的值,但是有时候我们需要限制实例随意修改属性,这时候就要用到私有属性。

定义私有属性很简单,只要在定义属性名字的时候使用两条下划线作为开头,Python解释器就会认为这个属性是私有的,外部不能随便访问这个属性。

私有属性是只能在类内部被操作的属性,实例不能直接访问。也就是说,在对象的方法内部,是可以访问对象的私有属性的,但在外部不能。

在平时的实际项目中,我们可以使用这个特性保护一些不想让用户随意修改的属性。

私有属性
print('私有属性')

class Dog:

    def __init__(self, name):
        self.__name = name  # 定义name私有属性
        self.__age = None  # 定义age私有属性
        print(self.__name, '取名成功')

    def set_age(self, age):
        if not isinstance(age, int):
            print('年龄必须是整型!')
            return False

        if age <= 0:
            print('年龄必须大于0!')
            return False

        self.__age = age

    def shout(self):
        print('汪汪汪!我今年%s岁' % self.__age)

dog = Dog('旺财')
dog.set_age('hello')
dog.set_age(-20)
dog.set_age(5)
dog.shout()
'''
print(dog.__name)
这是错误的语法,因为__name是私有属性,变量不能直接访问私有属性
在上面的例子中,__age是私有属性,实例化后只能通过set_age方法设置年龄,变量不能直接访问
'''
执行结果如下:
私有属性
旺财 取名成功
年龄必须是整型!
年龄必须大于0!
汪汪汪!我今年5岁
3.5.2 私有方法

与私有属性一样,私有方法只能在类内部被调用,实例不能直接调用。

定义私有方法的方式跟定义私有属性一样,只要在定义方法名字的时候使用两条下划线作为开头即可。

调用私有方法的格式为 "对象变量._类名__方法名(参数)" 。这是因为,在python解释器中,所有以双下划线开头的方法都会被翻译成方法前面加上单下划线和类名的形式。

私有方法
print('私有方法')

class Dog:

    def __animal(self, name):  # 定义私有方法
        print('我的名字叫:%s' % name)

dog = Dog()  # 创建对象变量
dog._Dog__animal('旺财')  # 调用私有方法
执行结果如下:
私有方法
我的名字叫:旺财

4 封装、继承和多态

面向对象编程具有三大特性——封装性、继承性和多态性,这些特性使程序设计具有良好的扩展性和健壮性。

  1. 封装:根据职责将属性和方法封装到一个抽象的类中
  2. 继承:实现代码的重用,相同的代码不需要重复的编写
  3. 多态:不同的对象调用相同的方法,产生不同的执行结果,增加代码的灵活度
4.1 封装

封装是面向对象编程的一大特点,是面向对象编程的第一步,它将属性和方法封装到一个抽象的类中,外部使用类创建对象,然后让对象调用方法,对象方法的细节都被封装在类的内部。

4.1.1 封装案例——小明爱跑步

需求:

  1. 小明体重75.0公斤
  2. 小明每次跑步会减肥0.5公斤
  3. 小明每次吃东西会增肥1公斤

对需求进行分析:首先用名词提炼法把需求中的名词提炼出来,本案例中有个 "小明" 的名字,因此我们可以定义一个 "人" 的类,在类中定义一个name属性,用来存储小明这个名字;再接着看,小明还有 "体重" 这个特征,所以我们再定义一个weight属性,用来存储小明的体重;明确完小明的特征属性之后,再看案例中的动词,案例中有 "跑步" 、 "吃东西" 两个动词,因此可以在类中定义两个方法run(self)和eat(self);同时在这个案例中,我们可以用 __ init __ 方法来简化对象的创建,用 __ str __ 方法简化小明这个对象的输出。

在这里插入图片描述

封装案例——小明爱跑步
class Person:

    def __init__(self, name, weight):
        # self.属性名 = 形参
        self.name = name  # 定义name属性
        self.weight = weight  # 定义weight属性

    def __str__(self):  # 用__str__方法存储自定义内容
        return '我的名字叫:%s  体重是 %.2f 公斤' % (self.name, self.weight)

    def run(self):
        print('%s爱跑步,运动使人健康!' % self.name)
        self.weight -= 0.5

    def eat(self):
        print('%s是吃货,吃完这顿再减肥~' % self.name)
        self.weight += 1

xiaoming = Person('小明', 75.0)  # 创建对象变量xiaoming

xiaoming.run()  # 调用run()方法
print(xiaoming)  # 打印自定义内容

xiaoming.eat()  # 调用eat()方法
print(xiaoming)  # 打印自定义内容
执行结果如下:
小明爱跑步,运动使人健康!
我的名字叫:小明  体重是 74.50 公斤
小明是吃货,吃完这顿再减肥~
我的名字叫:小明  体重是 75.50 公斤

在对象的方法内部,是可以直接访问对象属性的;同一个类创建的多个对象之间,属性互不干扰。

4.1.2 封装案例——摆放家具

需求:

  1. 房子(House)户型总面积家具名称列表

    ● 新房子没有任何的家具

  2. 家具(HouseItem)名字占地面积,其中

    席梦思(bed)占地 4 平米

​ ● 衣柜(chest)占地 2 平米

​ ● 餐桌(table)占地 1.5 平米

  1. 将以上三件家具添加到房子中

  2. 打印房子时,要求输出:户型总面积剩余面积家具名称列表

在这里插入图片描述

剩余面积

  1. 在创建房子对象时,定义一个剩余面积的属性,初始值和总面积相等
  2. 当调用add_item方法,向房间添加家具时,剩余面积 -= 家具面积

先开发哪个类?应该先开发家具类(被使用的类,通常应该先开发)

  1. 家具简单
  2. 房子要使用到家具

添加家具

  1. 判断家具的面积是否超过剩余面积,如果超过,应提示不能添加这件家具
  2. 将家具的名称添加到家具名称列表中
  3. 用房子的剩余面积 - 家具面积
封装案例——摆放家具
class HouseItem:  # 定义家具类

    def __init__(self, name, area):
        self.name = name  # 定义name属性
        self.area = area  # 定义area属性

    def __str__(self):
        return '"%s"占地 %.2f 平米' % (self.name, self.area)


class House:  # 定义房子类

    def __init__(self, house_type, area):
        self.house_type = house_type  # 定义house_type属性
        self.area = area  # 定义area属性
        self.free_area = area  # 剩余面积初始值就等于总面积
        self.item_list = []  # 家具名称列表初始值为空列表

    def __str__(self):
        # Python能够自动的将一对括号内的代码连接在一起
        return ('户型:%s\n总面积:%.2f\n剩余面积:%.2f\n家具名称列表:%s'
                % (self.house_type, self.area,
                   self.free_area, self.item_list))

    def add_item(self, item):
        print('添加家具:%s' % item.name)

        if item.area > self.free_area:  # 判断家具的面积是否超过剩余面积
            print('%s的面积已超过剩余面积,无法添加该家具' % item.name)
            return

        self.item_list.append(item.name)  # 将家具名称添加到家具名称列表中
        self.free_area -= item.area  # 计算剩余面积

bed = HouseItem('席梦思', 4)  #创建对象变量bed
chest = HouseItem('衣柜', 2)  #创建对象变量chest
table = HouseItem('餐桌', 1.5)  #创建对象变量table

print(bed)
print(chest)
print(table)

print('---------')

my_home = House('三室两厅', 120)  # 创建对象变量my_home

my_home.add_item(bed)  # 调用add_item()方法,并传入bed参数
print(my_home)

print('---------')

my_home.add_item(chest)  # 调用add_item()方法,并传入chest参数
print(my_home)

print('---------')

my_home.add_item(table)  # 调用add_item()方法,并传入table参数
print(my_home)
执行结果如下:
"席梦思"占地 4.00 平米
"衣柜"占地 2.00 平米
"餐桌"占地 1.50 平米
---------
添加家具:席梦思
户型:三室两厅
总面积:120.00
剩余面积:116.00
家具名称列表:['席梦思']
---------
添加家具:衣柜
户型:三室两厅
总面积:120.00
剩余面积:114.00
家具名称列表:['席梦思', '衣柜']
---------
添加家具:餐桌
户型:三室两厅
总面积:120.00
剩余面积:112.50
家具名称列表:['席梦思', '衣柜', '餐桌']
4.1.3 封装案例——士兵突击

一个对象的属性可以是另一个类创建的对象。

在定义属性时,如果不知道设置什么初始值,可以设置为 "None" 。

需求:

  1. 士兵许三多有一把AK47
  2. 士兵可以开火
  3. 发射子弹
  4. 装填子弹——增加子弹数量

在这里插入图片描述

封装案例——士兵突击
class Gun:  # 定义枪类

    def __init__(self, model):
        self.model = model  # 定义model属性,表示枪的型号
        self.bullet_count = 0  # 定义bullet_count属性,表示子弹的数量

    def add_bullet(self, count):  # 定义add_bullet()方法,表示装填子弹的动作
        self.bullet_count += count
        print('枪里有 %s 颗子弹' % self.bullet_count)

    def shoot(self):  # 定义shoot()方法,表示发射子弹的动作
        if self.bullet_count <= 0:  # 判断子弹数量
            print('%s没有子弹了...' % self.model)
            return

        self.bullet_count -= 1  # 发射子弹,子弹数量-1

        print('突突突...子弹剩余 %s 颗' % self.bullet_count)


class Soldier:  # 定义士兵类

    def __init__(self, name):
        self.name = name  # 定义name属性
        self.gun = None  # 定义gun属性
        print('士兵%s有一把%s!' % (self.name, ak47.model))

    def fire(self):  # 定义fire()方法,表示开火的动作
        if self.gun == None:  # 判断士兵是否有枪
            print('%s还没有枪...' % self.name)
            return

        print('冲啊...')

        self.gun.add_bullet(50)  # 让枪装填子弹
        self.gun.shoot()  # 让枪发射子弹

ak47 = Gun('AK47')  # 创建枪对象

xusanduo = Soldier('许三多')  # 创建士兵对象

xusanduo.gun = ak47  # 将ak47对象赋值给xusanduo.gun这个属性

xusanduo.fire()  # 调用fire()方法
执行结果如下:
士兵许三多有一把AK47!
冲啊...
枪里有 50 颗子弹
突突突...子弹剩余 49 颗
4.1.4 身份运算符

身份运算符用于比较两个对象的内存地址是否一致。

在Python中,身份运算符有两个:

  1. is,is是判断两个标识符是不是引用同一个对象,x is y 类似于 id(x) == id(y)
  2. is not,is not是判断两个标识符是不是引用不同对象,x is not y 类似于 id(x) != id(y)

is用于判断两个变量引用对象是否为同一个。

==用于判断引用变量的值是否相等。

在Python中,针对 "None" 比较时,建议使用 "is" 来判断。

>>> a = [1, 2, 3]
>>> b = [1, 2, 3]
>>> b == a
True
>>> b is a
False
4.2 继承

继承的基本思想是,在一个类的基础上定义出另一个新的类,这个新的类不仅可以继承原来类的所有属性和方法,还可以增加新的属性和方法;原来的类被称为父类,新的类被称为子类。

4.2.1 单继承

父类的定义和一般类的定义一样,子类的定义格式如下:

# SubClass为子类的名字,BaseClass为父类的名字
class SubClass(BaseClass1, BaseClass2, ...)
	代码块

在这里插入图片描述

子类的定义
class Animal:  # 定义父类Animal

    def eat(self):
        print('能吃!')

    def drink(self):
        print('能喝!')

    def run(self):
        print('能跑!')

    def sleep(self):
        print('能睡!')


class Dog(Animal):  # 定义Animal的子类Dog

    def bark(self):  # 新增bark()方法
        print('能叫!')


class XiaoTianQuan(Dog):  # 定义Dog的子类XiaoTianQuan

    def fly(self):  # 新增fly()方法
        print('能飞!')


class Cat(Animal):  # 定义Animal的子类Cat

    def catch(self):  # 新增catch()方法
        print('能抓老鼠!')

print('Animal类的对象:')

animal = Animal()  # 创建animal对象

animal.eat()
animal.drink()
animal.run()
animal.sleep()

print('---------')
print('Dog类的对象:')

dog = Dog()  # 创建dog对象

dog.eat()
dog.drink()
dog.run()
dog.sleep()
dog.bark()

print('---------')
print('XiaoTianQuan类的对象:')

xiaotianquan = XiaoTianQuan()  # 创建xiaotianquan对象

xiaotianquan.eat()
xiaotianquan.drink()
xiaotianquan.run()
xiaotianquan.sleep()
xiaotianquan.bark()
xiaotianquan.fly()

print('---------')
print('Cat类的对象:')

cat = Cat()  # 创建cat对象

cat.eat()
cat.drink()
cat.run()
cat.sleep()
cat.catch()
执行结果如下:
Animal类的对象:
能吃!
能喝!
能跑!
能睡!
---------
Dog类的对象:
能吃!
能喝!
能跑!
能睡!
能叫!
---------
XiaoTianQuan类的对象:
能吃!
能喝!
能跑!
能睡!
能叫!
能飞!
---------
Cat类的对象:
能吃!
能喝!
能跑!
能睡!
能抓老鼠!

上面的例子中,涉及到一些专业术语:

  1. Dog类是Animal类的子类,Animal类是Dog类的父类,Dog类从Animal类继承
  2. Dog类是Animal类的派生类,Animal类是Dog类的基类,Dog类从Animal类派生
4.2.2 方法重写

当父类的方法不能满足子类的需求时,可以在子类中对父类的方法进行重写。

方法重写有两种情况:

  1. 覆盖父类的方法。如果在开发中,父类的方法实现和子类的方法实现完全不同,就可以使用覆盖的方式,在子类中重新编写父类的方法实现;具体的实现方式,就是在子类中定义一个和父类中同名的方法,并且实现;重写之后,在运行时只会调用在子类中重写的方法,而不再会调用父类中名字相同的方法。
  2. 对父类方法进行扩展。如果在开发中,子类的方法实现包含父类的方法实现,即父类原本封装的方法实现是子类方法的一部分,就可以使用扩展的方式;具体的实现方式,是在子类中重写父类的方法,在需要的位置使用 "super().父类方法" 来调用父类的方法,在其它位置针对子类的需求编写子类特有的代码实现。

关于super

  • 在Python中是一个特殊的类
  • super()就是使用super类创建出来的对象
  • 最常使用的场景就是在重写父类方法时,调用在父类中封装的方法实现
方法重写——覆盖
class Animal:  # 定义父类Animal

    def eat(self):
        print('能吃!')

    def drink(self):
        print('能喝!')

    def run(self):
        print('能跑!')

    def sleep(self):
        print('能睡!')


class Dog(Animal):  # 定义Animal的子类Dog

    # 如果子类中,重写了父类的方法,在使用子类对象调用方法时,会调用子类中重写的方法
    def eat(self):
        print('小狗生病了,吃不下东西')

    def run(self):
        print('小狗腿折了,不能跑,只能走')

dog = Dog()  # 创建dog对象

dog.eat()  # 实现在子类中重写的方法
dog.drink()  # 实现从父类中继承的方法
dog.run()  # 实现在子类中重写的方法
dog.sleep()  # 实现从父类中继承的方法
执行结果如下:
小狗生病了,吃不下东西
能喝!
小狗腿折了,不能跑,只能走
能睡!
方法重写——扩展
class Animal:  # 定义父类Animal

    def eat(self):
        print('能吃!')

    def drink(self):
        print('能喝!')

    def run(self):
        print('能跑!')

    def sleep(self):
        print('能睡!')


class Dog(Animal):  # 定义Animal的子类Dog

    def eat(self):
        super().eat()  # 使用"super().父类方法"调用原本在父类中封装的eat()方法
        print('比隔壁邻居家的狗还能吃!!')

    def run(self):
        super().run()  # 使用"super().父类方法"调用原本在父类中封装的run()方法
        print('比隔壁邻居家的狗跑的还要快!!!')

dog = Dog()  # 创建dog对象

dog.eat()  # 实现在子类中重写的方法
dog.drink()  # 实现从父类中继承的方法
dog.run()  # 实现在子类中重写的方法
dog.sleep()  # 实现从父类中继承的方法
执行结果如下:
能吃!
比隔壁邻居家的狗还能吃!!
能喝!
能跑!
比隔壁邻居家的狗跑的还要快!!!
能睡!

调用父类方法还有另外一种方式,在Python 2.x中,如果需要调用父类的方法,可以使用 "父类名.方法(self)" 的方式。这种方式目前在Python 3.x中也被支持,但这种方式并不推荐使用,因为一旦父类发生变化,方法调用位置的类名同样需要修改。

4.2.3 多继承

在Python中,子类可以拥有多个父类,并且具有所有父类的属性和方法。

例如:孩子会继承自己父亲和母亲的特性。

在这里插入图片描述

多继承的语法格式如下:

class 子类名(父类名1, 父类名2, ...):
	代码块
多继承
print('多继承')

class A:  # 定义父类A

    def test(self):
        print('我是C的父类:A')


class B:  # 定义父类B

    def demo(self):
        print('我是C的父类:B')


class C(A, B):  # 定义A、B的子类C

    pass  # 占位语句

c = C()  # 创建子类C的对象

c.test()  # 调用父类A的方法
c.demo()  # 调用父类B的方法
执行结果如下:
多继承
我是C的父类:A
我是C的父类:B

Python中的MRO——方法搜索顺序

Pyhton中针对类提供了一个内置属性 "__ mro __" ,这个属性可以查看方法的搜索顺序。

MRO是method resolution order的缩写,主要用于查看多继承时方法、属性的调用路径。

使用格式为 "print(类名.__ mro __)" 。

  • 在搜索方法时,是按照__ mro __的输出结果从左至右查找的
  • 如果在当前类中找到方法,就直接执行,不再搜索
  • 如果没有找到,就查找下一个类中是否有搜索的方法,如果找到,就直接执行,不再搜索
  • 如果找到最后一个类,还没有找到搜索的方法 ,程序会报错

在这里插入图片描述

在开发时,应该尽量避免如上图中这种容易产生混淆的情况。如果父类之间存在同名的属性或方法,应该尽量避免使用多继承。

MRO——方法搜索顺序
print('MRO——方法搜索顺序')

class A:  # 定义父类A

    def test(self):
        print('A --- test方法')

    def demo(self):
        print('A --- demo方法')


class B:  # 定义父类B

    def test(self):
        print('B --- test方法')

    def demo(self):
        print('B --- demo方法')


class C(B, A):  # 定义A、B的子类C

    pass  # 占位语句

c = C()  # 创建子类C的对象

c.test()  # 调用父类A的方法
c.demo()  # 调用父类B的方法

print(C.__mro__)  # 使用内置属性__mro__查看子类对象调用方法的顺序

'''
先查看当前类,即C类,C类中没有要调用的方法,查看下一个类
因为在定义C类时,C的父类中,B类排在A类前面,所以先查看B类
B类中有要调用的方法,因此调用B类中的方法,调用完不再搜索
'''
执行结果如下:
MRO——方法搜索顺序
B --- test方法
B --- demo方法
(<class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>)

新式类与旧式(经典)类

object是Python为所有对象提供的基类,object类中提供有一些内置的属性和方法,可以使用dir()函数查看。

  • 新式类:以object为基类的类,推荐使用
  • 经典类:不以object为基类的类,不推荐使用

在Python 3.x中定义类时,如果没有指定父类,会默认使用object作为该类的基类,即Python 3.x中定义的类都是新式类。

在Python 2.x中定义类时,如果没有指定父类,则不会以object作为基类。

新式类和经典类在多继承时,会影响到方法的搜索顺序。

为了保证编写的代码能够同时在Python 2.x和Python 3.x中运行,今后在定义类时,如果没有父类,建议统一继承自object。

格式如下:

class 类名(object):
	代码块
object的内置属性和方法
print('object的内置属性和方法:')

class A(object):

    pass

a = A()

print(dir(a))  # dir(对象名)用来查看对象可以访问和调用的属性和方法
执行结果如下:
object的内置属性和方法:
['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
4.3 多态

多态是指不同的子类对象调用相同的父类方法,会产生不同的执行结果。

  • 多态以继承和重写父类方法为前提
  • 多态可以增加代码的灵活度

多态案例1

在这里插入图片描述

多态案例1
print('多态案例1')

class Human:  # 定义父类

    def work(self):
        print('人类要辛勤工作~')


class Programmer(Human):  # 定义human的子类

    def work(self):  # 重写父类的方法
        print('程序员要编写程序!!')


class Designer(Human):  # 定义human的子类

    def work(self):  # 重写父类的方法
        print('设计师要设计作品!!')

human = Human()
human.work()  # 调用父类的work()方法

programmer = Programmer()
programmer.work()  # 调用子类Programmer的work()方法

designer = Designer()
designer.work()  # 调用子类Designer的work()方法
执行结果如下:
多态案例1
人类要辛勤工作~
程序员要编写程序!!
设计师要设计作品!!

多态案例2

需求:

  1. 在Dog类中封装game()方法
    • 普通狗只是简单的玩耍
  2. 定义XiaoTianDog类继承自Dog类,并且重写game()方法
    • 哮天犬需要在天上玩耍
  3. 定义Person类,并且封装一个game_with_dog()方法
    • 在方法内部,直接让狗对象调用game()方法

在这里插入图片描述

多态案例2
print('多态案例2')

class Dog(object):  # 定义父类Dog

    def __init__(self, name):  # 定义初始化方法,并定义name参数
        self.name = name  # 定义name属性,并把参数赋值给它

    def game(self):  # 定义game()方法
        print('%s蹦蹦跳跳的去玩耍...' % self.name)


class XiaoTianDog(Dog):  # 定义Dog的子类XiaoTianDog

    def game(self):  # 重写父类的game()方法
        print('%s飞到天上去玩耍...' % self.name)


class Person(object):  # 定义Person类

    def __init__(self, name):  # 定义初始化方法,并定义name参数
        self.name = name  # 定义name属性,并把参数赋值给它

    def game_with_dog(self, dog):  # 定义game_with_dog()方法,并定义dog参数
        print('%s和%s快乐的玩耍...' % (self.name, dog.name))

        dog.game()  # 在方法内部调用game()方法

wangcai = Dog('旺财')  # 创建Dog类对象

fly_wangcai = XiaoTianDog('飞天旺财')  # 创建XiaoTianDog类对象

xiaoming = Person('小明')  # 创建Person类对象

xiaoming.game_with_dog(wangcai)  # 调用game_with_dog()方法,并传入实参wangcai

print('---------')

xiaoming.game_with_dog(fly_wangcai)  # 调用game_with_dog()方法,并传入实参fly_wangcai
执行结果如下:
多态案例2
小明和旺财快乐的玩耍...
旺财蹦蹦跳跳的去玩耍...
---------
小明和飞天旺财快乐的玩耍...
飞天旺财飞到天上去玩耍...

5 知识拓展

5.1 类的结构
5.1.1 术语——实例
  1. 使用面向对象开发,第一步是设计类

  2. 使用 "类名()" 创建对象,创建对象的动作有两步:

    • 在内存中为对象分配空间

    • 调用初始化方法 __ init __ 为对象初始化

  3. 对象创建后,内存中就有了一个对象的实实在在的存在——实例

在这里插入图片描述

因此,通常也会把:

  1. 创建出来的对象叫做类的实例
  2. 创建对象的动作叫做实例化
  3. 对象的属性叫做实例属性
  4. 对象调用的方法叫做实例方法

在程序执行时:

  1. 对象各自拥有自己的实例属性

  2. 调用对象方法,可以通过 "self."

    • 访问自己的属性

    • 调用自己的方法

每一个对象都有自己独立的内存空间,保存各自不同的属性。

多个对象的方法,在内存中只有一份,在调用方法时,需要把对象的引用传递到方法内部。

5.1.2 类是一个特殊的对象

Python中一切皆为对象。

在Python中,类是一个特殊的对象——类对象。

在程序运行时,类同样会被加载到内存;类对象在内存中只有一份,使用一个类可以创建出很多个对象实例。

除了封装实例的属性和方法外,类对象还可以拥有自己的属性和方法——类属性类方法

可以通过 "类名." 的方式访问类的属性或调用类的方法。

在这里插入图片描述

5.2 类属性(类变量)

类属性就是在类对象中定义的属性。通常用来记录与这个类相关的特征,类属性不会用于记录具体对象的特征。

类属性(类变量)是一种在定义类的时候定义的变量,它跟实例变量的区别在于,类变量不需要实例化(即创建对象)就能直接使用。

类变量也可以在实例中被调用。

注意,实例不能修改类变量。

案例需求:

  1. 定义一个工具类
  2. 每件工具都有自己的name
  3. 需要知道使用该类创建了多少个工具对象

在这里插入图片描述

类属性
print('类属性')

class Tool(object):
    
    # 要定义类属性,在类名下方使用赋值语句就可以
    count = 0  # 使用赋值语句,定义类属性,记录创建工具对象的总数

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

        Tool.count += 1  # 让类属性的值+1

# 创建工具对象
tool1 = Tool('斧头')
tool2 = Tool('锯子')
tool3 = Tool('铁锹')

print('一共创建了 %d 个工具对象!' % Tool.count)  # 输出工具对象总数
执行结果如下:
类属性
一共创建了 3 个工具对象!

属性的获取机制

在Python中,属性的获取存在一个向上查找机制。

在这里插入图片描述

因此,访问类属性有两种方式:

  1. 类名.类属性
  2. 对象名.类属性(不推荐)

注意,如果使用 "对象名.类属性 = 值" 赋值语句,只会给对象添加一个属性(即实例属性),而不会影响到类属性的值。

5.3 类方法

类方法就是针对类对象定义的方法。在类方法内部可以直接访问类属性或调用其它的类方法。

类方法需要用装饰器 "@classmethod" 来标识,告诉解释器这是一个类方法。

类方法的第一个参数不再是self,而是cls;这个参数和实例方法中的self参数类似;由哪一个类调用的方法,方法内的cls就是哪一个类的引用。

通过 "类名." 调用类方法时,不需要传递cls参数。

在方法内部,可以通过 "cls." 访问类的属性,也可以通过 "cls." 调用其它的类方法。

定义类方法的语法格式如下:

@classmethod
def 类方法名(cls):
	代码块
类方法
print('类方法')

class Tool(object):

    count = 0  # 使用赋值语句,定义类属性,记录创建工具对象的总数

    @classmethod  # 装饰器,标识类方法
    def show_tool_count(cls):  # 定义类方法
        print('工具对象的总数为:%d' % cls.count)

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

        Tool.count += 1  # 让类属性的值+1

# 创建工具对象
tool1 = Tool('斧头')
tool2 = Tool('锯子')

Tool.show_tool_count()  # 调用类方法,cls参数不用传入实参
执行结果如下:
类方法
工具对象的总数为:2
5.4 静态方法

在开发时,如果需要在类中封装一个方法,这个方法既不需要访问实例属性或调用实例方法,也不需要访问类属性或调用类方法,这个时候,就可以把这个方法封装成一个静态方法。

静态方法需要用装饰器 "@staticmethod" 来标识,告诉解释器这是一个静态方法。

通过 "类名." 调用静态方法。

静态方法不需要有类似self的参数。

定义静态方法的语法格式如下:

@staticmethod
def 静态方法名():
	代码块
静态方法
print('静态方法')

class Dog(object):

    @staticmethod  # 装饰器,标识静态方法
    def run():  # 定义静态方法
        print('小狗在跑...')

Dog.run()  # 调用静态方法,不需要创建对象,直接调用
执行结果如下:
静态方法
小狗在跑...
posted @ 2021-01-21 00:05  南城_以南  阅读(248)  评论(0编辑  收藏  举报