Python 进阶面试题阅读指南 II - 闭包,装饰器,生成器,迭代器,线程交互,猴子补丁

Author: Xcourse   2023-Mar-19 16:32   Reads: 515

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

官方邮箱:enquiry@xcourse.sg

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

WhatsApp群:@Singapore Jobs & Internships

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

Telegram英文群:@Singapore Jobs

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

 

21. 什么是闭包?

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

关于闭包的概念,官方的定义颇为严格,也很难理解,在《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) 即可,这种写法是不是看起来更简洁一些。

 

22. 什么是装饰器?

【装饰器】作为 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()

PythonCopy

上述实现了一个特殊的栈,特殊在多了检查当前用户是否为 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()

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

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

 

23. 函数装饰器有什么作用?

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

 

24. 是否使用过functools中的函数?其作用是什么?

Python的functools模块用以为可调用对象(callable objects)定义高阶函数或操作。

简单地说,就是基于已有的函数定义新的函数。

所谓高阶函数,就是以函数作为输入参数,返回也是函数。

 

25. 生成器、迭代器的区别

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

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

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

 

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

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

 

27. 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() 发生异常。

 

28. 谈下 python 的 GIL

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

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

 

29. Python 中的可变对象和不可变对象?

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

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

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

 

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

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

 

31. Python 中 is 和==的区别?

is 判断的是 a 对象是否就是 b 对象,是通过 id 来判断的。

==判断的是 a 对象的值是否和 b 对象的值相等,是通过 value 来判断的。

 

32. 谈谈你对面向对象的理解?

面向对象是相对于面向过程而言的。

面向过程语言是一种基于功能分析的、以算法为中心的程序设计方法

面向对象是一种基于结构分析的、以数据为中心的程序设计思想。在面向对象语言中有一个有很重要东西,叫做类。面向对象有三大特性:封装、继承、多态。

 

33. Python 里 match 与 search 的区别?

match()函数只检测 RE 是不是在 string 的开始位置匹配

search()会扫描整个 string 查找匹配

也就是说 match()只有在 0 位置匹配成功的话才有返回,如果不是开始位置匹配成功的话,match()就返回 none。

 

34. 用 Python 匹配 HTML g tag 的时候, 和 有什么区别?

<.*>是贪婪匹配,会从第一个“<”开始匹配,直到最后一个“>”中间所有的字符都会匹配到,中间可能会包含“<>”。

<.*?>是非贪婪匹配,从第一个“<”开始往后,遇到第一个“>”结束匹配,这中间的字符串都会匹配到,但是

不会有“<>”。

 

35. Python 中的进程与线程的使用场景?

多进程适合在 CPU 密集型操作(cpu 操作指令比较多,如位数多的浮点运算)。

多线程适合在 IO 密集型操作(读写数据操作较多的,比如爬虫)。

 

36. 解释一下并行(parallel)和并发(concurrency)的区别?

并行(parallel)是指同一时刻,两个或两个以上时间同时发生。

并发(concurrency)是指同一时间间隔(同一段时间),两个或两个以上时间同时发生。

 

37. 如果一个程序需要进行大量的 IO 操作,应当使用并行还是并发?

并发

 

38. 如果程序需要进行大量的逻辑运算操作,应当使用并行还是并发?

并行

 

39. 在 Python 中可以实现并发的库有哪些?

  • 线程
  • 进程
  • 协程
  • threading

 

40. Python 如何处理上传文件?

Python 中使用 GET 方法实现上传文件,下面就是用 Get 上传文件的例子,client 用来发 Get 请求,server 用来收请求。

请求端代码

import requests #需要安装 requests

 

with open('test.txt', 'rb') as f:

requests.get('http://服务器 IP 地址:端口', data=f)

 

服务端代码

var http = require('http');

var fs = require('fs');

var server = http.createServer(function(req, res){

 

   var recData = "";

   req.on('data', function(data){

   recData += data;

   })

   req.on('end', function(data){

   recData += data;

   fs.writeFile('recData.txt', recData, function(err){

   console.log('file received');

       })

   })

   res.end('hello');

   })

server.listen(端口);

 

41. 请列举你使用过的 Python 代码检测工具?

  • 移动应用自动化测试 Appium
  • OpenStack 集成测试 Tempest
  • 自动化测试框架 STAF
  • 自动化测试平台 TestMaker
  • JavaScript 内存泄露检测工具 Leak Finder
  • Python 的 Web 应用验收测试 Splinter
  • 即插即用设备调试工具 UPnP-Inspector

 

42. python 程序中文输出问题怎么解决?

  • 方法一

用encode和decode,如:

import os.path

import xlrd,sys

 

Filename=’/home/tom/Desktop/1234.xls’

if not os.path.isfile(Filename):

   raise NameError,”%s is not a valid filename”%Filename

 

bk=xlrd.open_workbook(Filename)

shxrange=range(bk.nsheets)

print shxrange

 

for x in shxrange:

   p=bk.sheets()[x].name.encode(‘utf-8′)

   print p.decode(‘utf-8′)

 

  • 方法二

在文件开头加上

reload(sys)

sys.setdefaultencoding(‘utf8′)

 

43. Python 如何 copy 一个文件?

shutil 模块有一个 copyfile 函数可以实现文件拷贝

 

44. 如何用Python删除一个文件?

使用 os.remove(filename) 或者 os.unlink(filename)

 

45. 如何用 Python 来发送邮件?

python实现发送和接收邮件功能主要用到poplib和smtplib模块。

poplib用于接收邮件,而smtplib负责发送邮件。

#! /usr/bin/env python

#coding=utf-8

import sys

import time

import poplib

import smtplib

 

# 邮件发送函数

def send_mail():

    try:

       handle = smtplib.SMTP('smtp.126.com',25)

       handle.login('XXXX@126.com','**********')

       msg = 'To: XXXX@qq.com\r\nFrom:XXXX@126.com\r\nSubject:hello\r\n'

       handle.sendmail('XXXX@126.com','XXXX@qq.com',msg)

       handle.close()

       return 1

   except:

       return 0

 

# 邮件接收函数

def accpet_mail():

   try:

       p=poplib.POP3('pop.126.com')

       p.user('pythontab@126.com')

       p.pass_('**********')

       ret = p.stat() #返回一个元组:(邮件数,邮件尺寸)

      #p.retr('邮件号码')方法返回一个元组:(状态信息,邮件,邮件尺寸)

   except poplib.error_proto,e:

       print "Login failed:",e

       sys.exit(1)

 

# 运行当前文件时,执行sendmail和accpet_mail函数

if __name__ == "__main__":

   send_mail()

   accpet_mail()

 

46. 当退出 Python 时,是否释放全部内存?

不是。

循环引用其它对象或引用自全局命名空间的对象的模块,在Python退出时并非完全释放。另外,也不会释放C库保留的内存部分。

 

47. 什么是猴子补丁?

在运行期间动态修改一个类或模块。

>>> class A:

   def func(self):

       print("Hi")

>>> def monkey(self):

            print "Hi, monkey"

>>> m.A.func = monkey

>>> a = m.A()

>>> a.func()

 

运行结果为:

Hi, Monkey

 


Tags: interview python backend

Topics: 面经