Python 进阶面试题阅读指南 I - Class, 断言,finally,map, reduce, lambda, 单例

Author: Xcourse   2023-Mar-14 23:20   Reads: 448

欢迎加入微信工作内部分享群,每天发布新的精选高薪工作。

官方邮箱:enquiry@xcourse.sg

微信分享群:@新加坡工作内部分享群

WhatsApp群:@Singapore Jobs & Internships

Telegram中文群:@新加坡工作内部分享群

Telegram英文群:@Singapore Jobs

------------------------------------------------------------------------------------------------------

 

1. Python 中类方法、类实例方法、静态方法有何区别?

类方法:是类对象的方法,在定义时需要在上方使用“@classmethod”进行装饰,形参为 cls,表示类对象,类对象和实例对象都可调用

类实例方法:是类实例化对象的方法,只有实例对象可以调用,形参为 self,指代对象本身

静态方法:是一个任意函数,在其上方使用“@staticmethod”进行装饰,可以用对象直接调用,静态方法实际上跟该类没有太大关系

 

2. Python 的内存管理机制及调优手段?

内存管理机制:引用计数、垃圾回收、内存池。

  • 引用计数

引用计数是一种非常高效的内存管理手段, 当一个 Python 对象被引用时其引用计数增加 1, 当其不再被一个变量引用时则计数减 1. 当引用计数等于 0 时对象被删除。

  • 垃圾回收

(1) 引用计数

引用计数也是一种垃圾收集机制,而且也是一种最直观,最简单的垃圾收集技术。当 Python 的某

个对象的引用计数降为 0 时,说明没有任何引用指向该对象,该对象就成为要被回收的垃圾了。比如

某个新建对象,它被分配给某个引用,对象的引用计数变为 1。如果引用被删除,对象的引用计数为 0,

那么该对象就可以被垃圾回收。不过如果出现循环引用的话,引用计数机制就不再起有效的作用了

(2)标记清除

如果两个对象的引用计数都为 1,但是仅仅存在他们之间的循环引用,那么这两个对象都是需要被

回收的,也就是说,它们的引用计数虽然表现为非 0,但实际上有效的引用计数为 0。所以先将循环引

用摘掉,就会得出这两个对象的有效计数。

(3) 分代回收

从前面“标记-清除”这样的垃圾收集机制来看,这种垃圾收集机制所带来的额外操作实际上与系统

中总的内存块的数量是相关的,当需要回收的内存块越多时,垃圾检测带来的额外操作就越多,而垃圾

回收带来的额外操作就越少;反之,当需回收的内存块越少时,垃圾检测就将比垃圾回收带来更少的额

外操作。

  • 举个例子:

当某些内存块 M 经过了 3 次垃圾收集的清洗之后还存活时,我们就将内存块 M 划到一个集合 A 中去,而新分配的内存都划分到集合 B 中去。当垃圾收集开始工作时,大多数情况都只对集合 B 进行垃圾回收,而对集合 A 进行垃圾回收要隔相当长一段时间后才进行,这就使得垃圾收集机制需要处理的内存少了,效率自然就提高了。在这个过程中,集合 B 中的某些内存块由于存活时间长而会被转移到集合 A 中,当然,集合 A 中实际上也存在一些垃圾,这些垃圾的回收会因为这种分代的机制而被延迟。

  • 内存池

(1) Python 的内存机制呈现金字塔形状,-1,-2 层主要有操作系统进行操作

(2) 第 0 层是 C 中的 malloc,free 等内存分配和释放函数进行操作

(3)第 1 层和第 2 层是内存池,有 Python 的接口函数 PyMem_Malloc 函数实现,当对象小于

256K 时有该层直接分配内存

(4) 第 3 层是最上层,也就是我们对 Python 对象的直接操作

Python 在运行期间会大量地执行 malloc 和 free 的操作,频繁地在用户态和核心态之间进行切换,这将严重影响 Python 的执行效率。为了加速 Python 的执行效率,Python 引入了一个内存池机制,用于管理对小块内存的申请和释放。

Python 内部默认的小块内存与大块内存的分界点定在 256 个字节,当申请的内存小于 256 字节时,PyObject_Malloc 会在内存池中申请内存;当申请的内存大于 256 字节时,PyObject_Malloc 的行为将蜕化为 malloc 的行为。当然,通过修改 Python 源代码,我们可以改变这个默认值,从而改变 Python 的默认内存管理行为。

 

3. 内存泄露是什么?如何避免?

由于疏忽或错误造成程序未能释放已经不再使用的内存的情况。

