面向对象

罗大富 BigRich大约 14 分钟Python

之前章节中介绍的示例,所使用的数据和函数之间是没有任何直接联系的,他们之间的联系就是通过函数调用提供参数的形式将数据传入函数进行处理,这种方式也被称为面向过程编程。这样会有一个问题,经常可能因为错误的传递参数,错误地修改了数据而导致程序出错,甚至崩溃。维护时要从程序提供的一堆数据中去找,要拓展函数的功能,只能重新建立一个函数或修改它,开发效率会相对比较低。于是,面向对象编程的概念就诞生了。

1. 概述

面向对象编程是在面向过程编程的基础上发展来的,它比面向过程编程具有更强的灵活性和扩展性。面向对象编程是程序员发展的分水岭,很多初学者会因无法理解面向对象而放弃学习编程。

面向对象就是实际的事物模型或计算对象的模型,在程序中以类方式进行定义。类从某种意义上来说仍旧是对现实世界的模拟,他模拟的是现实世界中的各种事物,而现实世界中的各种事物都是有类别的,比如猫,狗等名词所指的都是一类事物。在程序中,定义的类就代表着同一类别的模型。

面向对象编程可以理解为一种封装代码的方法,在前面的章节中,我们已经接触了封装,比如说,将乱七八糟的数据扔进列表中,这就是一种简单的封装,是数据层面的封装;把常用的代码块打包成一个函数,这也是一种封装,是语句层面的封装。面向对象相比于前两种封装更先进,它可以更好地模拟真实世界的事物(将其视为对象),并把描述特征的数据和代码块(函数)封装到一起。

举个例子,如果想要设计一只狗的对象,你需要首先思考一下,狗具有什么特征?

  1. 从表面特征来看,比如狗的毛发颜色,体重,名字等等。
  2. 从所具有的行为来描述,例如,狗会汪汪叫,会吃饭,会睡觉等等。

如果把狗的信息用代码来表示,那么,狗的表面特征可以用变量表示,行为特征可以用函数来表示。示例:

# 表面特征
color = '白'
weight = 10
name = '旺财'

# 行为特征
def eat():
    print('狗会吃东西')

def sleep():
    print('狗会睡觉')

而对象可以把表面特征与行为特征建立起联系,因此,相比于只用变量和函数,类可以更好地模拟现实生活中的事物。

2. 定义和使用类

在 Python 中定义类的基本形式为:

class '类名'('父类名')pass
  • class 是定义类的关键字;
  • 类名处使用符合规范的名称;
  • 父类名是指该类继承的父类名称,如果没有可以连同括号都不写;
  • pass 表示空语句,什么也不做,常用来预留语句位置或临时未写等待以后完成。

我们继续拿小狗举例:

# 声明一个小狗类
class Dog:
	pass

类在定义后必须先实例化才能使用,类的实例化跟函数调用类似,只要使用类名加圆括号的形式就可以实例化一个类。

类实例化以后会生成该类的一个实例,一个类可以实例化多个实例,实例与实例之间并不相互影响,类实例化以后就可以直接使用了。

举个例子,我的小狗就是狗这个大类的一个实例,因此我们需要先用 Dog 类来实例化一个实例来代表我的小狗:

# 声明一个小狗类
class Dog:
	pass

# 将自定义类 Dog 实例化,命名该实例为 my_dog
my_dog = Dog()
print(my_dog)

3. 类的方法与属性

上一节中,我们学习了如何声明和实例化一个类,但是,单纯这么做是没有什么实际价值的。要用类来解决实际问题,就要定义一个具有一些属性和方法的类,因为这样才符合真实世界中的事物特征。

3.1. 属性

为了区分类中的变量与全局变量,将类中的变量称为属性

Python 中的类的属性有两种:

  • 实例属性,即同一个类的不同实例的属性,他们的值是不会互相影响的,定义时使用 实例名.属性名
  • 类属性,是所有同一个类的实例共有的,直接在类中独立定义,引用时要使用 类名.属性名 的形式。

为了方便理解,我们继续拿之前的狗类的表象特征来演示:

# 声明一个小狗类,此处声明的属性,为所有小狗实例对象共有的属性
class Dog:
    # 所有狗都具有的特征
    legs = 4  # 四条腿
    eyes = 2  # 两只眼睛


# 实例化两个对象
my_dog = Dog()
your_dog = Dog()

