Python面向对象

面向对象(OOP)

面向对象编程- Object Oriented Programming 简写 OOP

1. 面向对象基本概念

  • 我们之前学习的编程方式就是 面向过程
  • 面向过程面向对象,是两种不同的 编程方式
  • 对比 面向过程 的特点,可以更好的了解什么是 面向对象

1.1 过程和函数(科普)

  • 过程 是早期的一个编程概念
  • 过程 类似于函数,只能执行,但是没有返回值
  • 函数 不仅能执行,还可以返回结果

1.2 面向过程和面向对象的基本概念

  1. 面向过程-怎么做?

    1. 把完成某一个需求的 所有步骤 从头到尾 逐步实现
  2. 根据开发需求,将某些 功能独立 的代码 封装 成一个又一个 函数

    1. 最后完成的代码,就是顺序地调用 不同的函数

      特点

    • 注重 步骤与过程,不注重职责分工

    • 如果需求复杂,代码会变得很复杂

    • 开发复杂项目,没有固定的套路,开发难度很大

  3. 面向对象-谁来做?

    1. 在完成某一个需求前,首先确定 职责-要做的事情(方法)

    2. 根据 职责 确定不同的 对象,在 对象 内部封装不同的 方法 (多个)

    3. 最后完成的代码,就是顺序地让 不同的对象 调用 不同的方法

      特点

    • 注重 对象和职责,不同的对象承担不同的职责

    • 更加适合应对复杂的需求变化,是专门应对复杂项目开发,提供的固定套路

    • 需要在面向过程基础上,再学习一些面向对象的语法

      相比较函数,面向对象是更大的封装 ,根据 职责 在 一个对象中封装多个方法

2. 类和对象

2.1 类和对象的概念

对象面向对象编程的两个核心概念

    • 是对一群具有 相同特征 或者 行为 的事物的一个统称,是抽象的,不能直接使用

      • 特征 被称为 属性
      • 行为 被称为 方法
    • 就相当于制造飞机时的 图纸 ,是一个 模板,是 负责创建对象的

  1. 对象

    • 对象由类创建出来的一个具体存在,可以直接使用

    • 哪一个类 创建出来的 对象,就拥有在 哪一个类 中定义的:

      • 属性
      • 方法
    • 对象 就相当于用 图纸 制造 的飞机

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

2.2 类和对象的关系

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

  • 只有一个,而 对象 可以有很多个

    • 不同的对象 之间 属性 可能会各不相同
  • 中定义了什么 属性和方法对象 中就有什么属性和方法,不可能多,也不可能少

2.3 类的设计

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

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

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

驼峰命名法

例:CapWords

  1. 每一个单词的首字母大写
  2. 单词与单词之间没有下划线

2.4 类名的确定

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

2.5 属性和方法的确定

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

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

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

3.面向对象基础语法

3.1 dir内置函数(拓展)

  • Python对象几乎是无所不在的,我们之前学习的 变量,函数 都是对象

Python 中可以使用以下两个方法验证:

  1. 标识符/数据 后输入一个 . ,然后按下 TAB 键, iPython 会提示该对象能够调用的 方法列表
  2. 使用内置函数 dir 传入 标识符/数据,可以查看对象内的 所有属性及方法

提示 __方法名__ 格式的方法是 Python 提供的 内置方法/属性,下面是一些常用的内置方法/属性:

序号 方法名 类型 作用
01 _new_ 方法 创建对象时,会被自动调用
02 _init_ 方法 对象被初始化时,会被自动调用
03 _del_ 方法 对象被从内存中销毁前,会被自动调用
04 _str_ 方法 返回对象的描述信息print 函数输出使用

提示 利用好 dir() 函数,在学习时很多内容不需要死记硬背了

