面向对象相关面试题

6. 面向对象

6.1 简述面向对象中__new__和__init__区别

难度指数: ★★
重要指数: ★★★

__new__ 是构造函数,用于创建新的对象,而__init__ 是初始化函数,__new__ 函数创建对象后,会调用__init__函数初始化对象的属性

__new__必须要有返回值,返回实例化出来的实例,这点在自己实现__new__时要特别注意;__init__有一个参数self,就是这个__new__返回的实例,__init__在__new__的基础上可以完成一些其它初始化的动作,__init__不需要返回值

6.2 写一段自定义异常代码

难度指数: ★★
重要指数: ★★★

这是一道简单的编程题,目的是要考察你对异常的理解,对继承的理解,以及你的编码习惯

class MyException(Exception):
    pass


raise MyException("测试异常")

这是最简单的自定义异常的方法,使用raise关键字可以抛出该异常,通过传入一个字符串,可以向外传递关键信息。

如果有需要,可以重载__str__方法,来更好的输出异常信息

class MyException(Exception):
    def __init__(self, value):
        super(MyException).__init__()     # 调用父类的__init__方法,可加可不加
        self.value = value
        self.min_value = 5

    def __str__(self):
        return f'规定的最小值是{self.min_value}, 实际结果值是{self.value}'

raise MyException(4)

6.3 写一个单列模式

难度指数: ★★★★
重要指数: ★★★

单例模式是最常使用的一种设计模式,该模式的目的是确保在一个系统中,一个类只有一个实例。python中最简单的实现方式当属借助模块,由于模块只会加载一次,因此如果你期望一个对象在系统中只存在一个,那么在某个模块中创建这个对象即可,此后其他模块对它进行引用,即使是多线程环境,也是安全的。

回答出基于模块编写一个单例模式,这个题目基本就算合格了,但如果想展示出更高的实力,就必须说出剩余的4种方法,并至少能手写其中一种。

剩余4种实现单例模式的方法,参见文章python实现单例模式的5种方法

6.4 列出几种魔法方法并简要介绍用途

难度指数: ★★
重要指数: ★★★

python有许多魔法方法,最常见的几个:

  1. __init__ 用于初始化对象实例
  2. __new__ 用于实例化对象,也就是通常理解的构造函数
  3. __call__ 可以让你像函数一样去调用对象

这个题目,你至少能回答出以上3个魔法方法,接下来,就是加分的回答了

魔法方法中还有__lt__, __le__, __eq__ 等可以重载比较运算符的方法

和属性相关的方法包括

  1. __getattr__ 定义当用户试图获取一个不存在的属性时的行为
  2. __getattribute__ 定义当该类的属性被访问时的行为
  3. __setattr__ 定义当一个属性被设置时的行为

回答到这里,基本也就可以了,如果想更进一步展示你对python的深入理解,还有两对魔法方法需要回答

  1. __enter__ 和 __exit__ 实现上下文管理器
  2. __iter__ 和 __next__ 实现迭代器

6.5 Python中的self是什么?

难度指数: ★★★
重要指数: ★★★

self是指对象方法里的第一个参数,self是调用方法的实例对象,谁调用方法,self就是谁

class T:
    def run(self):
        print(id(self))


t = T()
print(id(t))
t.run()

使用print输出实例t 和 self的内存地址,他们完全相同

6.6 什么是对象方法,类方法,静态方法?

难度指数: ★★★
重要指数: ★★★

这三个概念都是面向对象里的概念,类里的方法有3种,这3种方法有各自的定义方式和权限

名称 定义方法 权限 调用方法
实例方法 第一个参数必须是示例,一般命名为self 可以访问实例的属性和方法,也可以访问类的实例和方法 一般通过示例调用,类也可以调用
类方法 使用装饰器@classmethod修饰,第一个参数必须是当前的类对象,一般命名为cls 可以访问类的实例和方法 类实例和类都可以调用
静态方法 使用装饰器@staticmethod修饰,参数随意,没有self和cls 不可以访问类和实例的属性和方法 实例对象和类对象都可以调用

5.7 讲一讲你对python的元类的理解

难度指数: ★★★★
重要指数: ★★★

首先要回答python中一切皆对象,我们所定义的类,也是对象实例。那么我们所定义的类,是哪个类的实例呢?答案正是元类。

你平时所定义的类,并没有特殊说明其元类是什么,这个时候默认元类是type,这个type就是所谓的内置函数,但它其实是一个类。

一个类,如果继承了type,那么这个类就是元类,你可以自己定义一个元类。

