附答案 | 最强Python面试题之Python进阶题第三弹

本文正在参与【[ 面霸养成记 ] 】 征文活动,一起来聊聊校招的那些事吧,牛客周边和百元京东卡等你来领~

写在之前

大家好呀,我是帅蛋。

我来更新 Python 进阶面试题第三弹啦!!!让我们喊出那句话:Python 面试八股文都在帅蛋的【最强Python面试题】系列里啦!!!

每天一定出现的帅蛋,一定要记得点赞收藏呀!!!

顺便提一句,我所有和面试相关的内容都会放在#帅蛋的面试空间# 中,大家可以关注下这个话题~

我会尽我最大的努力帮助到大家哒!!!

主要内容

这些面试题是我结合自己的经验整理的,主要就是下面这 5 个专题:

  • Python 基础面试题
  • Python 进阶
  • Python 后台开发
  • 爬虫
  • 机器学习

已完成

Python 基础题

正在更新

Python 进阶题

对每道面试题我都会附带详细的答案,有些我觉得重要的内容会详细讲解,虽然是面试八股文,我还是希望大家不是只“知其然”,更得“知其所以然”

关于更新频率,每天我会更新 10 道题左右,总共会有差不多 200 道。

无论是准备面试还是自己学习,这份面试题绝对值得你去看,去学习。

大家可以关注我,再关注我,使劲关注我,不要错过每天的更新~

以下是正文

Python 进阶面试题第三弹正式开始,大家一定要记得点赞收藏,一起加油!

1、单例模式的应用场景有哪些?

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

(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()

接下来就是重写 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)

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

<__main__.Printer object at 0x10a940780>
<__main__.Printer object at 0x10a94d3c8>

