【转】Python之面向对象与类

【转】Python之面向对象与类

本节内容


  1. 面向对象的概念
  2. 类的封装
  3. 类的继承
  4. 类的多态
  5. 静态方法、类方法 和 属性方法
  6. 类的特殊成员方法
  7. 继承层级关系中子类的实例对象对属性的查找顺序问题

一、面向对象的概念


1. "面向对象(OOP)"是什么?

简单点说,“面向对象”是一种编程范式,而编程范式是按照不同的编程特点总结出来的编程方式。俗话说,条条大路通罗马,也就说我们使用不同的方法都可以达到最终的目的,但是有些办法比较快速、安全且效果好,有些方法则效率低下且效果不尽人意。同样,编程也是为了解决问题,而解决问题可以有多种不同的视角和思路,前人把其中一些普遍适用且行之有效的编程模式归结为“范式”。常见的编程范式有:

  • 面向过程编程:OPP(Procedure Oriented Programing)
  • 面向对象编程:OOP(Object Oriented Programing)
  • 函数式编程:(Functional Programing)

面向过程编程的步骤:

1)分析出解决问题所需要的步骤;

2)用函数把这些步骤一次实现;

3)一个一个地调用这些函数来解决问题;

面向对象编程的步骤:

1)把构成问题的事务分解、抽象成各个对象;

2)结合这些对象的共有属性,抽象出类;

3)类层次化结构设计--继承 和 合成;

4)用类和实例进行设计和实现来解决问题。

关于面向对象编程 与 面向过程编程的区别与优缺点可以参考这篇文章

2. 面向对象编程的特点

面向对象编程达到了软件工程的3个目标:重用性、灵活性、扩展性,而这些目标是通过以下几个主要特点实现的:

  • 封装: 可以隐藏实现细节,使代码模块化
  • 继承: 可以通过扩展已存在的类来实现代码重用,避免重复编写相同的代码
  • 多态: 封装和继承的目的都是为了实现 代码重用, 而多态是为了实现 接口重用,使得类在继承和派生的时候能够保证任何一个类的实例都能正确调用约定好的属性和方法。简单来说,就是为了约定相同的属性和方法名称。

需要说明的是,Python不像Java中又专门的“接口”定义,Python中的接口与类没有什么区别,但是我们可以通过在一个用于当做接口的类中所定义的方法体中 raise NotImplementedError异常,来强制子类必须重新实现该方法。

3. 面向对象编程的使用场景

我们知道,Python既可以面向过程编程,也可以面向对象编程。那么什么场景下应该使用面向对象编程呢?如果我们仅仅是写一个简单的脚本来跑一些简单的任务,我们直接用面向过程编程就好了,简单,快速。当我们需要实现一个复杂的系统时,或者以下场景下,就需要使用面向对象编程:

  • 场景1: 当多个函数需要传入多个共同的参数时,可以将这些函数封装到一个类中,并将这些参数提取为这个类的属性;
  • 场景2: 当需要根据一个模板来创建某些东西时,可以通过类来完成。

二、类的封装


封装是面向对象的主要特征之一,是对象和类概念的主要特性。简单的说,一个类就是一个封装了数据以及操作这些数据的方法的逻辑实体,它向外暴露部分数据和方法,屏蔽具体的实现细节。除此之外,在一个对象内部,某些数据或方法可以是私有的,这些私有的数据或方法是不允许外界访问的。通过这种方式,对象对内部数据提供了不同级别的保护以防止程序中无关的部分意外的改变或错误使用了对象的私有部分,比如java中修饰类变量和方法的相关关键字有:private、protected, public等。下面我们通过类的定义和实例化的实例来说明一下Python中的是如何实现对这些不同等级数据的保护的。

1. 类的定义

类的定义是对显示事务的抽象过程和能力,类是一个对象/实例的模板,也是一个特殊的对象/实例(因为Pythobn中一切皆对象,所以类本身也是一个对象)