print('===========获取狗类的公共属性============')
print(my_dog.legs, my_dog.eyes)
print(your_dog.legs, your_dog.eyes)

# 添加实例属性
# my_dog 与 your_doy 虽然都属于狗类,但是他们都有自己的特征
my_dog.weight = 10  # 我的狗重 10kg
my_dog.color = '白色' # 我的狗是白色
my_dog.name = '旺财'  # 我的狗叫旺财

your_dog.weight = 5 # 你的狗重 5kg
your_dog.color = '黑色'   # 你的狗是黑色
your_dog.age = 4    # 你的狗今年四岁

print('=======打印不同实例的实例属性=========')
print(my_dog.weight, my_dog.color, my_dog.name)
print(your_dog.weight, your_dog.color, your_dog.age)
# 这里如果你想要打印 my_dog.age 或者 your_dog.name 都会报错

# 修改类属性
# 所有的狗突然一夜之间进化了,前肢进化为了手臂,以后只靠双腿走路
Dog.legs = 2

print('==========打印修改后的类的公共属性===========')
print(my_dog.legs)
print(your_dog.legs)

# 但是我的狗腿瘸了,只剩一条腿, 我的狗腿瘸了,不代表所有的狗腿都瘸了,因此,我在修改实例时,不会影响其他实例的属性
my_dog.legs = 1
print('========在实例中用实例属性替代类属性,不影响其他实例========')
print(my_dog.legs)
print(your_dog.legs)
print(Dog.legs)

3.2. 类的方法

为了区分在类中定义的函数和类外定义的全局函数,将类中定义的函数称为方法

类中的方法定义与函数基本相同,区别有:

  • 方法中的第一个参数必须是 self, 而且不能省略;
  • 方法的调用需要实例化类,并以实例名.方法名形式调用;
  • 整体进行一个单位的缩进,表示其属于类中的内容。

我们继续用狗举例,定义狗的吃饭和睡觉方法:

# 给小狗上了基因锁,从此狗再也不可能直立行走,必须靠 4 条腿走路,这样,legs 就是不可修改的。
class Dog:

    def sleep(self):
        print('睡觉')

    def eat(self, food):
        print(f'吃{food}')

# 实例化
my_dog = Dog()

# 调用 sleep 方法
my_dog.sleep()
# 调用 eat 方法并传参
my_dog.eat('骨头')

注意:定义方法时,也可以像定义函数一样声明各种形式的参数;方法调用时,不用提供 self 参数。

在 Python 中的类定义中,可以定义一个特殊的构造方法,即 __init__() 方法,用于类实例化时,初始化相关数据,如果在这个方法中有相关参数,则实例化时就必须提供。

我们拿小狗来举例,我们要求每一条小狗都要有自己的名字,然后我们创建一个方法去调用名字属性。

# 给小狗上了基因锁,从此狗再也不可能直立行走,必须靠 4 条腿走路,这样,legs 就是不可修改的。
class Dog:

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

    def sleep(self):
        print('睡觉')

    def eat(self, food):
        print(f'吃{food}')

    # 这条狗叫什么名字
    def speak(self):
        print(f'这条狗的名字叫{self.name}')

# 用关键字传参
my_dog = Dog(name='旺财')
# 位置传参
your_dog = Dog('小黑')

my_dog.speak()
your_dog.speak()

如果我们在实例化的时候没有填写参数,例如:

his_dog = Dog()

运行结果为:

TypeError: __init__() missing 1 required positional argument: 'name'

还有要注意的一点,属性名和方法名不能重复。

4. 面向对象的三大特征

面向对象编程的三大特性:继承、封装、多态。

  1. 多态 多态是指同一个方法调用由于对象不同会产生不同的行为。生活中这样的例子比比皆是:同样是狗,但是我家养的是哈士奇,你家养的柴犬,他家养的萨摩耶。
  2. 封装 隐藏对象的属性和实现细节,只对外提供必要的方法。 通过私有属性、私有方法的方式,实现封装。Python 追求简洁的语法
  3. 继承 继承可以让子类具有父类的特性,提高了代码的重用性。从设计上是一种增量进化,原有父类设计不变的情况下,可以增加新的功能,或者改进已有的算法。

4.1. 多态

在我们前面几节的内容里,我们已经接触过多态的概念了,比如在我们学习类的属性的时候,我们实例化我的小狗和你的小狗,他俩有不同的名字,体重,名字和年龄。这些都是不同的实例的属性,包括方法也一样。