内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。导致程序运行速度减慢甚至系统崩溃等严重后果。

  • del() 函数的对象间的循环引用是导致内存泄漏的主凶。
  • 不使用一个对象时使用:del object 来删除一个对象的引用计数就可以有效防止内存泄漏问题。
  • 通过 Python 扩展模块 gc 来查看不能回收的对象的详细信息。
  • 可以通过 sys.getrefcount(obj) 来获取对象的引用计数,并根据返回值是否为 0 来判断是否内存
  • 泄漏。

 

4. Python 函数调用的时候参数的传递方式是值传递还是引用传递?

Python 的参数传递有:位置参数、默认参数、可变参数、关键字参数。函数的传值到底是值传递还是引用传递,要分情况:

  • 不可变参数用值传递

像整数和字符串这样的不可变对象,是通过拷贝进行传递的,因为你无论如何都不可能在原处改变不可变对象

  • 可变参数是引用传递的

比如像列表,字典这样的对象是通过引用传递、和 C 语言里面的用指针传递数组很相似,可变对象能在函数内部改变。

 

5. 对缺省参数的理解?

缺省参数指在调用函数的时候没有传入参数的情况下,调用默认的参数,在调用函数的同时赋值时,所传入的参数会替代默认参数。

*args 是不定长参数,他可以表示输入参数是不确定的,可以是任意多个。

**kwargs 是关键字参数,赋值的时候是以键 = 值的方式,参数是可以任意多对在定义函数的时候\n不确定会有多少参数会传入时,就可以使用两个参数。

  • 补充
  1. *args

如果你之前学过 C 或者 C++,看到星号的第一反应可能会认为这个与指针相关,然后就开始方了,其实放宽心,Python 中是没有指针这个概念的。

  1. **kwargs

使用两个星号是收集关键字参数,可以将参数收集到一个字典中,参数的名字是字典的 “键”,对应的参数的值是字典的 “值”。

 

6. 为什么函数名字可以当做参数用?

Python 中一切皆对象,函数名是函数在内存中的空间,也是一个对象。

 

7. Python 中 pass 语句的作用是什么?

在编写代码时只写框架思路,具体实现还未编写就可以用 pass 进行占位,使程序不报错,不会进行任何操作。

 

8. 面向对象中super的作用?

super() 函数是用于调用父类(超类)的一个方法。

super 是用来解决多重继承问题的,直接用类名调用父类方法在使用单继承的时候没问题,但是如果使用多继承,会涉及到查找顺序(MRO)、重复调用(钻石继承)等种种问题。

MRO 就是类的方法解析顺序表, 其实也就是继承父类方法时的顺序表。

作用:

  • 根据 mro 的顺序执行方法
  • 主动执行 Base 类的方法

 

9. json序列化时,默认遇到中文会转换成unicode,如果想要保留中文怎么办?

import json

a = json.dumps({"ddf": "你好"}, ensure_ascii=False)

print(a)

# {"ddf": "你好"}

 

10. 什么是断言?应用场景?

assert断言——声明其布尔值必须为真判定,发生异常则为假。

info = {}

info['name'] = 'egon'

info['age'] = 18

# 用assert取代上述代码:

assert ('name' in info) and ('age' in info)

 

设置一个断言目的就是要求必须实现某个条件。

 

11. 有用过with statement吗?它的好处是什么?

with语句的作用是通过某种方式简化异常处理,它是所谓的上下文管理器的一种

用法举例如下:

with open('output.txt', 'w') as f:

      f.write('Hi there!')

 

当你要成对执行两个相关的操作的时候,这样就很方便,以上便是经典例子,with语句会在嵌套的代码执行之后,自动关闭文件。

这种做法的还有另一个优势就是,无论嵌套的代码是以何种方式结束的,它都关闭文件。

如果在嵌套的代码中发生异常,它能够在外部exception handler catch异常前关闭文件。

如果嵌套代码有return/continue/break语句,它同样能够关闭文件。

 

12. 简述 Python 在异常处理中,else 和 finally 的作用分别是什么?

如果一个 Try – exception 中,没有发生异常,即 exception 没有执行,那么将会执行 else 语句的内容。反之,如果触发了 Try – exception(异常在 exception 中被定义),那么将会执行exception

中的内容,而不执行 else 中的内容。

如果 try 中的异常没有在 exception 中被指出,那么系统将会抛出 Traceback(默认错误代码),并且终止程序,接下来的所有代码都不会被执行,但如果有 Finally 关键字,则会在程序抛出 Traceback 之前(程序最后一口气的时候),执行 finally 中的语句。这个方法在某些必须要结束的操作中颇为有用,如释放文件句柄,或释放内存空间等。

 