现在我们来定义个Person类,它有以下3个属性:

  • nationality:国籍
  • name:姓名
  • id:身份证号码

假设现在我们有以下几个前提:

  • 所有人的国籍基本都是相同的,且允许直接通过类或实例来访问,允许随意修改
  • 大部分人的姓名是不同的,且允许直接通过类的实例来访问和随意修改
  • 所有人的身份证号码都是不一样的,且不允许直接通过类或实例来访问或随意修改
import uuid

class Person(object):
    nationality = 'China'
    
    def __init__(self, name):
        self.name = name
        self.__id = str(uuid.uuid1())
    
    def hello(self):
        print('Hi, i am %s, from %s, my id is %s' % (self.name, self.nationality, self.__id))
    
    def get_and_print_id(self):
        print(self.__id)
        return self.__id

2. 类的实例化

类实例化的方式:类名([参数...]),参数是__init__方法中除了第一个self参数之外的其他参数,上面定义的这个Person类中,实例化时需要传递的参数只有一个name。比如我们来实例化3个Person对象,他们的name分别是 tom 和 jerry:

tom = Person('tom')
jerry = Person('jerry')
jack = Person('jack')

3. 不同保护等级的属性说明

公有属性/类属性

直接定义在class下的属性就是公有属性/类属性,比如上面那个Person类中的nationality属性。“公有”的意思是这个属性是这个类的所有实例对象共同所有的,因此默认情况下这个属性值值保留一份,而不会为该类的每个实例都保存一份。

print(Person.nationality, tom.nationality, jerry.nationality, jack.nationality)

tom.nationality = 'USA'
print(Person.nationality, tom.nationality, jerry.nationality, jack.nationality)

Person.nationality = 'India'
print(Person.nationality, tom.nationality, jerry.nationality, jack.nationality)

输出结果如下:

China China China China
China USA China China
India USA India India

结论:

- 公有属性/静态属性 可以直接通过类直接访问,也可以直接通过实例进行访问;
- 通过类的某个实例对公有属性进行修改,实际上对为该实例添加了一个与类的公有属性名称相同的成员属性,对真正的公有属性是没有影响的,因此它不会影响其他实例获取的该公有属性的值;
- 通过类对公有属性进行修改,必然是会改变公有属性原有的值,他对该类所有的实例是都有影响的。

成员属性/实例属性

成员属性,又称成员变量 或 实例属性,也就是说这些属性是 该类的每个实例对象单独持有的属性。成员属性需要在类的__init__方法中进行声明,比如上面的Person类中定义的name属性就是一个成员属性。

print(tom.name, jerry.name, jack.name)

jerry.name = 'jerry01'
print(tom.name, jerry.name, jack.name)

输出结果:

tom jerry jack
tom jerry01 jack

来看看能不能直接通过类访问成员属性

print(Person.name)

输出结果:

Traceback (most recent call last):
  ...
AttributeError: type object 'Person' has no attribute 'name'

结论:

- 成员属性可以直接通过实例对象来访问和更改;
- 成员属性是每个实例对象独有的,某个实例对象的成员属性被更改不会影响其他实例对象的相同属性的值;
- 成员属性的值不能通过类来访问和修改;

私有属性

私有属性和成员属性一样,是在__init__方法中进行声明,但是属性名需要以双下划线__开头,比如上面定义的Person中的__id属性。私有属性是一种特殊的成员属性,它只允许在实例对象的内部(成员方法或私有方法中)访问,而不允许在实例对象的外部通过实例对象或类来直接访问,也不能被子类继承。

通过实例对象访问私有属性:

print(tom.__id)

输出结果

Traceback (most recent call last):
    ...
AttributeError: 'Person' object has no attribute '__id'

通过类访问私有属性:

print(Person.__id)

输出结果:

Traceback (most recent call last):
  ...
AttributeError: type object 'Person' has no attribute '__id'

通过类的成员方法访问私有属性:

tom.hello()
jerry.hello()
jack.hello()

输出结果:

Hi, i am tom, from China, my id is b6ac08c6-9dae-11e7-993f-208984d7aa83
Hi, i am jerry, from China, my id is b6ac08c7-9dae-11e7-b508-208984d7aa83
Hi, i am jack, from China, my id is b6ac08c8-9dae-11e7-9ace-208984d7aa83

结论:

- 私有变量不能通过类直接访问;
- 私有变量也不能通过实例对象直接访问;
- 私有变量可以通过成员方法进行访问。

那么要访问私有变量怎么办呢? 有两种办法:

办法1:通过一个专门的成员方法返回该私有变量的值,比如上面定义的get_id()方法,搞过java的同学很自然就会想到java类中的set和get方法。

tom_id = tom.get_id()
jerry_id = jerry.get_id()
jack_id = jack.get_id()

print(tom_id, jerry_id, jack_id)

输出结果:

46bc6b5c-9dd6-11e7-8306-208984d7aa83 46cbfe68-9dd6-11e7-b5d1-208984d7aa83 46cbfe69-9dd6-11e7-9b5c-208984d7aa83

办法2:通过 实例对象._类名__私有变量名 的方式来访问

print(tom._Person__id, jerry._Person__id, jack._Person__id)

输出结果:

e1f4ee86-9dd6-11e7-a186-208984d7aa83 e1f5b1f8-9dd6-11e7-b1c3-208984d7aa83 e1f5b1f9-9dd6-11e7-b74a-208984d7aa83

总结

  • 公有属性、成员属性 和 私有属性 的受保护等级是依次递增的;
  • 私有属性 和 成员属性 是存放在已实例化的对象中的,每个对象都会保存一份;
  • 公有属性是保存在类中的,只保存一份;
  • 哪些属性应该是公有属性的,哪些属性应该是私有属性 需要根据具体业务需求来确定。

三、类的继承


1. 继承的相关概念

继成组合 是类的两个最主要的关系,而 继承 关系的类之间是有层级的。被继承的类被称为 父类、基类 或 超类 ;继承的类被称为 子类 或 派生类

2. 继承的作用

继承 是一个从一般到特殊的过程, 子类可以继承现有类的所有功能,而不需要重新实现代码。简单来说就是 继承提高了代码重用性和扩展性

3. 继承的分类

Python中类的继承按照父类中的方法是否已实现可分为两种:

  • 实现继承 :指直接继承父类的属性和已定义并实现的的方法;
  • 接口继承 :仅继承父类类的属性和方法名称,子类必须自行实现方法的具体功能代码。

如果是根据要继承的父类的个数来分,有可以分为:

  • 单继承: 只继承1个父类
  • 多继承: 继承多个父类
    通常,我们都是用 单继承 ,很少用到 多继承。

4. 类继承实例

类的继承关系

父类/基类/超类--Person
class Person(object):
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    def walk(self):
        print('%s is walking...' % self.name)
        
    def talk(self):
        print('%s is talking...' % self.name )
子类--Teacher
class Teacher(Person):
    def __init__(self, name, age, level, salary):
        super(Teacher, self).__init__(name, age)
        self.level = level
        self.salary = salary

    def teach(self):
        print('%s is teaching...' % self.name)
子类--Class
class Student(Person):
    def __init__(self, name, age, class_):
        Person.__init__(self, name, age)
        self.class_ = class_

    def study(self):
        print('%s is studying...' % self.name)

子类实例化

t1 = Teacher('张老师', 33, '高级教师', 20000)
s1 = Student('小明', 13, '初一3班')

t1.talk()
t1.walk()
t1.teach()

s1.talk()
s1.walk()
s1.study()

输出结果:

张老师 is talking...
张老师 is walking...
张老师 is teaching...
小明 is talking...
小明 is walking...
小明 is studying...