3.2 定义简单的类(包含方法)

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

  1. 定义只包含方法的类

    • Python 中要定义一个包含方法的类,语法格式如下:

        class 类名:
      
            def 方法1(self, 参数):
                pass
      
            def 方法2(self, 参数):
                pass
    • 方法 的定义格式和之前学习过的 函数 几乎一样

    • 区别在于第一个参数必须是 self

      注意: 类名的命名规则要符合驼峰命名法

  1. 创建对象

    • 当一个类定义完成之后,要使用这个类来创建对象,语法格式如下:

        对象变量 = 类名()
  1. 设计一个面向对象程序

    需求

    • 小猫爱吃鱼,小猫要喝水

    分析

    1. 定义一个猫类 Cat

    2. 定义两个方法 eatdrink

    3. 按照需求-不需要定义属性

      class Cat:
       '''这是一个猫类'''
      
       def eat(self):
           print('小猫爱吃鱼')
      
       def drink(self):
           print('小猫在喝水')
      
      tom = Cat()
      tom.drink()
      tom.eat()

    引用概念的强调

    在面向对象开发中,引用 的概念是同样适用的

    • Python 中使用类 创建对象之后tom 变量中仍然记录的是 对象在内存中的内存地址
  • 也就是 tom 变量 引用新建的猫对象

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

      提示:在计算机中,通常使用 十六进制 表示 内存地址

      • 十进制十六进制 都是用来表达数字的,只是表示的方式不一样
      • 十进制十六进制 的数字之间可以来回转换
    • %d 可以以 10进制 输出数字

    • %s 可以以 16进制 输出数字

  1. 方法中的 self 参数

    1. 案例改造-给对象增加属性

      • Python 中,要 给对象设置属性,非常的容易,但是不推荐使用

        • 因为对象属性的封装应该封装在类的内部
      • 只需要在 类的外部的代码 中直接通过,设置一个属性即可

        注意:这种方式虽然简单,但是不推荐使用

        tom.name = 'Tom'
        ...
        lazy_cat.name = '大懒猫'
    2. 使用 self 在方法内部输出每一只猫的名字

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

      • 在类封装的方法内部,self 就表示 当前调用方法的对象自己
    • 调用方法时,程序员不需要传递 self 参数

    • 在方法内部 也可以通过 self.访问对象的属性

4. 初始化方法

4.1 之前代码存在的问题-在类的外部给对象增加属性

  • 将案例代码进行调整,先调用方法再设置属性,观察一下执行结果

      tom = Cat()
      tom.drink()
      tom.eat()
      tom.name = 'Tom'
      print(tom)
  • 程序执行报错如下:

      AttributeError: 'Cat' object has no attribute 'name'
    
      属性错误: ‘Cat’对象没有‘name’属性

    提示

    • 在日常开发中,不推荐在 类的外部 给对象增加属性

      • 如果 在日常运行时,没有找到属性,程序会报错
    • 对象应该包含有哪些属性,应该 封装在类的内部

4.2 初始化方法

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

    1. 为对象在内存中 分配空间-创建对象
    2. 为对象的属性 设置初始值-初始化方法(init)
  • 这个 初始化方法 就是 __init__ 方法,__init__ 是对象的 内置方法

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

    Cat 中增加 __init__ 方法,验证该方法在创建对象时会被自动调用

      class Cat:
          '''这是一个猫类'''
    
          def __init__(self):
              print('初始化方法')

4.3 在初始化方法内部定义属性

  • __init__ 方法内部使用 self.属性名 = 属性的初始值 就可以 定义属性

  • 定义属性之后,再使用 Cat 类创建对象,都会拥有该属性

      class Cat:
    
          def __init__(self):
    
              print('这是一个初始化方法')
    
              # 定义用 Cat 类创建的猫对象都有一个 name 的属性
              self.name = 'Tom'
    
          def eat(self):
              print('{} 爱吃鱼'.format(self.name))
    
      # 使用类名()创建对象的时候,会自动使用初始化方法 __init__
      tom = Cat()
    
      tom.eat()

4.4 改造初始化方法-初始化的同时设置初始值

  • 在开发中,如果希望在 创建对象的同时,就设置对象的属性,可以对 __init__ 方法进行 改造

    1. 把希望设置的属性值,定义成 __init__ 方法的参数

    2. 在方法内部使用 self.属性 = 行参 接收外部传递的参数

    3. 在创建对象时,使用 类名(属性1,属性2...) 调用

      class Cat:
      
       def __init__(self, name):
           print('初始化方法 {}'.format(name))
           self.name = name
       ...
      
      tom = Cat('Tom')
      ...
      lazy_cat = Cat('大懒猫')
      ...

5.内置方法和属性

序号 方法名 类型 作用
01 _del_ 方法 对象被从内存中销毁前,会被 自动 调用
02 _str_ 方法 返回 对象的描述信息,print 函数输出使用