13. map 函数和 reduce 函数?

  • (1) 从参数方面来讲:

map()包含两个参数,第一个参数是一个函数,第二个是序列(列表 或元组)。其中,函数(即 map 的第一个参数位置的函数)可以接收一个或多个参数。

reduce()第一个参数是函数,第二个是序列(列表或元组)。但是,其函数必须接收两个参数。

  • (2) 从对传进去的数值作用来讲:

map()是将传入的函数依次作用到序列的每个元素,每个元素都是独自被函数“作用”一次 。

reduce()是将传人的函数作用在序列的第一个元素得到结果后,把这个结果继续与下一个元素作用(累积计算)。

 

补充 Python 特殊函数

  • lambda 函数

lambda 是一个可以只用一行就能解决问题的函数,让我们先看下面的例子:

lambda 后面直接跟变量,变脸后面是冒号,冒号后面是表达式,表达式的计算结果就是本函数的返回值。

在这里有一点需要提醒的是,虽然 lambda 函数可以接收任意多的参数并且返回单个表达式的值,但是 lambda 函数不能包含命令且包含的表达式不能超过一个。如果你需要更多复杂的东西,你应该去定义一个函数。

lambda 作为一个只有一行的函数,在你具体的编程实践中可以选择使用,虽然在性能上没什么提升,但是看着舒服呀。

  • map 函数

map 是 Python 的一个内置函数,它的基本格式是:map(func, seq)。

func 是一个函数对象,seq 是一个序列对象,在执行的时候,seq 中的每个元素按照从左到右的顺序依次被取出来,塞到 func 函数里面,并将 func 的返回值依次存到一个列表里。

对于 map 要主要理解以下几个点就好了:

1.对可迭代的对象中的每一个元素,依次使用 fun 的方法(其实本质上就是一个 for 循环)。

2.将所有的结果返回一个 map 对象,这个对象是个迭代器。

3. map 还不和 lambda 一样对性能没有什么提高,map 在性能上的优势也是杠杠的。

  • filter 函数

filter 翻译过来的意思是 “过滤器”,在 Python 中,它也确实是起到的是过滤器的作用。

  • reduce 函数

reduce 函数的第一个参数是一个函数,第二个参数是序列类型的对象,将函数按照从左到右的顺序作用在序列上。

map 相当于是上下运算的,而 reduce 是从左到右逐个元素进行运算。

 

14. 递归函数停止的条件?

递归的终止条件一般定义在递归函数内部,在递归调用前要做一个条件判断,根据判断的结果选择是继续调用自身,还是 return;返回终止递归。

终止的条件:

(1) 判断递归的次数是否达到某一限定值

(2) 判断运算的结果是否达到某个范围等,根据设计的目的来选择

 

15. 回调函数,如何通信的?

回调函数是把函数的指针(地址)作为参数传递给另一个函数,将整个函数当作一个对象,赋值给调用的函数。

 

16. setattr,getattr,delattr函数使用详解?

1.setattr(self,name,value):如果想要给 name 赋值的话,就需要调用这个方法。

2.getattr(self,name):如果 name 被访问且它又不存在,那么这个方法将被调用。

3.delattr(self,name):如果要删除 name 的话,这个方法就要被调用了。

 

17. 请描述抽象类和接口类的区别和联系?

(1) 抽象类

规定了一系列的方法,并规定了必须由继承类实现的方法。由于有抽象方法的存在,所以抽象类不能实例化。可以将抽象类理解为毛坯房,门窗、墙面的样式由你自己来定,所以抽象类与作为基类的普通类的区别在于约束性更强。

(2) 接口类

与抽象类很相似,表现在接口中定义的方法,必须由引用类实现,但他与抽象类的根本区别在于用途:与不同个体间沟通的规则(方法),你要进宿舍需要有钥匙,这个钥匙就是你与宿舍的接口,你的同室也有这个接口,所以他也能进入宿舍,你用手机通话,那么手机就是你与他人交流的接口。

(3) 区别和关联

  • 接口是抽象类的变体,接口中所有的方法都是抽象的。而抽象类中可以有非抽象方法。抽象类是声明方法的存在而不去实现它的类。
  • 接口可以继承,抽象类不行。
  • 接口定义方法,没有实现的代码,而抽象类可以实现部分方法。
  • 接口中基本数据类型为 static 而抽类象不是。
  • 接口可以继承,抽象类不行。
  • 可以在一个类中同时实现多个接口。
  • 接口的使用方式通过 implements 关键字进行,抽象类则是通过继承 extends 关键字进行。

 