# 声明一个小狗类,此处声明的属性,为所有小狗实例对象共有的属性
class Dog:
    # 所有狗都具有的特征
    legs = 4  # 四条腿
    eyes = 2  # 两只眼睛


# 实例化两个对象
my_dog = Dog()
your_dog = Dog()

print('===========获取狗类的公共属性============')
print(my_dog.legs, my_dog.eyes)
print(your_dog.legs, your_dog.eyes)

# 添加实例属性
# my_dog 与 your_doy 虽然都属于狗类,但是他们都有自己的特征
my_dog.weight = 10  # 我的狗重 10kg
my_dog.color = '白色' # 我的狗是白色
my_dog.name = '旺财'  # 我的狗叫旺财

your_dog.weight = 5 # 你的狗重 5kg
your_dog.color = '黑色'   # 你的狗是黑色
your_dog.age = 4    # 你的狗今年四岁

print('=======打印不同实例的实例属性=========')
print(my_dog.weight, my_dog.color, my_dog.name)
print(your_dog.weight, your_dog.color, your_dog.age)
# 这里如果你想要打印 my_dog.age 或者 your_dog.name 都会报错

# 修改类属性
# 所有的狗突然一夜之间进化了,前肢进化为了手臂,以后只靠双腿走路
Dog.legs = 2

print('==========打印修改后的类的公共属性===========')
print(my_dog.legs)
print(your_dog.legs)

# 但是我的狗腿瘸了,只剩一条腿, 我的狗腿瘸了,不代表所有的狗腿都瘸了,因此,我在修改实例时,不会影响其他实例的属性
my_dog.legs = 1
print('========在实例中用实例属性替代类属性,不影响其他实例========')
print(my_dog.legs)
print(your_dog.legs)
print(Dog.legs)

4.2. 封装

封装性是面向对象重要的基本特性之一。封装隐藏了对象的内部细节,只保留有限的对外接口,外部调用者不用关心对象的内部细节,使得操作对象变得简单。

例如:一台计算机内部及其复杂,有主板,CPU,硬盘,内存等,而一般人不需要了解它的内部细节。计算机制造商用机箱把计算机封装起来,对外提供了一些接口,如鼠标,键盘,和显示器等,使用计算机就变得非常简单了。

私有属性

为了防止外部调用者随意存取类的内部数据(成员变量),内部数据(成员变量)会被封装成为“私有变量”,外部调用者只能通过方法调用私有变量。

默认情况下,Python 中的变量是公有的,可以在类的外部访问它们。如果想让它们成为私有变量,则在变量前加上双下划线(__)即可。

例如,

# 我给狗类上了基因锁,狗永远只能 4 条腿走路
class Dog():
    # 设置私有属性
    __legs = 4
    
    # 内部获取狗腿数量
    def print_legs_count(self):
        print(f'狗有{self.__legs}条腿')

# 实例化我的小狗
my_dog = Dog()
my_dog.print_legs_count()	# 在内部可以正常调用
print(my_dog.__legs)		# 在外部调用时,无法获取该属性,抛出异常

注意: 私有变量可以在类的内部进行访问,不能在类的外部进行访问

私有方法

私有方法与私有变量的封装是类似的,在方法前面加上双下划线(__)就是私有方法了。

例如:

# 为了限制狗的进化,我们给狗类上了基因锁,狗永远只能 4 条腿走路
class Dog():
    # 设置私有属性
    __legs = 4
    
    # 设置私有方法,内部获取狗腿数量
    def __print_legs_inner(self):
        print(f'狗有{self.__legs}条腿')
    
    def print_legs_out(self):
        self.__print_legs_inner()

# 实例化我的小狗
my_dog = Dog()
my_dog.print_legs_out()			# 正常运行
my_dog.__print_legs_inner()		# 抛出异常

为了实现对象的封装,在一个类中不应该有公有的成员变量,这些成员变量应该都被设计成为私有的,然后通过公有的 set(赋值)和 get(取值)方法来访问。

class Dog():
    # 设置私有属性
    __legs = 4

    # get 方法
    def get_legs(self):
        return self.__legs

    # set 方法
    def set_legs(self, legs):
        self.__legs = legs