5.1 _del_ 方法(知道)

  • Python

    • 当使用 类名() 创建对象时,为对象 分配完空间 后,自动 调用 __init__ 方法
    • 当一个 对象被从内存中销毁 前,会 自动 调用 __del__ 方法
  • 应用场景

    • __init__ 改造初始化方法,可以让创建对象更加灵活
    • __del__ 如果希望在对象被销毁前,再做一些事情,可以考虑一下 __del__ 方法
  • 生命周期

    • 一个对象从调用 类名() 创建,生命周期开始
    • 一个对象的 __del__ 方法一旦被调用,生命周期结束
    • 在对象的生命周期内,可以访问对象属性,或者让对象调用方法

5.2 _str_ 方法

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

  • 如果在开发中,希望使用 print 输出 对象变量 时,能够打印 自定义的内容,就可以利用 __str__ 这个内置方法了

    注意:__str__ 方法必须返回一个字符串

      class Cat:
    
          def __init__(self, new_name):
    
              self.name = new_name
              print('{} 来了'.format(self.name))
    
          def __del__(self):
    
              print('{} 去了'.format(self.name))
    
          def __str__(self):
              return '我是小猫 {}'.format(self.name)
    
      tom = Cat('Tom')
      print(tom)

6. 类的结构

6.1 术语-实例

  1. 在使用面向对象开发,第一步是设计类
  2. 使用 类名() 创建对象,创建对象 的动作有两步:
    1. 在内存中为对象 分配空间
    2. 调用初始化方法 __init__对象初始化
  3. 对象创建后,内存 中就有了一个对象的 实实在在 的存在-实例

因此,通常也会把:

  1. 创建出来的 对象 叫做 实例

  2. 创建对象的 动作 叫做 实例化

  3. 对象的属性 叫做 实例属性

  4. 对象调用的方法 叫做 实例方法

在程序执行时:

  1. 对象各自拥有自己的 实例属性
  2. 调用对象方法,可以通过 self
    • 访问自己的属性
    • 调用自己的方法

结论

  • 每一个对象 都有自己 独立的内存空间,保存各自不同的属性
  • 多个对象的方法,在内存中只有一份,在调用方法时,需要把对象的引用 传递到方法内部

6.2 类是一个特殊的对象

Python一切皆对象

  • class AAA:定义的类属于 *类对象***
  • obj1 = AAA() 属于 实例对象
  • 在程序运行时, 同样 会被加载到内存

  • Python 中, 是一个特殊的对象-类对象

  • 在程序运行时,类对象 在内存中 只有一份使用一个类,可以创建出 很多个对象实例

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

    1. 类属性
    2. 类方法
  • 通过 类名 的方式可以 访问类的属性 或者 调用类的方法

6.3 类属性和实例属性

  1. 概念和使用

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

    示例需求

    • 定义一个 工具类

    • 每件工具都有自己的 name

    • 需求-知道使用这个类,创建了多少个工具对象

      class Tool(object):
      
        # 使用赋值语句,定义类属性,记录创建工具对象的总数
        count = 0
      
        def __init__(self):
            self.name = name
      
            # 针对类属性做一个计数+1
            Tool.count += 1
      
      # 创建工具对象
      tool1 = Tool('斧头')
      tool2 = Tool('榔头')
      tool3 = Tool('铁锹')
      
      # 知道使用 Tool 类到底创建了多少个对象?
      print('现在创建了{}个工具'.format(Tool.count))
  2. 属性的获取机制(科普)

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

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

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

    注意

    • 如果使用 对象.类属性 = 值 赋值语句,只会 给对象添加一个属性,而不会影响到 类属性的值