18. 请描述方法重载与方法重写?

(1)方法重载

是在一个类里面,方法名字相同,而参数不同。返回类型呢?可以相同也可以不同。重载是让类以统一的方式处理不同类型数据的一种手段。

(2) 方法重写

子类不想原封不动地继承父类的方法,而是想作一定的修改,这就需要采用方法的重写。方法重写又称方法覆盖。

 

19. 什么是 lambda 函数? 有什么好处?

lambda 函数是一个可以接收任意多个参数(包括可选参数)并且返回单个表达式值的函数

1、lambda 函数比较轻便,即用即仍,很适合需要完成一项功能,但是此功能只在此一处使用,连名字都很随意的情况下

2、匿名函数,一般用来给 filter, map 这样的函数式编程服务

3、作为回调函数,传递给某些应用,比如消息处理

补充

  • lambda 是一个可以只用一行就能解决问题的函数。
  • lambda 函数的具体使用方法:lambda 后面直接跟变量,变脸后面是冒号,冒号后面是表达式,表达式的计算结果就是本函数的返回值。
  • 在这里有一点需要提醒的是,虽然 lambda 函数可以接收任意多的参数并且返回单个表达式的值,但是 lambda 函数不能包含命令且包含的表达式不能超过一个。如果你需要更多复杂的东西,你应该去定义一个函数。
  • lambda 作为一个只有一行的函数,在你具体的编程实践中可以选择使用,虽然在性能上没什么提升,但是看着舒服呀。

 

20. 单例模式的应用场景有哪些?

单例模式应用的场景一般发现在以下条件下:

(1)资源共享的情况下,避免由于资源操作时导致的性能或损耗等。如日志文件,应用配置。

(2)控制资源的情况下,方便资源之间的互相通信。如线程池等。 1.网站的计数器 2.应用配置 3.多线程池 4.数据库配置,数据库连接池 5.应用程序的日志应用….

补充

01.单例设计模式

「单例设计模式」估计对很多人来说都是一个陌生的概念,其实它就环绕在你的身边。比如我们每天必用的听歌软件,同一时间只能播放一首歌曲,所以对于一个听歌的软件来说,负责音乐播放的对象只有一个;再比如打印机也是同样的道理,同一时间打印机也只能打印一份文件,同理负责打印的对象也只有一个。

结合说的听歌软件和打印机都只有唯一的一个对象,就很好理解「单例设计模式」。

 

 单例设计模式确保一个类只有一个实例,并提供一个全局访问点。

 

「单例」就是单个实例,我们在定义完一个类的之后,一般使用「类名()」的方式创建一个对象,而单例设计模式解决的问题就是无论执行多少遍「类名()」,返回的对象内存地址永远是相同的。

02.__new__ 方法

当我们使用「类名()」创建对象的时候,Python 解释器会帮我们做两件事情:第一件是为对象在内存分配空间,第二件是为对象进行初始化。初始化(init)我们已经学过了,那「分配空间」是哪一个方法呢?就是我们这一小节要介绍的 new 方法。

那这个 new 方法和单例设计模式有什么关系呢?单例设计模式返回的对象内存地址永远是相同的,这就意味着在内存中这个类的对象只能是唯一的一份,为达到这个效果,我们就要了解一下为对象分配空间的 new 方法。

明确了这个目的以后,接下来让我们看一下 new 方法。new 方法在内部其实做了两件时期:第一件事是为「对象分配空间」,第二件事是「把对象的引用返回给 Python 解释器」。当 Python 的解释器拿到了对象的引用之后,就会把对象的引用传递给 init 的第一个参数 self,init 拿到对象的引用之后,就可以在方法的内部,针对对象来定义实例属性。

这就是 new 方法和 init 方法的分工。

总结一下就是:之所以要学习 new 方法,就是因为需要对分配空间的方法进行改造,改造的目的就是为了当使用「类名()」创建对象的时候,无论执行多少次,在内存中永远只会创造出一个对象的实例,这样就可以达到单例设计模式的目的。

03.重写 __new__ 方法

在这里我用一个 new 方法的重写来做一个演练:首先定义一个打印机的类,然后在类里重写一下 new 方法。通过对这个方法的重写来强化一下 new 方法要做的两件事情:在内存中分配内存空间 & 返回对象的引用。同时验证一下,当我们使用「类名()」创建对象的时候,Python 解释器会自动帮我们调用 new 方法。

首先我们先定义一个打印机类 Printer,并创建一个实例:

class Printer():

   def __init__(self):

       print("打印机初始化")

# 创建打印机对象

printer = Printer()