# 实例化一个对象
my_dog = Dog()
print(my_dog.get_legs())
my_dog.set_legs(2)
print(my_dog.get_legs())        

还有一种进阶的方法来访问私有变量, 通过 @property@属性名.setter 装饰器来完成。装饰器是函数中的一个进阶概念,我们在入门阶段,先不详细讲。

示例:

class Dog():
    # 设置私有属性
    __legs = 4

    # 内部获取狗腿数量
    def print_legs_count(self):
        print(f'狗有{self.__legs}条腿')

    # 替代 get_legs
    @property
    def legs(self):
        return self.__legs

    # 替代 set_legs
    @legs.setter
    def legs(self, legs):
        self.__legs = legs


# 实例化一个对象
my_dog = Dog()
my_dog.print_legs_count()
my_dog.legs = 2
my_dog.print_legs_count()

4.3. 继承

在现实世界中的继承关系无处不在,例如:猫与动物之间的关系:猫是一种特殊动物,具有动物的全部特征和行为,即数据和操作。在面向对象中动物是一般类,被称为父类,猫是特殊类,被称为子类。特殊类拥有一般类的全部数据和操作,可称子类继承父类。

在 Python 中声明子类继承父类的语法很简单,定义类时在类的后面使用一对小括号指定它的父类就可以了,在 Python 中一般类都继承 object。

单继承

语法格式:

class '父类名':
	pass

class '子类名'('父类名'):
	pass

示例:

# 定义动物类
class Animal(object):
    def __init__(self, name):
        self.name = name

    def print_info(self):
        print(f'动物的名字叫:{self.name}')


# 定义猫类使其继承动物类
class Cat(Animal):
    def __init__(self, name, age):
        Animal.__init__(self, name)  # 调用父类的构造方法
        self.age = age


cat = Cat('汤姆', 3)
cat.print_info()  # 父类的方法被子类继承,子类对象可调用

在调用父类的构造方法时,我们还有一种写法,那就是使用 super() 函数

super() 函数,它会使子类从其父继承所有方法和属性:

class Cat(Animal):
    def __init__(self,name,age):
        super.__init__(name)   # 调用父类的构造方法
        self.age = age

这种方法与用父类名调用的方法效果是一样的。

多继承

一个类继承多个父类。在多继承中 如果多个父类中属性名 或者是方法名相同 那么将按照MRO算法查找

MRO:

  1. 在自己的类中查找 如果找到 就结束
  2. 在父类元组中按照顺序查找 从左到右

所有在Python中,当子类继承多个父类时,如果在多个父类有相同的成员方法和成员变量,则子类优先继续左边父类中的成员方法或成员变量,从左到右继承级别从高到低。

语法格式:

class A(Object):
	pass
 
class B(object):
	pass
 
class C(A,B):
	pass

示例:

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

    def show_info(self):
        print(f'狗的名字叫{self.name}')

    def run(self):
        print('狗跑的很快')


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

    def show_info(self):
        print(f'哈士奇的名字叫{self.name}')

    def run(self):
        print('哈士奇跑的很慢')

    def sofa(self):
        print('咬沙发')


class MyDog(Dog, Husky):
    def __init__(self, name, age):
        super().__init__(name)
        self.age = age


d = MyDog('二哈', 2)
d.run()  # 继承父类狗方法
d.sofa()  # 继承父类哈士奇方法
d.show_info()  # 继承父类哈士奇方法

运行结果为:

狗跑的很快
咬沙发
狗的名字叫二哈

方法重写(重载)

如果子类的方法名与父类的方法名相同,则在这种情况下,子类的方法会重写父类的同名方法。

示例:

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

    def show_info(self):
        print(f'狗的名字叫{self.name}')

    def run(self):
        print('狗跑的很快')


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

    def show_info(self):
        print(f'哈士奇的名字叫{self.name}')

    def run(self):
        print('哈士奇跑的很慢')

    def sofa(self):
        print('咬沙发')


# 在 MyDog 类中添加 show_info() 方法
class MyDog(Dog, Husky):
    def __init__(self, name, age):
        super().__init__(name)
        self.age = age

    # 重写 show_info() 方法
    def show_info(self):
        print(f'我家的狗叫{self.name}')


d = MyDog('二哈', 2)
d.run()  # 继承父类狗方法
d.sofa()  # 继承父类哈士奇方法
d.show_info()  # 重写父类方法方法
上次编辑于:
贡献者: Luo