6.4 类方法和静态方法

  1. 类方法

    • 类属性 就是针对 类对象 定义的属性

      • 使用 赋值语句class 关键字下方可以定义 类属性
      • 类属性 用于记录 与这个类相关 的特征
    • 类方法 就是针对 类对象 定义的方法

      • 类方法 内部可以直接访问 类属性 或者调用其他的 类方法

    语法如下

     @classmethod
     def 类方法名(cls):
         pass
    • 类方法需要用 修饰器 @classmethod 来标识,告诉解释器这是一个类方法

    • 类方法的 第一个参数 应该是 cls

      • 哪一个类 调用的方法,方法内的 cls 就是 哪一个类的引用
      • 这个参数和 实例方法 的第一个参数是 self 类似
      • 提示 使用其他名称也可以,不过习惯使用 cls
    • 通过 类名,调用 类方法调用方法时,不需要传递 cls 参数

    • 在方法内部

      • 可以通过 cls访问类的属性
      • 也可以通过 cls调用其他的类方法

    示例需求

    • 定义一个 工具类

    • 每件工具都有自己的 name

    • 需求-在 封装一个 show_tool_count 的类方法,输出使用当前这个类,创建的对象个数

      @classmethod
      def show_tool_count(cls):
        '''显示工具对象的总数'''
        print('工具对象的总数:{}'.format(cls.count))

      在类方法内部,可以直接使用 cls 访问 *类属性** 或者 调用类方法*

  2. 静态方法

    • 在开发时,如果需要在 中封装一个方法,这个方法:

      • 不需要 访问 实例属性 或者调用 实例方法
      • 不需要 访问 类属性 或者调用 类方法
    • 这个时候,可以把这个方法封装成一个 静态方法

    语法如下

     @staticmethod
     def 静态方法():
         pass
    • 静态方法 需要用 修饰器 @staticmethod 来标识,告诉解释器这是一个静态方法

    • 通过 类名,调用 静态方法

      class Dog(object):
      
        # 狗对象计数
        dog_count = 0
      
        @staticmethod
        def run():
      
            # 不需要访问实例属性也不需要访问类属性的方法
            print('狗在跑...')
      
        def __init__(self, name):
            self.name = name
  3. 方法综合案例

    需求

    1. 设计一个 Game

    2. 属性:

      • 定义一个 类属性 top_score 记录着游戏的历史最高分
      • 定义一个 实例属性 player_name 记录 当前游戏的玩家姓名
    3. 方法:

      • 静态方法 show_help 显示游戏帮助信息
      • 类方法 show_top_score 显示历史最高分
      • 实例方法 start_game 开始当前玩家的游戏
    4. 主程序步骤

      1. 查看帮助信息
      2. 查看历史最高分
      3. 创建游戏对象,开始游戏
      class Game(object):
      
       # 历史最高分
       top_score = 0
      
       def __init__(self, player_name):
           self.player_name = player_name
      
       @staticmethod
       def show_help():
           print('帮助信息:让僵尸进入大门')
      
       @classmethod
       def show_top_score(cls):
           print('历史记录 {}'.format(cls.top_score))
      
       def start_game(self):
           print('{} 开始游戏'.format(self.player_name))
      
      # 1. 查看游戏的帮助信息
      Game.show_help()
      
      # 2. 查看历史最高分
      Game.show_top_score()
      
      # 3. 创建游戏对象
      game = Game('小明')
      
      # 4. 开始游戏
      game.start_game()

7. 继承

7.1 什么是面向对象的继承

继承(英语:inheritance)是面向对象软件技术当中的一个概念。如果一个类别A“继承自”另一个类别B,就把这个A称为“B的子类别”,而把B称为“A的父类别”也可以称“B是A的超类”。继承可以使得子类别具有父类别的各种属性和方法,而不需要再次编写相同的代码。在令子类别继承父类别的同时,可以重新定义某些属性,并重写某些方法,即覆盖父类别的原有属性和方法,使其获得与父类别不同的功能。另外,为子类别追加新的属性和方法也是常见的做法。 一般静态的面向对象编程语言,继承属于静态的,意即在子类别的行为在编译期就已经决定,无法在执行期扩充。

字面意思就是:子承父业,合法继承家产,就是如果你是独生子,而且你也很孝顺,不出意外,你会继承你父母所有家产,他们的所有财产都会由你使用(败家子儿除外)。

那么用一个例子来看一下继承:

class Person:
    def __init__(self,name,sex,age):
        self.name = name
        self.age = age
        self.sex = sex

class Cat:
    def __init__(self,name,sex,age):
        self.name = name
        self.age = age
        self.sex = sex

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

# 继承的用法:
class Aniaml(object):
    def __init__(self,name,sex,age):
            self.name = name
            self.age = age
            self.sex = sex


class Person(Aniaml):
    pass

class Cat(Aniaml):
    pass

class Dog(Aniaml):
    pass

继承的有点也是显而易见的:

  1. 增加了类的耦合性(耦合性不宜多,宜精)。
  2. 减少了重复代码。
  3. 使得代码更加规范化,合理化。