class MyMeta(type):
    def __new__(cls, *args, **kwargs):
        _class = super().__new__(cls, *args, **kwargs)
        print(_class.__name__)
        return _class


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

我这里定义了类MyMeta, 它继承了type,因此是一个元类,在定义Animal这个类时,指定MyMeta 作为它的元类。Animal这个类,是元类MyMeta的实例对象。执行上面的代码,MyMeta类的__new__会被执行,因为它需要实例化一个对象,这个对象正是Animal。实例化一个Animal的对象时,则会执行MyMeta类的__call__方法。

要点总结:

  1. python一切皆对象
  2. 自定义的类是元类的示例对象,默认元类是type
  3. 一个类继承了type,它就是元类
  4. 元类的__new__会创建一个新的类,__call__ 会实例化一个自定义类的对象

5.8 如何理解类属性和实例属性

难度指数: ★★★★
重要指数: ★★★

我们所定义的类,是元类的实例对象,从这个角度看,类可以有自己的属性。而所谓的实例属性,指的是我们所定义的类的实例对象的属性。

当访问的实例对象的属性不存在时,会去类里寻找是否有与之同名的类属性,如果有则返回该属性。

所有实例对象都可以访问类属性,但不能通过实例对象来修改类的属性,一旦进行修改,本质上就是创建是对象自己的属性。

class T:
    name = '类T'

t1 = T()
t2 = T()

print(t1.name, t2.name)   # 类T

t1.name = 't1'     # 创建了属于t1 的name属性
print(t1.name, t2.name)   # t1 类T

5.9 说说Python中重写

难度指数: ★★★
重要指数: ★★★

封装,继承,多态是面向对象的三大特性,与其他编程语言不通,python中没有重载,但有重写。

重载是在一个类里一系列参数不同名字相同的方法,重写是继承后重新实现父类的方法。重写,是实现多态的基础。

class Base():
    def print(self):
        print("base")


class A(Base):
    def print(self):
        print("A")


a = A()
a.print()

A 重写了父类Base的print方法,a.print()执行的是A 自己的print方法,如果没有重写,则会执行Base的print方法。

5.10 阐述一下super的作用

难度指数: ★★★
重要指数: ★★★★★

为了解决python菱形继承导致基类方法重复调用的问题,引入了super()

何为菱形继承,A 是基类,B 和 C 都继承了A, 最后D 继承B和C, 这样就形成了菱形继承。

如果D的初始化函数(__init__)里调用了B和C的初始化函数,而B和C的初始化里又分别调用了A的初始化函数,那么最终A的初始化函数会被执行两次,这显然是非常危险的。

下面的例子就向你演示这种情况

class A:
    def __init__(self):
        self.attr_a = 1
        print('执行A的初始化函数')


class B(A):
    def __init__(self):
        A.__init__(self)
        self.attr_b = 2
        print('执行B的初始化函数')

class C(A):
    def __init__(self):
        A.__init__(self)
        self.attr_c = 3
        print('执行C的初始化函数')

class D(B, C):
    def __init__(self):
        B.__init__(self)
        C.__init__(self)
        self.attr_d = 4
        print('执行D的初始化函数')


d = D()
print(d.attr_a, d.attr_b, d.attr_c, d.attr_d)

代码执行结果

执行A的初始化函数
执行B的初始化函数
执行A的初始化函数
执行C的初始化函数
执行D的初始化函数
1 2 3 4

你应该注意到,类A的初始化函数被执行了两次,这是一个非常危险的行为,如果A的初始化函数执行了一些一个进程中只能执行一次的代码,这样的多进程就会导致严重的问题, super的引入就是为了解决这种问题。

class A:
    def __init__(self):
        self.attr_a = 1
        print('执行A的初始化函数')


class B(A):
    def __init__(self):
        super().__init__()
        self.attr_b = 2
        print('执行B的初始化函数')

class C(A):
    def __init__(self):
        super().__init__()
        self.attr_c = 3
        print('执行C的初始化函数')

class D(B, C):
    def __init__(self):
        super().__init__()
        self.attr_d = 4
        print('执行D的初始化函数')


d = D()
print(d.attr_a, d.attr_b, d.attr_c, d.attr_d)

程序执行结果

执行A的初始化函数
执行C的初始化函数
执行B的初始化函数
执行D的初始化函数
1 2 3 4

在D的初始化函数中,只使用了一行代码super().__init__(), 就将两个父类B和C的初始化函数都执行了, 而且不会重复执行A的初始化函数,这些都是super帮助我们完成的。

扫描关注, 与我技术互动

QQ交流群: 211426309

加入知识星球, 每天收获更多精彩内容

分享日常研究的python技术和遇到的问题及解决方案