继承说明

  • Teacher类 和 Student类 都继承 Person类,因此Teacher和Student是Person的子类/派生类,而Person是Teacher和Student的父类/基类/超类;
  • Teacher和Student对Person的继承属于实现继承,且是单继承;
  • Teacher类继承了Person的name和age属性,及talk()和walk()方法,并扩展了自己的level和salary属性,及teach()方法;
  • Student类继承了Person的name和age属性,及talk()和walk()方法,并扩展了自己的class属性,及study()方法;
  • Teacher和Student对Person类属性和方法继承体现了 “代码的重用性”, 而Teacher和Student扩展的属性和方法体现了 “灵活的扩展性”;
  • 子类需要在自己的__init__方法中的第一行位置调用父类的构造方法,上面给出了两种方法:
    • super(子类名, self).__init__(父类构造参数),如super.(Teacher, self).__init__(name, age)
    • 父类名.__init__(self, 父类构造参数),如Person.__init__(self, name, age),这是老式的用法。
  • 子类 Teacher 和 Student 也可以在自己的类定义中 重新定义 父类中的talk()和walk()方法,改变其实现代码,这叫做方法重写

关于多继承,以及多继承时属性查找顺序(广度优先、深度优先)的问题会在下面进行单独说明。

四、类的多态


多态是指,相同的成员方法名称,但是成员方法的行为(代码实现)却各不相同。这里所说的多态是通过 继承接口的方式实现的。Java中有interface,但是Python中没有。Python中可以通过在一个成员方法体中抛出一个NotImplementedError异常来强制继承该接口的子类在调用该方法前必须先实现该方法的功能代码。

接口--Animal

class Animal(object):
    def __init__(self, name):
        self.name = name

    def walk(self):
        raise NotImplemented('Subclass must implement the abstract method by self')

    def talk(self):
        raise NotImplemented('Subclass must implement the abstract method by self')

子类--Dog

class Dog(Animal):
    pass

执行代码:

dog = Dog('大黄')
dog.talk()

输出结果:

Traceback (most recent call last):
  ...
    raise NotImplemented('Subclass must implement the abstract method by self')
TypeError: 'NotImplementedType' object is not callable

可见,此时子类必须自己先实现talk方法才能调用。

实现了接口方法的子类--Dog 和 Duck

class Dog(Animal):
    def talk(self):
        print('%s is talking:旺旺...' % self.name)

    def walk(self):
        print('%s 是一条小狗,用4条腿走路' % self.name)

class Duck(Animal):
    def talk(self):
        print('%s is talking: 嘎嘎...' % self.name)

    def walk(self):
        print('%s 是一只鸭子,用两条腿走路' % self.name)

执行代码:

dog = Dog('大黄')
dog.talk()
dog.walk()

duck = Duck('小白')
duck.talk()
duck.walk()

输出结果:

大黄 is talking:旺旺...
大黄 是一条小狗,用4条腿走路
小白 is talking: 嘎嘎...
小白 是一只鸭子,用两条腿走路

由此可知:

  • 接口的所有子类拥有接口中定义的所有同名的方法;
  • 接口的所有子类在调用接口中定义的方法时,必须先自己实现方法代码;
  • 接口的各个子类在实现接口中同一个方法时,具体的代码实现各不相同,这就是多态。

五、属性方法、类方法、静态方法