可以看出,一般类中多次调用的内存地址不同(即 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 本质上是相同的一个对象。

2、什么是闭包?

我们都知道在数学中有闭包的概念,但此处我要说的闭包是计算机编程语言中的概念,它被广泛的使用于函数式编程。

关于闭包的概念,官方的定义颇为严格,也很难理解,在《Python语言及其应用》一书中关于闭包的解释我觉得比较好 -- 闭包是一个可以由另一个函数动态生成的函数,并且可以改变和存储函数外创建的变量的值。乍一看,好像还是比较很难懂,下面我用一个简单的例子来解释一下:

>>> a = 1
>>> def fun():
...     print(a)
...
>>> fun()
1
>>> def fun1():
...     b = 1
...
>>> print(b)
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
NameError: name 'b' is not defined

毋庸置疑,第一段程序是可以运行成功的,a = 1 定义的变量在函数里可以被调用,但是反过来,第二段程序则出现了报错。

在函数 fun() 里可以直接使用外面的 a = 1,但是在函数 fun1() 外面不能使用它里面所定义的 b = 1,如果我们根据作用域的关系来解释,是没有什么异议的,但是如果在某种特殊情况下,我们必须要在函数外面使用函数里面的变量,该怎么办呢?

我们先来看下面的例子:

>>> def fun():
...    a = 1
...    def fun1():
...            return a
...    return fun1
...
>>> f = fun()
>>> print(f())
1

如果你看过昨天的文章,你一定觉得的很眼熟,上述的本质就是我们昨天所讲的嵌套函数。

在函数 fun() 里面,有 a = 1 和 函数 fun1() ,它们两个都在函数 fun() 的环境里面,但是它们两个是互不干扰的,所以 a 相对于 fun1() 来说是自由变量,并且在函数 fun1() 中应用了这个自由变量 -- 这个 fun1() 就是我们所定义的闭包。

闭包实际上就是一个函数,但是这个函数要具有 1.定义在另外一个函数里面(嵌套函数);2.引用其所在环境的自由变量。

上述例子通过闭包在 fun() 执行完毕时,a = 1依然可以在 f() 中,即 fun1() 函数中存在,并没有被收回,所以 print(f()) 才得到了结果。

当我们在某些时候需要对事务做更高层次的抽象,用闭包会相当舒服。比如我们要写一个二元一次函数,如果不使用闭包的话相信你可以轻而易举的写出来,下面让我们来用闭包的方式完成这个一元二次方程:

>>> def fun(a,b,c):
...    def para(x):
...            return a*x**2 + b*x + c
...    return para
...
>>> f = fun(1,2,3)
>>> print(f(2))
11

上面的函数中,f = fun(1,2,3) 定义了一个一元二次函数的函数对象,x^2 + 2x + 3,如果要计算 x = 2 ,该一元二次函数的值,只需要计算 f(2) 即可,这种写法是不是看起来更简洁一些。

3、什么是装饰器?

「装饰器」作为 Python 高级语言特性中的重要部分,是修改函数的一种超级便捷的方式,适当使用能够有效提高代码的可读性和可维护性,非常的便利灵活。

「装饰器」本质上就是一个函数,这个函数的特点是可以接受其它的函数当作它的参数,并将其替换成一个新的函数(即返回给另一个函数)。

可能现在这么看的话有点懵,为了深入理解「装饰器」的原理,我们首先先要搞明白「什么是函数对象」,「什么是嵌套函数」,「什么是闭包」。关于这三个问题我在很久以前的文章中已经写过了,你只需要点击下面的链接去看就好了,这也是面试中常问的知识哦:

零基础学习 Python 之函数对象

零基础学习 Python 之嵌套函数

零基础学习 Python 之闭包

装饰器

搞明白上面的三个问题,其实简单点来说就是告诉你:函数可以赋值给变量,函数可嵌套,函数对象可以作为另一个函数的参数。

首先我们来看一个例子,在这个例子中我们用到了前面列出来的所有知识:

def first(fun):
    def second():
        print('start')
        fun()
        print('end')
        print fun.__name__
    return second

def man():
    print('i am a man()')

f = first(man)
f()

上述代码的执行结果如下所示:

start
i am a man()
end
man

上面的程序中,这个就是 first 函数接收了 man 函数作为参数,并将 man 函数以一个新的函数进行替换。看到这你有没有发现,这个和我在文章刚开始时所说的「装饰器」的描述是一样的。既然这样的话,那我们就把上述的代码改造成符合 Python 装饰器的定义和用法的样子,具体如下所示:

def first(func):
    def second():
        print('start')
        func()
        print('end')
        print (func.__name__)
    return second

@first
def man():
    print('i am a man()')

man()

上面这段代码和之前的代码的作用一模一样。区别在于之前的代码直接“明目张胆”的使用 first 函数去封装 man 函数,而上面这个是用了「语法糖」来封装 man 函数。至于什么是语法糖,不用细去追究,你就知道是类似「@first」这种形式的东西就好了。

在上述代码中「@frist」在 man 函数的上面,表示对 man 函数使用 first 装饰器。「@」 是装饰器的语法,「first」是装饰器的名称。

下面我们再来看一个复杂点的例子,用这个例子我们来更好的理解一下「装饰器」的使用以及它作为 Python 语言高级特性被人津津乐道的部分:

def check_admin(username):
    if username != 'admin':
        raise Exception('This user do not have permission')

class Stack:
    def __init__(self):
        self.item = []

    def push(self,username,item):
        check_admin(username=username)
        self.item.append(item)

    def pop(self,username):
        check_admin(username=username)
        if not self.item:
            raise Exception('NO elem in stack')
        return self.item.pop()

上述实现了一个特殊的栈,特殊在多了检查当前用户是否为 admin 这步判断,如果当前用户不是 admin,则抛出异常。上面的代码中将检查当前用户的身份写成了一个独立的函数 check_admin,在 push 和 pop 中只需要调用这个函数即可。这种方式增强了代码的可读性,减少了代码冗余,希望大家在编程的时候可以具有这种意识。

下面我们来看看上述代码用装饰器来写成的效果:

def check_admin(func):
    def wrapper(*args, **kwargs):
        if kwargs.get('username') != 'admin':
            raise Exception('This user do not have permission')
        return func(*args, **kwargs)
    return wrapper

class Stack:
    def __init__(self):
        self.item = []

    @check_admin
    def push(self,username,item):
        self.item.append(item)

    @check_admin
    def pop(self,username):
        if not self.item:
            raise Exception('NO elem in stack')
        return self.item.pop()

PS:可能很多人对 args 和 *kwargs 不太熟悉,详情请戳下面的链接:

Python 拓展之 args & *kwargs

对比一下使用「装饰器」和不使用装饰器的两种写法,乍一看,好像使用「装饰器」以后代码的行数更多了,但是你有没有发现代码看起来好像更容易理解了一些。在没有装饰器的时候,我们先看到的是 check_admin 这个函数,我们得先去想这个函数是干嘛的,然后看到的才是对栈的操作;而使用装饰器的时候,我们上来看到的就是对栈的操作语句,至于 check_admin 完全不会干扰到我们对当前函数的理解,所以使用了装饰器可读性更好了一些。

就和我在之前的文章中所讲的「生成器」那样,虽然 Python 的高级语言特性好用,但也不能乱用。装饰器的语法复杂,通过我们在上面缩写的装饰器就可以看出,它写完以后是很难调试的,并且使用「装饰器」的程序的速度会比不使用装饰器的程序更慢,所以还是要具体场景具体看待。

4、函数装饰器有什么作用?

装饰器本质上是一个 Python 函数,它可以在让其他函数在不需要做任何代码的变动的前提下增加额外的功能。装饰器的返回值也是一个函数的对象,它经常用于有切面需求的场景。 比如:插入日志、性能测试、事务处理、缓存、权限的校验等场景 有了装饰器就可以抽离出大量的与函数功能本身无关的雷同代码并发并继续使用。

5、生成器、迭代器的区别

迭代器是一个更抽象的概念,任何对象,如果它的类有 next 方法和 iter 方法返回自己本身,对于 string、list、dict、tuple 等这类容器对象,使用 for 循环遍历是很方便的。在后台 for 语句对容器对象调用 iter()函数,iter()是 python 的内置函数。iter()会返回一个定义了 next()方法的迭代器对象,它在容器中逐个访问容器内元素,next()也是 python 的内置函数。在没有后续元素时,next()会抛出一个 StopIteration 异常。

生成器(Generator)是创建迭代器的简单而强大的工具。它们写起来就像是正规的函数,只是在需要返回数据的时候使用 yield 语句。每次 next()被调用时,生成器会返回它脱离的位置(它记忆语句最后一次执行的位置和所有的数据值)

区别:生成器能做到迭代器能做的所有事,而且因为自动创建了 iter()和 next()方法,生成器显得特别简洁,而且生成器也是高效的,使用生成器表达式取代列表解析可以同时节省内存。除了创建和保存程序状态的自动方法,当发生器终结时,还会自动抛出 StopIteration 异常。

6、多线程交互,访问数据,如果访问到了就不访问了,怎么避免重读?

创建一个已访问数据列表,用于存储已经访问过的数据,并加上互斥锁,在多线程访问数据的时候先查看数据是否已经在已访问的列表中,若已存在就直接跳过。

7、Python 中 yield 的用法?

yield 就是保存当前程序执行状态。你用 for 循环的时候,每次取一个元素的时候就会计算一次。用yield 的函数叫 generator,和 iterator 一样,它的好处是不用一次计算所有元素,而是用一次算一次,可以节省很多空间。generator每次计算需要上一次计算结果,所以用 yield,否则一 return,上次计算结果就没了。

补充

在 Python 中,定义生成器必须要使用 yield 这个关键词,yield 翻译成中文有「生产」这方面的意思。在 Python 中,它作为一个关键词,是生成器的标志。接下来我们来看一个例子:

>>> def f():
...    yield 0
...    yield 1
...    yield 2
...
>>> f
<function f at 0x00000000004EC1E0>

上面是写了一个很简单的 f 函数,代码块是 3 个 yield 发起的语句,下面让我们来看看如何使用它:

>>> fa = f()
>>> fa
<generator object f at 0x0000000001DF1660>
>>> type(fa)
<class 'generator'>

上述操作可以看出,我们调用函数得到了一个生成器(generator)对象。

>>> dir(fa)
['__class__', '__del__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__iter__', '__le__', '__lt__', '__name__', '__ne__',
'__new__', '__next__', '__qualname__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'close', 'gi_code', 'gi_frame', 'gi_running', 'gi_yieldfrom', 'send', 'throw']

在上面我们看到了 iter() 和 next(),虽然我们在函数体内没有显示的写 iter() 和 next(),仅仅是写了 yield,但它就已经是「迭代器」了。既然如此,那我们就可以进行如下操作:

>>> fa = f()
>>> fa.__next__()
0
>>> fa.__next__()
1
>>> fa.__next__()
2
>>> fa.__next__()
Traceback (most recent call last):
 File "<stdin>", line 1, in <module>
StopIteration

从上面的简单操作可以看出:含有 yield 关键词的函数 f() 是一个生成器对象,这个生成器对象也是迭代器。所以就有了这样的定义:把含有 yield 语句的函数称为生成器,生成器是一种用普通函数语法定义的迭代器。

通过上面的例子可以看出,这个生成器(即迭代器)在定义的过程中并没有昨天讲的迭代器那样写 iter(),而是只用了 yield 语句,之后一个普普通通的函数就神奇的成了生成器,同样也具备了迭代器的特性。

yield 语句的作用,就是在调用的时候返回相应的值。下面我来逐行的解释一下上面例子的运行过程:

1.fa = f():fa 引用生成器对象。

2.fa.next():生成器开始执行,遇到了第一个 yield,然后返回后面的 0,并且挂起(即暂停执行)。

3.fa.next():从上次暂停的位置开始,继续向下执行,遇到第二个 yield,返回后面的值 1,再挂起。

4.fa.next():重复上面的操作。

5.fa.next():从上次暂停的位置开始,继续向下执行,但是后面已经没有 yield 了,所以 next() 发生异常。

8、谈下 python 的 GIL

GIL 是python的全局解释器锁,同一进程中假如有多个线程运行,一个线程在运行python程序的时候会霸占python解释器(加了一把锁即GIL),使该进程内的其他线程无法运行,等该线程运行完后其他线程才能运行。如果线程运行过程中遇到耗时操作,则解释器锁解开,使其他线程运行。所以在多线程中,线程的运行仍是有先后顺序的,并不是同时进行。

多进程中因为每个进程都能被系统分配资源,相当于每个进程有了一个python解释器,所以多进程可以实现多个进程的同时运行,缺点是进程系统资源开销大

9、Python 中的可变对象和不可变对象?

不可变对象,该对象所指向的内存中的值不能被改变。当改变某个变量时候,由于其所指的值不能被改变,相当于把原来的值复制一份后再改变,这会开辟一个新的地址,变量再指向这个新的地址。

可变对象,该对象所指向的内存中的值可以被改变。变量(准确的说是引用)改变后,实际上是其所指的值直接发生改变,并没有发生复制行为,也没有开辟新的出地址,通俗点说就是原地改变。

Python 中,数值类型(int 和 float)、字符串 str、元组 tuple 都是不可变类型。而列表 list、字典 dict、集合 set 是可变类型。

10、一句话解释什么样的语言能够用装饰器?

函数可以作为参数传递的语言,可以使用装饰器


以上就是今天的内容,我是帅蛋,我们明天见~

❤️ 欢迎关注我,有问题,找帅蛋,我最看不得别人迷茫!

❤️ 如果你觉得有帮助,希望爱学习的你不要吝啬三连击哟[点赞 + 收藏 + 评论]~

还有小小公众号 【编程文青李狗蛋】,聊聊迷茫吹吹牛皮~

#帅蛋的面试空间##面试八股文##秋招##Python##python面试#
全部评论
您好这个好像和基础题3一样呀,重复了
1 回复
分享
发布于 2022-07-27 14:49
点赞 回复
分享
发布于 2022-07-27 15:41
博乐游戏
校招火热招聘中
官网直投
还是大佬的文章看了不嗑睡
点赞 回复
分享
发布于 2022-07-27 15:54
越学越精神
点赞 回复
分享
发布于 2022-07-27 16:12
深夜学习,醍醐灌顶
点赞 回复
分享
发布于 2022-07-27 23:05
更新慢点
点赞 回复
分享
发布于 2022-07-29 13:38
面试必问的几个问题
点赞 回复
分享
发布于 2022-08-01 14:36
爱你佬,但是多花点时间更新就更爱你了
点赞 回复
分享
发布于 2022-08-01 21:57
大佬🐮
点赞 回复
分享
发布于 2022-08-09 12:03

相关推荐

32 106 评论
分享
牛客网
牛客企业服务