面向对象的三大特性及派生和组合

发布时间 2024-01-12 21:29:15作者: 蓝幻ﹺ

面向对象的三大特性

  • 面向对象编程有三大特性:
    • 封装、继承、多态 --> 派生和组合

【一】封装

【1】什么是封装

  • 在程序设计中,封装(Encapsulation)是对具体对象的一种抽象
  • 封装就是对具体对象的一种抽象
  • 简单理解就是将不想让别人看到的东西全部藏起来

【2】为什么要封装

  • 封装数据的主要原因是:保护隐私(把不想别人知道的东西封装起来)

【3】封装的方法---隐藏属性

在类外部,调用不到,两个 _ 及以上封装起来的属性

隐藏数据属性

class A:
    name = 'aa'
    _name = 'aa_'
    __name = 'aa__'

    def run(self):
        ...

a = A()
print(a.name)  # aa
print(a._name)  # aa_
print(a.__name)  # AttributeError: 'A' object has no attribute '__name'

开放接口:修改隐藏属性age值

class Student:
    def __init__(self, age):
        self.__age = age

    # 对外提供访问接口
    def get_info(self):
        print(self.__age)

    # 对外提供设置接口
    def set_info(self, age):
        self.__age = age


student = Student(18)
student.get_info()  # 18
student.set_info(22)
student.get_info()  # 22

隐藏函数属性

  • 目的的是为了隔离复杂度
class A:
    def __init__(self, name):
        self.name = name

    def run(self):
        print(f'{self.name}在跑步')

    def _run(self):
        print(f'{self.name}在跑步')

    def __run(self):
        print(f'{self.name}在跑步')


a = A(name='qc')

a.run()  # qc在跑步
a._run()  # qc在跑步
a.__run()  # AttributeError: 'A' object has no attribute '__run'

【4】总结

  • 封装数据属性,在类内部,没有用 _ 或者用一个 _ 封装起来的属性,在类内部可以任意访问,在类外部(对象)也可以随意访问

  • 封装数据属性,在类内部,用 __ 封装起来的属性,在类内部可以任意访问,在类外部(对象)不可以访问

  • 封装函数属性,在类内部,没有用 _ 或者用一个 _ 封装起来的属性,在类内部可以任意访问,在类外部(对象)也可以随意访问

  • 封装函数属性,在类内部,用 __ 封装起来的属性,在类内部可以任意访问,在类外部(对象)不可以访问

【二】继承

【1】什么是继承

  • 继承是一种创建新类的方式,新建的类可以继承一个或多个父类(python支持多继承),父类又可称为基类或超类,新建的类称为派生类或子类。
  • 子类会“”遗传”父类的属性,从而解决代码重用问题(去掉冗余的代码)
  • python中类的继承分为:单继承和多继承

【2】单继承和多继承

# 定义父类
class A:
    pass

# 定义父类
class B:
    pass

# 单继承
class C(A):
    pass

# 多继承
class D(A, B):
    pass

# 看所有继承的父类
print(C.__bases__)
# (<class '__main__.A'>,)
print(D.__bases__)
# (<class '__main__.A'>, <class '__main__.B'>)

【3】经典类与新式类

  • 只有在python2中才分新式类和经典类,python3中统一都是新式类
  • 在python2中,没有显式的继承object类的类,以及该类的子类,都是经典类
  • 在python2中,显式地声明继承object的类,以及该类的子类,都是新式类
  • 在python3中,无论是否继承object,都默认继承object,即python3中所有类均为新式类

【4】继承与抽象(先抽象再继承)

  • 继承描述的是子类与父类之间的关系,是一种什么是什么的关系。
  • 要找出这种关系,必须先抽象再继承
  • 抽象即抽取类似或者说比较像的部分

抽象

  • 抽象分成两个层次
    • 将奥巴马和梅西这俩对象比较像的部分抽取成类;
    • 将人,猪,狗这三个类比较像的部分抽取成父类。
  • 抽象最主要的作用是划分类别(可以隔离关注点,降低复杂度)