上面提到过,类中封装的是数据和操作数据的方法。数据就是属性,且上面已经介绍过了属性分为:
公有属性/类变量、成员属性/实例变量 和 私有属性。现在我们来说说类中的方法,类中的方法分为以下几种:

  • 成员方法: 上面定义的都是成员方法,通常情况下,它们与成员属性相似,是通过类的实例对象去访问;成员方法的第一个参数必须是当前实例对象,通常写为self;实际上,我们也可以通过类名来调用成员方法,只是此时我们需要手动的传递一个该类的实例对象给成员方法的self参数,这样用明显不是一种优雅的方法,因此基本不会这样使用。

  • 私有方法: 以双下划线开头的成员方法就是私有方法,与私有属性类似,只能在实例对象内部访问,且不能被子类继承;私有方法的第一个参数也必须是当前实例对象本身,通常写为self;

  • 类方法: 以@classmethod来装饰的成员方法就叫做类方法,它要求第一次参数必须是当前类。与公有属性/静态属性 相似,除了可通过实例对象进行访问,还可以直接通过类名去访问,且第一个参数表示的是当前类,通常写为cls;另外需要说明的是,类方法只能访问公有属性,不能访问成员属性,因此第一个参数传递的是代表当前类的cls,而不是表示实例对象的self。

  • 静态方法: 以@staticmethod来装饰的成员方法就叫做静态方法,静态方法通常都是通过类名去访问,且严格意义上来讲,静态方法已经与这个类没有任何关联了,因为静态方法不要求必须传递实例对象或类参数,这种情况下它不能访问类中的任何属性和方法。

  • 属性方法: 这个比较有意思,是指可以像访问成员属性那样去访问这个方法;它的第一个参数也必须是当前实例对象,且该方法必须要有返回值。

我们先来定义这样一个类:

import uuid

class Person(object):
    nationality = 'China'

    def __init__(self, name, age):
        self.name = name
        self.age = age
        self.__id = str(uuid.uuid1())

    # 成员方法/实例方法
    def sayHello(self):
        print('Hello, i am %s from %s, i am %d years old.' % (self.name, self.nationality, self.age))

    # 私有方法
    def __func0(self):
        print('private method: func0')
        print(self.name, self.age, self.__id, self.nationality)

    # 类方法
    @classmethod
    def func1(cls):
        print(cls.nationality)

    # 静态方法
    @staticmethod
    def func2(a, b):
        print(a + b)

    # 属性方法
    @property
    def func3(self):
        return '%s: %d' % (self.name, self.age)

执行代码:

p = Person('Tom', 18)

p.sayHello()
Person.sayHello(p)

Person.func1()
p.func1()

Person.func2(3, 4)
p.func2(3, 4)


print(p.func3)

输出结果:

Hello, i am Tom from China, i am 18 years old.
Hello, i am Tom from China, i am 18 years old.

China
China

7
7

Tom: 18

总结:

  • 成员方法也可以通过类名去访问,但是有点多此一举的感觉;
  • 类方法和静态方法也可以通过实例对象去访问,但是通常情况下都是通过类名直接访问的;
  • 最重要的一条总结:类的各种方法,能访问哪些属性实际上是跟方法的参数有关的:
    • 比如成员方法要求第一个参数必须是一个该类的实例对象,那么实例对象能访问的属性,成员方法都能访问,而且还能访问私有属性;
    • 再比如,类方法要求第一个参数必须是当前类,因此它只能访问到类属性/公有属性,而访问不到成员属性 和 私有属性;
    • 再比如,静态方法对参数没有要求,也就意味着我们可以任意给静态方法定义参数;假如我们给静态方法定义了表示当前类的参数,那么就可以访问类属性/公有属性;假如我们给静态方法定义了表示当前类的实例对象的参数,那么就可以访问成员属性;假如我们没有给静态方法定义这两个参数,那么就不能访问该类或实例对象的任何属性。

六、类的特殊成员属性及特殊成员方法


我们上面提到过:名称以双下划线__开头的属性是私有属性,名称以双下划线__开头的方法是私有方法。这里我们要来说明的是,Python的类中有一些内置的、特殊的属性和方法,它们的名称是以双下划线__开头,同时又以双下划线__结尾。这些属性和方法不再是私有属性和私有方法,它们是可以在类的外部通过实例对象去直接访问的,且它们都有着各自特殊的意义,我们可以通过这些特殊属性和特殊方法来获取一些重要的信息,或执行一些有用的操作。

1. 类的特殊成员属性