7.2 继承的分类

就像上面的例子:

Aminal 叫做父类,基类,超类。
Person Cat Dog: 子类,派生类。
继承:可以分单继承,多继承

这里需要补充一下python中类的种类(继承需要):

在python2x版本中存在两种类.:
  ⼀个叫经典类. 在python2.2之前. ⼀直使⽤的是经典类. 经典类在基类的根如果什么都不写.
  ⼀个叫新式类. 在python2.2之后出现了新式类. 新式类的特点是基类的根是object类。
python3x版本中只有一种类:
python3中使⽤的都是新式类. 如果基类谁都不继承. 那这个类会默认继承 object

7.3 单继承

类名,对象执行父类方法

class Aniaml(object):
    type_name = '动物类'

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

    def eat(self):
        print(self)
        print('吃东西')


class Person(Aniaml):
    pass


class Cat(Aniaml):
    pass


class Dog(Aniaml):
    pass

# 类名:
print(Person.type_name)  # 可以调用父类的属性,方法。
Person.eat(111)
print(Person.type_name)

# 对象:
# 实例化对象
p1 = Person('春哥','男',18)
print(p1.__dict__)
# 对象执行类的父类的属性,方法。
print(p1.type_name)
p1.type_name = '666'
print(p1)
p1.eat()

执行顺序

class Aniaml(object):
    type_name = '动物类'
    def __init__(self,name,sex,age):
            self.name = name
            self.age = age
            self.sex = sex

    def eat(self):
        print(self)
        print('吃东西')

class Person(Aniaml):

    def eat(self):
        print('%s 吃饭'%self.name)

class Cat(Aniaml):
    pass

class Dog(Aniaml):
    pass

p1 = Person('barry','男',18)
# 实例化对象时必须执行__init__方法,类中没有,从父类找,父类没有,从object类中找。
p1.eat()
# 先要执行自己类中的eat方法,自己类没有才能执行父类中的方法。

同时执行类以及父类方法

方法一:

如果想执行父类的func方法,这个方法并且子类中夜用,那么就在子类的方法中写上:

父类.func(对象,其他参数)

举例说明:

class Aniaml(object):
    type_name = '动物类'
    def __init__(self,name,sex,age):
            self.name = name
            self.age = age
            self.sex = sex

    def eat(self):
        print('吃东西')

class Person(Aniaml):
    def __init__(self,name,sex,age,mind):
        '''
        self = p1
        name = '春哥'
        sex = 'laddboy'
        age = 18
        mind = '有思想'
        '''
        # Aniaml.__init__(self,name,sex,age)  # 方法一
        self.mind = mind

    def eat(self):
        super().eat()
        print('%s 吃饭'%self.name)
class Cat(Aniaml):
    pass

class Dog(Aniaml):
    pass

# 方法一: Aniaml.__init__(self,name,sex,age)
# p1 = Person('春哥','laddboy',18,'有思想')
# print(p1.__dict__)

# 对于方法一如果不理解:
# def func(self):
#     print(self)
# self = 3
# func(self)

方法二:

利用super,super().func(参数)

class Aniaml(object):
    type_name = '动物类'
    def __init__(self,name,sex,age):
            self.name = name
            self.age = age
            self.sex = sex

    def eat(self):
        print('吃东西')

class Person(Aniaml):
    def __init__(self,name,sex,age,mind):
        '''
        self = p1
        name = '春哥'
        sex = 'laddboy'
        age = 18
        mind = '有思想'
        '''
        # super(Person,self).__init__(name,sex,age)  # 方法二
        super().__init__(name,sex,age)  # 方法二
        self.mind = mind

    def eat(self):
        super().eat()
        print('%s 吃饭'%self.name)
class Cat(Aniaml):
    pass

class Dog(Aniaml):
    pass
# p1 = Person('春哥','laddboy',18,'有思想')
# print(p1.__dict__)

单继承练习题:

# 1
class Base:
    def __init__(self, num):
        self.num = num
    def func1(self):
        print(self.num)

class Foo(Base):
    pass
obj = Foo(123)
obj.func1() # 123 运⾏的是Base中的func1  

# 2      
class Base:
    def __init__(self, num):
        self.num = num
    def func1(self):
        print(self.num)