PythonCopy

接下来就是重写 new 方法,在此之前,先说一下注意事项,只要⚠️了这几点,重写 new 就没什么难度:

重写 new 方法一定要返回对象的引用,否则 Python 的解释器得不到分配了空间的对象引用,就不会调用对象的初始化方法;

new 是一个静态方法,在调用时需要主动传递 cls 参数。

# 重写 __new__ 方法

class Printer():

   def __new__(cls, *args, **kwargs):

       # 可以接收三个参数

       # 三个参数从左到右依次是 class,多值元组参数,多值的字典参数

       print("this is rewrite new")

       instance = super().__new__(cls)

       return instance

   def __init__(self):

       print("打印机初始化")

# 创建打印机对象

player = Printer()

print(player)

 

上述代码对 new 方法进行了重写,我们先来看一下输出结果:

this is rewrite new

打印机初始化

<__main__.Printer object at 0x10fcd2ba8>

 

上述的结果打印出了 new 方法和 init 方法里的内容,同时还打印了类的内存地址,顺序正好是我们在之前说过的。new 方法里的三行代码正好做了在本小节开头所说的三件事:

print(this is rewrite new):证明了创建对象时,new 方法会被自动调用;

instance = super().new(cls):为对象分配内存空间(因为 new 本身就有为对象分配内存空间的能力,所以在这直接调用父类的方法即可);

return instance:返回对象的引用。

 

04.设计单例模式

说了这么多,接下来就让我们用单例模式来设计一个单例类。乍一看单例类看起来比一般的类更唬人,但其实就是差别在一点:单例类在创建对象的时候,无论我们调用多少次创建对象的方法,得到的结果都是内存中唯一的对象。

可能到这有人会有疑惑:怎么知道用这个类创建出来的对象是同一个对象呢?其实非常的简单,我们只需要多调用几次创建对象的方法,然后输出一下方法的返回结果,如果内存地址是相同的,说明多次调用方法返回的结果,本质上还是同一个对象。

class Printer():

   pass

printer1 = Printer()

print(printer1)

printer2 = Printer()

print(printer2)

PythonCopy

上面是一个一般类的多次调用,打印的结果如下所示:

<__main__.Printer object at 0x10a940780>

<__main__.Printer object at 0x10a94d3c8>

PythonCopy

可以看出,一般类中多次调用的内存地址不同(即 printer1 和 printer2 是两个完全不同的对象),而单例设计模式设计的单例类 Printer(),要求是无论调用多少次创建对象的方法,控制台打印出来的内存地址都是相同的。

那么我们该怎么实现呢?其实很简单,就是多加一个「类属性」,用这个类属性来记录「单例对象的引用」。

为什么要这样呢?其实我们一步一步的来想,当我们写完一个类,运行程序的时候,内存中其实是没有这个类创建的对象的,我们必须调用创建对象的方法,内存中才会有第一个对象。在重写 new 方法的时候,我们用 instance = super().new(cls) ,为对象分配内存空间,同时用 istance 类属性记录父类方法的返回结果,这就是第一个「对象在内存中的返回地址」。当我们再次调用创建对象的方法时,因为第一个对象已经存在了,我们直接把第一个对象的引用做一个返回,而不用再调用 super().new(cls) 分配空间这个方法,所以就不会在内存中为这个类的其它对象分配额外的内存空间,而只是把之前记录的第一个对象的引用做一个返回,这样就能做到无论调用多少次创建对象的方法,我们永远得到的是创建的第一个对象的引用。

这个就是使用单例设计模式解决在内存中只创建唯一一个实例的解决办法。下面我就根据上面所说的,来完成单例设计模式。

class Printer():

   instance = None

   def __new__(cls, *args, **kwargs):

       if cls.instance is None:

           cls.instance = super().__new__(cls)

   return cls.instance

printer1 = Printer()

print(printer1)

printer2 = Printer()

print(printer2)

上述代码很简短,首先给类属性复制为 None,在 new 方法内部,如果 instance 为 None,证明第一个对象还没有创建,那么就为第一个对象分配内存空间,如果 instance 不为 None,直接把类属性中保存的第一个对象的引用直接返回,这样在外界无论调用多少次创建对象的方法,得到的对象的内存地址都是相同的。

下面我们运行一下程序,来看一下结果是不是能印证我们的说法:

<__main__.Printer object at 0x10f3223c8>

<__main__.Printer object at 0x10f3223c8>

上述输出的两个结果可以看出地址完全一样,这说明 printer1 和 printer2 本质上是相同的一个对象。

 


Tags: interview python backend

Topics: 面经