属性名称说明
__doc__ 类的描述信息
__module__ 表示当前操作的对象对应的类的定义所在的模块名
__class__ 表示当前操作的对象对应的类名
__dict__ 一个字典,保存类的所有的成员(包括属性和方法)或实例对象中的所有成员属性

现在来看一个实例:

在dog.py模块定义一个Dog类

class Dog(object):
    """这是一个Dog类"""
    # print('Hello, This is a dog.')
    color = '白色'

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

    def func1(self):
        pass

    def __func1(self):
        pass

    @classmethod
    def func2(cls):
        pass

    @staticmethod
    def func3():
        pass

在test.py模块执行下面的代码

from dog import Dog

dog1 = Dog('泰迪')
print(dog1.__doc__)
print(dog1.__module__)
print(dog1.__class__)
print(dog1.__dict__)
print(Dog.__dict__)

输出结果

这是一个Dog类
dog
<class 'dog.Dog'>
{'name': '泰迪', '_Dog__id': 1234}
{'__dict__': <attribute '__dict__' of 'Dog' objects>, '__module__': 'dog', 'func2': <classmethod object at 0x000001DF0C658F98>, 'color': '白色', 'func3': <staticmethod object at 0x000001DF0C658FD0>, '_Dog__func1': <function Dog.__func1 at 0x000001DF0C63E400>, '__weakref__': <attribute '__weakref__' of 'Dog' objects>, '__doc__': '这是一个Dog类', '__init__': <function Dog.__init__ at 0x000001DF0C63E2F0>, 'func1': <function Dog.func1 at 0x000001DF0C63E378>}

总结:

  • 实例对象.__dict__ 和 类.__dict__ 的值是不同的:实例对象.__dict__的值中只包含成员属性和私有属性,类.__dict__的值中包含公有属性/类属性和所有类型的方法;
  • __module__和__class__的值可用于反射来实例化一个类的对象,下面会介绍。

2. 类的特殊成员方法

方法名称说明
__init__ 类构造方法,通过类创建对象时会自动触发执行该方法
__del__ 析构方法,当对象在内存中被什邡市,会自动触发执行该方法。比如实例对象的作用域退出时,或者执行 del 实例对象操作时。
__str__ 如果一个类中定义了__str__方法,那么在打印对象时默认输出该方法的返回值,否则会打印出该实例对象的内存地址。
__xxxitem__ 是指__getitem__、__setitem__、__delitem这3个方法,它们用于索引操作,比如对字典的操作,分别表示 获取、设置、删除某个条目。 数据。可以通过这些方法来定义一个类对字典进行封装,从而可以对字典中key的操作进行控制,尤其是删除操作。
__new__ 该方法会在__init__方法之前被执行,该方法会创建被返回一个新的实例对象,然后传递给__init__。另外需要说明的是,这不是一个成员方法,而是一个静态方法。
__call__ 源码中的注释是"Call self as a function." 意思是把自己(实例对象)作为一个函数去调用,而函数的调用方式是函数名()。也就是说,当我们执行实例对象()或者 类名()()这样的操作时会触发执行该方法。

示例1

先来定义这样一个类:

class Person(object):
    def __call__(self, *args, **kwargs):
        print(self.name, '__call__')

    def __init__(self, name, age):
        self.name = name
        self.age = age
        print(self.name, '__init__')

    def __del__(self):
        print(self.name, '__del__')

    def __str__(self):
        print(self.name, '__str__')
        return '%s: %d'% (self.name, self.age)

执行下面的代码:

print('--------实例化对象-----------')
p = Person('Tom', 18)
print('--------打印实例对象-----------')
print(p)
print('--------把实例对象作为方法进行调用-----------')
p()  # 等价于 Person('Tom', 18)()
print('--------程序运行结束-----------')

输出结果:

--------实例化对象-----------
Tom __init__
--------打印实例对象-----------
Tom __str__
Tom: 18
--------把实例对象作为方法进行调用-----------
Tom __call__
--------程序运行结束-----------
Tom __del__