img

继承

  • 继承:是基于抽象的结果,通过编程语言去实现它,肯定是先经历抽象这个过程,才能通过继承的方式去表达出抽象的结构。
  • 抽象只是分析和设计的过程中,一个动作或者说一种技巧,通过抽象可以得到类

img

继承的代码实现

class A:
    def eat(self):
        print(f"{self.name} 吃饭" )


class B(A):
    def __init__(self, name):
        self.name = name


a = B('cat')
a.eat()  # cat 吃饭

【5】属性查找顺序

  • 属性查找顺序

  • 继承的顺序:从上至下继承

  • 属性查找:从下至上查找

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

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

class B(A):
    def f1(self):
        print('B.f1')

b = B()

b.f2()
# A.f2
# B.f1

隐藏属性: 隐藏给当前类,除了当前类,其他继承的子类均不能找到我隐藏的属性

class A:
    def __f1(self):
        print('A.f1')

    def f2(self):
        print('A.f2')
        self.__f1()

class B(A):
    def __f1(self):
        print('B.f1')


b = B()

b.f2()
# A.f2
# A.f1

【6】继承实现的原理

(一)非菱形结构继承顺序

  • 在Java和C#中子类只能继承一个父类,而Python中子类可以同时继承多个父类,如A(B,C,D)
  • 如果继承关系为非菱形结构,则会按照先找B这一条分支,然后再找C这一条分支,最后找D这一条分支的顺序直到找到我们想要的属性

(二)菱形结构继承顺序

  • 如果继承关系为菱形结构,那么属性的查找方式有两种,分别是:深度优先和广度优先
  • python3中统一都是新式类
  • pyhon2中才分新式类与经典类
深度优先
  • 当类是经典类时,多继承情况下,在查找属性不存在时,会按照深度优先的方式查找下去

img

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

img

代码
class A(object):
    def test(self):
        print('A')


class B(A):
    def test(self):
        print('B')


class C(A):
    def test(self):
        print('C')


class D(B):
    def test(self):
        print('D')


class E(C):
    def test(self):
        print('E')


class F(D, E):
    pass


class G(F, D, E):
    pass


f1 = F()
f1.test()  # D
print(F.__mro__)
# (<class '__main__.F'>, <class '__main__.D'>, <class '__main__.B'>, <class '__main__.E'>, <class '__main__.C'>, <class '__main__.A'>, <class 'object'>)

【7】继承的延伸 : 派生

(一)什么是派生

派生是指,子类继承父类,派生出自己的属性与方法,并且重用父类的属性与方法

(二)派生的使用

  • 指名道姓的调用某一个类的函数
class A:
    def __init__(self, name):
        self.name = name

class B(A):
    def __init__(self, name, age):
        # 调用的是函数,因而需要传入self
        A.__init__(self, name)
        self.age = age

obj = B('cat', 18)
print(obj.name, obj.age)  # cat 18
  • 超类 super()
    • 调用super()会得到一个特殊的对象
    • 该对象专门用来引用父类的属性
    • 且严格按照MRO规定的顺序向后查找
class A:
    def __init__(self, name):
        self.name = name


class B(A):
    def __init__(self, name, age):
        # 调用的是绑定方法,因此会自动传入self,但是需要传入相应的参数
        super().__init__(name)
        self.age = age

obj = B('cat', 18)
print(obj.name, obj.age)  # cat 18

(三)总结

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

【8】 组合

(一)什么是组合

  • 在一个类中以另外一个类的对象作为数据属性,称为类的组合。

组合与继承都是用来解决代码的重用性问题。

不同的是:

  • 继承是一种“是”的关系,比如老师是人、学生是人,当类之间有很多相同的之处,应该使用继承;
  • 而组合则是一种“有”的关系,比如老师有生日,老师有多门课程,当类之间有显著不同,并且较小的类是较大的类所需要的组件时,应该使用组合