class Foo(Base):
    def func1(self):
        print("Foo. func1", self.num)
obj = Foo(123)
obj.func1() # Foo. func1 123 运⾏的是Foo中的func1       

# 3         
class Base:
    def __init__(self, num):
        self.num = num
    def func1(self):
        print(self.num)
class Foo(Base):
    def func1(self):
        print("Foo. func1", self.num)
obj = Foo(123)
obj.func1() # Foo. func1 123 运⾏的是Foo中的func1     
# 4
class Base:
    def __init__(self, num):
        self.num = num
    def func1(self):
        print(self.num)
        self.func2()
    def func2(self):
        print("Base.func2")
class Foo(Base):
    def func2(self):
    print("Foo.func2")
obj = Foo(123)
obj.func1() # 123 Foo.func2 func1是Base中的 func2是⼦类中的 
# 再来
class Base:
    def __init__(self, num):
        self.num = num
    def func1(self):
        print(self.num)
        self.func2()
    def func2(self):
        print(111, self.num)
class Foo(Base):
    def func2(self):
        print(222, self.num)
lst = [Base(1), Base(2), Foo(3)]
for obj in lst:
    obj.func2() # 111 1 | 111 2 | 222 3

# 再来
class Base:
    def __init__(self, num):
        self.num = num
    def func1(self):
        print(self.num)
        self.func2()
    def func2(self):
        print(111, self.num)
class Foo(Base):
    def func2(self):
        print(222, self.num)
lst = [Base(1), Base(2), Foo(3)]
for obj in lst:
    obj.func1() # 拿笔来吧. 好好算

7.4 多继承

class ShenXian: # 神仙
    def fei(self):
        print("神仙都会⻜")
class Monkey: # 猴
    def chitao(self):
        print("猴⼦喜欢吃桃⼦")
class SunWukong(ShenXian, Monkey): # 孙悟空是神仙, 同时也是⼀只猴
    pass
sxz = SunWukong() # 孙悟空
sxz.chitao() # 会吃桃⼦
sxz.fei() # 会⻜

此时, 孙悟空是⼀只猴⼦, 同时也是⼀个神仙. 那孙悟空继承了这两个类. 孙悟空⾃然就可以执⾏这两个类中的⽅法. 多继承⽤起来简单. 也很好理解. 但是多继承中, 存在着这样⼀个问题. 当两个⽗类中出现了重名⽅法的时候. 这时该怎么办呢? 这时就涉及到如何查找⽗类⽅法的这么⼀个问题.即MRO(method resolution order) 问题. 在python中这是⼀个很复杂的问题. 因为在不同的python版本中使⽤的是不同的算法来完成MRO的.

这里需要补充一下python中类的种类(继承需要):

在python2x版本中存在两种类.:
  ⼀个叫经典类. 在python2.2之前. ⼀直使⽤的是经典类. 经典类在基类的根如果什么都不写.
  ⼀个叫新式类. 在python2.2之后出现了新式类. 新式类的特点是基类的根是object类。
python3x版本中只有一种类:
python3中使⽤的都是新式类. 如果基类谁都不继承. 那这个类会默认继承 object

经典类的多继承

虽然在python3中已经不存在经典类了. 但是经典类的MRO最好还是学⼀学. 这是⼀种树形结构遍历的⼀个最直接的案例. 在python的继承体系中. 我们可以把类与类继承关系化成⼀个树形结构的图. 来, 上代码:

class A:
    pass
class B(A):
    pass
class C(A):
    pass
class D(B, C):
    pass
class E:
    pass
class F(D, E):
    pass
class G(F, D):
    pass
class H:
    pass
class Foo(H, G):
    pass

对付这种 mro 画图就可以:

继承关系图已经有了. 那如何进⾏查找呢? 记住⼀个原则. 在经典类中采⽤的是深度优先,遍历⽅案. 什么是深度优先. 就是⼀条路走到头. 然后再回来. 继续找下⼀个:

图中每个圈都是准备要送鸡蛋的住址. 箭头和⿊线表⽰线路. 那送鸡蛋的顺序告诉你入⼝在最下⾯R. 并且必须从左往右送. 那怎么送呢?