可以看到,所有代码都执行完后,进程退出时实例对象的__del__方法才被调用,这是因为对象要被销毁了。

示例2

定义一个类似字典的类

class MyDict(object):
    def __init__(self, init=None):
        self.__dict = init if init is not None else {}

    def __setitem__(self, key, value):
        print('__setitem__', key)
        self.__dict[key] = value

    def __getitem__(self, item):
        print('__getitem__', item)
        return self.__dict.get(item, None)

    def __delitem__(self, key):
        print('__delitem__', key)
        if key is not None and key.startswith('wh'):
            print('You can not delete this item ')
            return None
        return self.__dict.pop(key, None)

执行下面的代码

# 类实例化与get item
my_dict = MyDict(init={'what': '打豆豆', 'who': '企鹅团', 'time': '吃饱睡好之后'})
print(my_dict['who'], my_dict['time'], my_dict['what'])

# set item
my_dict['num'] = '10次'
print(my_dict['who'], my_dict['time'], my_dict['what'], my_dict['num'])

# del item
del my_dict['num']
print(my_dict['num'])

del my_dict['what']
print(my_dict['what'])

输出结果

__getitem__ who
__getitem__ time
__getitem__ what
企鹅团 吃饱睡好之后 打豆豆

__setitem__ num
__getitem__ who
__getitem__ time
__getitem__ what
__getitem__ num
企鹅团 吃饱睡好之后 打豆豆 10次

__delitem__ num
__getitem__ num
None

__delitem__ what
You can not delete this item 
__getitem__ what
打豆豆

可见,如果一个类实现了__setitem__,__getitem、__delitem 这几个方法,就可以执行一些类似字典一样的操作,比如上面用到的:

  • my_dict['KEY'] 会自动调用my_dict实例对象的__getitem__方法;
  • my_dict['KEY'] = VALUE 会自动调用my_dict实例对象的__setitem__方法;
  • del my_dict['KEY'] 会自动调用my_dict实例独享的__delitem__方法;
    而我们定义这样一个类的目的在于,我们可以更好对字典操作进行控制,比如上面的例子中我们不允许删除key以'wh'开头的条目。

七、继承层级关系中子类的实例对象对属性的查找顺序问题


有的同学说:“这个还不简单吗?子类肯定是先找自己有没有这个属性或方法,有的话直接调用自己的,没有再去父类里面找。” 没毛病,确实是这样的,但是我们真的理解这句话了吗?另外,如果是多继承且有多个类的层级关系,查找顺序又是怎样的呢?再来简单描述下这里要讨论的问题是什么,简单来说,就是要大家搞明白2个问题:

  • 1)子类的实例对象调用的某个属性或方法到底是父类的还是自己的;
  • 2)如果是多继承(同时继承多个父类),调用的的到底是哪个父类的属性或方法,查找顺序是怎样的。

如果大家把这2问题搞明白了,这里目的就达到了。下面我们分别以单继承和多继承两个实例来讲解我们这里要说明的问题。

1. 单继承的情况

A、B、C三个类的定义如下:

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

    def func1(self):
        print('class A: func1')

    def func2(self):
        print('class A: func2')

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

    def func2(self):
        print('class B: func2')

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

    def func3(self):
        print('class C: func3')

现在要执行这段代码:

objB = B('Tom', 18)
objC = C('Jerry')

print(objB.name, objB.age)
print(objC.name)
objB.func1()
objC.func1()

请先思考下面代码的输出结果,再去看下面的答案。

输出结果:

Tom 18
Jerry
class A: func1
class C: func1

这是最简单的情况,也是最容易用开头那段话来解释清楚的情况,因此不做过多赘述。但这只是个引子,真正要讨论的是下面这种情况:

A与B两个类的定义如下:

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

    def func1(self):
        print('class A: func1')
        self.func2()

    def func2(self):
        print('class A: func2')

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

    def func2(self):
        print('class B: func2')