(二)组合的使用

class Course:
    def __init__(self, name, period, price):
        self.name = name
        self.period = period
        self.price = price

    def tell_info(self):
        print(f'当前课程名字 {self.name}')


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):
        super().__init__(name, age, sex)
        # 实例化后,往该列表中添加Course类的对象
        self.courses = []

    def teach(self):
        print(f'当前老师正在授课 {self.name}')


python = Course('python', '5', 300)
linux = Course('go', '20', 500)
teacher = Teacher('qc', 'male', 20)

# teacher有两门课程
teacher.courses.append(python)
teacher.courses.append(linux)

# 重用Course类的功能
for obj in teacher.courses:
    obj.tell_info()
# 当前课程名字 python
# 当前课程名字 go

(三)总结

​ 组合指的是将不同的类或对象组合在一起以创建更复杂的对象或数据结构。在组合中,一个类可以包含其他类的实例作为其属性,从而实现了对象之间的关联和协作。这种关联关系可以用于构建更复杂的系统,同时保持代码的模块化和可维护性。

【9】组合和继承的区别

组合与继承都是有效地利用已有类的资源的重要方式。但是二者的概念和使用场景皆不同。

(一)继承的方式

  • 通过继承建立了派生类与基类之间的关系,它是一种'是'的关系,比如白马是马,人是动物。
  • 当类之间有很多相同的功能,提取这些共同的功能做成基类,用继承比较好,比如老师是人,学生是人

(二)组合的方式

  • 用组合的方式建立了类与组合的类之间的关系,它是一种‘有’的关系,比如教授有生日,教授教python和linux课程,教授有学生s1、s2、s3...

【三】多态

【1】什么是多态

  • 多态指的是一类事物有多种形态

  • 比如动物,猪狗牛羊猴

import abc


# 同一类事物:动物
class Animal(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def talk(self):
        pass

# 动物的形态之一:人
class People(Animal):
    def talk(self):
        print('你真帅')

# 动物的形态之二:狗
class Dog(Animal):
    def talk(self):
        print('汪汪汪')
  • 文件有多种形态:文本文件,可执行文件
import abc


# 同一类事物:文件
class File(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def click(self):
        pass

# 文件的形态之一:文本文件
class Text(File):  
    def click(self):
        print('open file')

# 文件的形态之二:可执行文件
class ExeFile(File):  
    def click(self):
        print('execute file')

【2】多态动态绑定(多态性)

  • 多态动态绑定在继承的背景下使用时,有时也称为多态性
  • 多态性是指在不考虑实例类型的情况下使用实例

静态多态性

  • 如任何类型都可以用运算符 + 进行运算

动态多态性

import abc

# 同一类事物:动物
class Animal(metaclass=abc.ABCMeta):
    @abc.abstractmethod
    def talk(self):
        pass

# 动物的形态之一:人
class People(Animal):
    def talk(self):
        print('你真帅')

# 动物的形态之二:狗
class Dog(Animal):
    def talk(self):
        print('汪汪汪')
p = People()
dog = Dog()
p.talk()  # 你真帅
dog.talk()  # 汪汪汪

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

def func(obj):
    obj.talk()

func(p)  # 你真帅
func(dog)  # 汪汪汪

【3】多态性的好处

  • 增加程序的灵活性和可扩展性

【4】鸭子类型

  • 鸭子类型是一种编程风格,决定一个对象是否有正确的接口
  • 如果它看起来像鸭子,像鸭子一样嘎嘎叫,那么它一定是鸭子。
class NormalDuck():
    def eat(self):
        print(f"正常鸭子可以吃饭")

    def walk(self):
        print(f"正常鸭子可以走路")


class RockDuck():
    def eat(self):
        print(f"肉鸭子可以吃饭")

    def walk(self):
        print(f"肉鸭子可以走路")