如图. 肯定是按照123456这样的顺序来送. 那这样的顺序就叫深度优先遍历. ⽽如果是142356呢? 这种被称为⼴度优先遍历. 好了. 深度优先就说这么多. 那么上⾯那个图怎么找的呢? MRO是什么呢? 很简单. 记住. 从头开始. 从左往右. ⼀条路跑到头, 然后回头. 继续⼀条路跑到头. 就是经典类的MRO算法.

类的MRO: Foo-> H -> G -> F -> E -> D -> B -> A -> C. 你猜对了么?

新式类的多继承

mro序列

MRO是一个有序列表L,在类被创建时就计算出来。
通用计算公式为:

mro(Child(Base1,Base2)) = [ Child ] + merge( mro(Base1), mro(Base2), [ Base1, Base2] )
(其中Child继承自Base1, Base2)

如果继承至一个基类:class B(A)
这时B的mro序列为:

mro( B ) = mro( B(A) )
= [B] + merge( mro(A) + [A] )
= [B] + merge( [A] + [A] )
= [B,A]

如果继承至多个基类:class B(A1, A2, A3 …)
这时B的mro序列:

mro(B) = mro( B(A1, A2, A3 …) )
= [B] + merge( mro(A1), mro(A2), mro(A3) ..., [A1, A2, A3] )
= ...

计算结果为列表,列表中至少有一个元素即类自己,如上述示例[A1,A2,A3]。merge操作是C3算法的核心。

表头和表尾

  • 表头

    • 列表的第一个元素
  • 表尾

    • 列表中表头以外的元素集合(可以为空)
  • 示例

    • 列表:[A, B, C]
      • 表头是A,表尾是B和C

列表之间的 + 操作

  • +操作:
    • [A] + [B] = [A, B]

merge操作示例:

如计算merge( [E,O], [C,E,F,O], [C] )
有三个列表 :  ①      ②          ③
merge不为空,取出第一个列表列表①的表头E,进行判断                              
   各个列表的表尾分别是[O], [E,F,O],E在这些表尾的集合中,因而跳过当前当前列表
取出列表②的表头C,进行判断
   C不在各个列表的集合中,因而将C拿出到merge外,并从所有表头删除
   merge( [E,O], [C,E,F,O], [C]) = [C] + merge( [E,O], [E,F,O] )
进行下一次新的merge操作 ......

计算mor(A)方式:

mro(A) = mro( A(B,C) )

原式= [A] + merge( mro(B),mro(C),[B,C] )

  mro(B) = mro( B(D,E) )
         = [B] + merge( mro(D), mro(E), [D,E] )  # 多继承
         = [B] + merge( [D,O] , [E,O] , [D,E] )  # 单继承mro(D(O))=[D,O]
         = [B,D] + merge( [O] , [E,O]  ,  [E] )  # 拿出并删除D
         = [B,D,E] + merge([O] ,  [O])
         = [B,D,E,O]

  mro(C) = mro( C(E,F) )
         = [C] + merge( mro(E), mro(F), [E,F] )
         = [C] + merge( [E,O] , [F,O] , [E,F] )
         = [C,E] + merge( [O] , [F,O]  ,  [F] )  # 跳过O,拿出并删除
         = [C,E,F] + merge([O] ,  [O])
         = [C,E,F,O]

原式= [A] + merge( [B,D,E,O], [C,E,F,O], [B,C])
    = [A,B] + merge( [D,E,O], [C,E,F,O],   [C])
    = [A,B,D] + merge( [E,O], [C,E,F,O],   [C])  # 跳过E
    = [A,B,D,C] + merge([E,O],  [E,F,O])
    = [A,B,D,C,E] + merge([O],    [F,O])  # 跳过O
    = [A,B,D,C,E,F] + merge([O],    [O])
    = [A,B,D,C,E,F,O]

结果OK. 那既然python提供了. 为什么我们还要如此⿇烦的计算MRO呢? 因为笔试…….你在笔试的时候, 是没有电脑的. 所以这个算法要知道. 并且简单的计算要会. 真实项⽬开发的时候很少有⼈这么去写代码.

这个说完了. 那C3到底怎么看更容易呢? 其实很简单. C3是把我们多个类产⽣的共同继承留到最后去找. 所以. 我们也可以从图上来看到相关的规律. 这个要⼤家⾃⼰多写多画图就能感觉到了. 但是如果没有所谓的共同继承关系. 那⼏乎就当成是深度遍历就可以了


  转载请注明: 浩大大 Python面向对象

  目录