现在要执行下面的代码:

objB = B('Tom', 18)
objB.func1()

你心里有答案了吗?func1必然是执行的父类 A中的func1,因为子类B中没有这个方法。那么父类func1中调用的func2方法,到底是父类的还是子类的呢?解决了这个疑问,答案自然就出来了。

分析1:

class B 是class A的子类,因此它会继承class A的的方法func1和func2。但是,class B已经重写了func2,可以理解为class A中的func2方法已经被覆盖了,class B现在只能看到自己重写后的那个func2方法,所以func1中调用的应该是class B 重写后的func2方法。

分析2:

有的同学可能不太能理解,class A中的方法怎么能调用class B中的方法呢?下面我们来看下class B与class
A的包含关系图:

因为子类 class B继承了 class A的内容,因此绿框中的内容(class A)是属于蓝框(class B)中的一部分,他们应该看做一个整体。绿框中的func1是可以调用绿框外的func2的,因为他们都是objB中的成员方法。其实理解这些之后,现在我们来套用开始那句话:

  • func1方法的查找与调用: objB调用func1方法,发现class B本身(蓝框内的直接内容)并没有该方法,所以去父类class A(蓝框内的绿框)中去找,发现找到了,于是进行调用;
  • func2方法的查找与调用: func1中调用了func2方法,这个时候还是先找子类class B本身(蓝框内的直接内容),发现找到了,于是直接调用子类自己的func2方法。

因此上面这段代码的执行结果是:

class A: func1
class B: func2

2. 多继承的情况

新式类 与 经典类

Python 2.2引入了新式类,与它对应的是经典类,这里我们仅仅是解释下他们的概念,为讲解下面的内容做铺垫,不会深入讨论的它们的之间的区别。这里我们主要说明一下几个点就可以了:

  • Python 2.x中,默认是经典类,只有显示继承了object的才是新式类;
  • Python 3.x中,默认就是新式类,经典类已经被废弃;
  • 新式类的子类也是新式类

深度优先 与 广度优先

深度优先 可以理解为 纵向优先,广度优先 可以理解为 水平方法优先。我们知道,类与类之间是有层级关系的,父类与子类是纵向的层级关系,同一个父类的多个直接子类是水平方向的同级关系。

上图中 A是父类、B和C是继承A的子类,D是同时继承B和C的子类。此时D的一个实例对象去查找一个父类中的属性或方法的查找顺序就有两种可能,但是这两种查找顺序中第一个查找的父类必然都是B:

  • B-->A-->C:这就是深度优先,因为优先查找的是与B上一层级的、纵向的A
  • B-->C-->A:这就是广度优先,因为优先查找的是与B同一层极的、水平方向的C

实例

定义以下几个类:

class A(object):
    def func1(self):
        print('class A: func1')

    def func2(self):
        print('class A: func2')

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

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

class D(B, C):
    pass

执行如下代码:

objD = D()
objD.func1()

Python 2.7 和 Python 3.5的输出结果都是一样的:

class C: func1

我们更改下A的定义,不显示的指定其继承object:

class A():
    def func1(self):
        print('class A: func1')

    def func2(self):
        print('class A: func2')

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

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

class D(B, C):
    pass

再来执行同样的代码:

objD = D()
objD.func1()

Python 2.7的输出结果:

class A: func1

Python 3.5的输出结果:

class C: func1

结论

前面我们已经说过了 在Python 3.x中无论是否显示指定继承object,所有的类都是新式类,那么我们根据上面的两个实例的输出结果可以得出这样的结论:在多继承的情况下,经典类查找父类属性或方法的顺序是深度优先,新式类查找父类属性的顺序是广度优先。

由于篇幅问题,还有几个与类相关的话题会单独再写一篇,比如类的实例化过程、反射以及异常类的介绍和使用等。

问题交流群:666948590

posted @ 2018-11-07 15:17  mugua250  阅读(167)  评论(0编辑  收藏  举报