类和继承相关面试题精讲

闻道有先后,术业有专攻。

—— 唐·韩愈·《师说》

Python 提供了完整的面向对象编程能力,将面向对象编程思想带到实际项目,可极大提高开发效率。因此,面向对象编程也是 Python 面试中必问的重要话题。

想要在 Python 项目中应用面向对象编程技术,除了掌握基本的理论概念外,还要理解 Python 对象模型、 类机制、继承与属性查找的关系、描述符以及元类等诸多知识。这些都是面试中经常考察的关键知识点。

本节精选若干典型面试题,以此抛砖引玉。

❓ 如何理解面向对象编程中的方法重写( overriding )和重载( overloading )?

请结合 Python 或其他编程语言进行说明。

方法重写 ( overriding )是指在子类中重新实现已在父类中定义的方法。

在面向对象编程语言中,子类可以继承父类中的方法,而无须重新编写相同的方法。但有时子类并不想原封不动地继承父类所有功能,想对父类中的某些方法进行修改,这就需要采用方法重写特性。方法重写不能发生在同一个类中,只能发生在子类中。

这是一个最简单的方法重写实例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Dog:

    def eat(self):
        print('yummy!')

    def yelp(self):
        print('woof!')

class Sleuth(Dog):

    def yelp(self):
        print('WOOF! WOOF! WOOF!')

Dog 是一个普通狗类,实现了 eatyelp 方法。猎犬 Sleuth 继承于 Dog 类,因此继承了父类的 eat 方法。注意到,我们对 yelp 方法进行 重写 ,以连续三个大写的 WOOF 突出猎犬铿锵有力的吠声。

这样一来,Sleuth 类继承了 Dog 中的 eat 方法,但自己的 yelp 方法覆盖了 Dog 中的相关定义:

1
2
3
4
5
>>> sleuth = Sleuth()
>>> sleuth.eat()
yummy!
>>> sleuth.yelp()
WOOF! WOOF! WOOF!

重载 ( overloading )既可发生在同一个类的方法之间,一般称作方法重载;亦可发生在普通函数间,一般称作函数重载。这个特性允许开发人员定义名字相同,但输入参数不同的类方法或者普通函数,即同名方法/函数的不同版本。

当程序调用重载方法或函数时,编译器将根据 参数个数参数类型 ,自动绑定正确的版本。由于方法/函数绑定时涉及类型检查,因此一般只有静态类型编程语言才支持重载特性。

Python 是一种动态类型编程语言,不支持方法重载,我们举一个简单的 C++ 程序作为例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>

using namespace std;
  
void print(int i) {
    cout << " Here is int " << i << endl;
}

void print(double  f) {
    cout << " Here is float " << f << endl;
}

void print(char const *c) {
    cout << " Here is char* " << c << endl;
}

int main() {
    print(10);
    print(10.10);
    print("ten");
    return 0;
}

程序定义了 print 函数,分为 3 个不同版本,分别以整型、双精度浮点以及常字符串为参数。main 函数中调用 print 函数时,编译器将根据参数类型,自动选择正确的 print 函数版本。以 print(10) 为例,由于参数 10 是一个整数,编译器可以据此推导出 void print(int i) 版本。

❓ Python 支持多继承吗?试说明多继承场景下实例对象类属性查找顺序?

Python 支持多继承,只需将基类按顺序逐一声明即可:

1
2
class Child(Base1, Base2):
    pass

当子类实例对象查找某个类属性时,先在子类 Child 中查找,再按定义顺序到基类中逐个中查找。如果基类也继承于其他类的基类,Python 将沿着继承链逐级回溯,最终来到 object

类属性查找顺序决定程序的行为,不可不察,特别是在复杂多继承场景下。实际上,Python 类属性查找顺序是一个特殊的拓扑排序。这个拓扑排序首先是深度优先的,其次需要确保多继承基类按照定义的顺序查找。

接下来,我们构造一个多继承关系网,考察决定属性查找顺序的重要因素,例子如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class A: pass

class B: pass

class C(A): pass

class D(C): pass

class E(B, A): pass

class F(D, E): pass

例子涉及各个类的继承关系图如下:

子类总比父类先被搜索,因此必须满足以下关系,拓扑排序即可胜任:

  • F 先于 D
  • F 先于 E
  • D 先于 C
  • C 先于 A
  • A 先于 object
  • E 先于 A
  • E 先于 B
  • B 先于 object

而根据多继承基类列表顺序,必须保证:

  • B 先于 A
  • D 先于 E

因此,F 最先被搜索,接着是 D ,然后按照深度优先的原则来到 C ;由于 B 先于 A ,不能接着搜索 A ;这时只能先搜索 E 分支,然后是 B ,再到 AAB 皆搜索过后才能搜索 object 。因此,完整的搜索顺序是这样的:

1
F -> D -> C -> E -> B -> A -> object

Python 完成类对象初始化后,通过 C3 算法计算类属性搜索顺序,并将其保存在 mro 属性中。我们可以据此确认推理结果:

1
2
>>> F.__mro__
(<class '__main__.F'>, <class '__main__.D'>, <class '__main__.C'>, <class '__main__.E'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>)

如果规则前后矛盾,Python 将抛 TypeError 异常。这是一个典型的例子:

1
2
3
4
5
6
7
8
9
class A: pass

class B: pass

class C(A, B): pass

class D(B, A): pass

class F(C, D): pass

对于 F 类,基类 C 要求 A 先于 B 被搜索,而基类 D 要求 B 先于 A 被搜索,前后矛盾:

1
2
3
4
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: Cannot create a consistent method resolution
order (MRO) for bases A, B

由于多继承存在一定的歧义性,实际项目开发一般不鼓励复杂的多继承关系。如果多继承不可避免,则需要严谨确认类属性搜索顺序。最好查看 mro 属性确认顺序符合预期,切勿想当然。

更多面试真题

❓ 试设计装饰器 mystaticmethod ,实现与 staticmethod 相同的功能

❓ Python 如何实现单例模式?试举例说明。

❓ 如果程序中有成千上万的 User 类实例对象,如何优化内存使用?

❓ 用 type 元类型创建一个等价的 MyFloat 类

上述的问题,你是否都能对答如流呢?点击 阅读原文,获取详细题解!

【Python源码剖析】系列文章首发于公众号【小菜学编程】,敬请关注:

【Python源码剖析】系列文章首发于公众号【小菜学编程】,敬请关注: