难度指数: ★★★
重要指数: ★★★
如果面试时被问及python的GIL,那么你应当从以下2个方面回答
GIL,即全局解释器锁 ,一个被广泛吐槽的技术,即便python3.9已经到来,这个GIL依然存在,而且似乎将继续存在。
为了解决多线程之间数据完整性和状态同步的问题,引入了GIL, 它可以保证多个原生线程不会并发执行 Python 字节码。
对于GIL的理解,有一点我们必须严肃的弄清楚,GIL并不是python的特性,而是实现Python解析器(CPython,我们平时默认下载安装使用的解释器)时所引入的一个概念,这个就好比C++是一套语言标准,但可以用 GCC,INTEL C++,Visual C++ 等编译器来编译,编译出来的可执行程序也就不一样。你可以使用 CPython,PyPy , JPython 来执行python的代码,CPython里有GIL,而JPython 就没有。
Python 的维护者们不是没考虑过将GIL移除,可是已经有太多的库依赖这个特性,积重难返。
多线程原本是为了更好的利用CPU的多核资源,但是由于GIL的存在,使得同一个时刻,只能有一个python的线程在运行,显然,这是对资源的无耻浪费。
因此,你无法将python的多线程作为提升程序性能的一种手段,对于CPU密集型任务,请使用多进程。虽然如此,python的多线程也并非如大家所吐槽的那样鸡肋,对于I/O密集型任务,python多线程仍然有用武之地。
当一个线程I/O等待时,会释放GIL,这样其他线程就可以获得GIL来执行自己的任务,这就是你编写多线程爬虫加快爬取速度的技术依据。
最后做如下总结,面试的时候按照以下要点回答
难度指数: ★★★★
重要指数: ★★★★
回答这个问题,要抓住3个要点
python对象的核心则是一个结构体
typedef struct_object {
int ob_refcnt;
struct_typeobject *ob_type;
} PyObject;
当一个对象有新的引用时,ob_refcnt加1,引用它的对象被删除时,ob_refcnt减1,当ob_refcnt=0时,这个对象的生命就结束了,这时,垃圾回收机制就会启动将这个对象回收。引用计数的机制是不受标记清除和分代回收影响的,只要引用计数为0,对象就会被回收。
引入标记清除机制,是为了解决循环引用的问题,下面是一个简单的循环引用的示例
dict_1 = {}
dict_2 = {}
dict_1['a'] = dict_2
dict_2['b'] = dict_1
由于循环引用的存在,使得对象的引用计数在程序运行期间始终不为0,内存无法被释放。
标记清除分为两个阶段,第一阶段是标记阶段。GC会把所有活动对象打上标记,这些活动的对象就如同一个点,他们之间的引用关系构成边,最终点个边构成了一个有向图;2阶段,搜索清除阶段,从根对象(root)出发,沿着有向边遍历整个图,不可达的对象就是需要清理的垃圾对象。这个根对象就是全局对象,调用栈,寄存器。
分代回收建立标记清除的基础之上,是一种以空间换时间的操作方式。标记清除可以回收循环引用的垃圾,但是,回收的频次是需要控制的,如果时时刻刻都回收,势必会影响python程序的性能。
分代回收,根据内存中对象的存活时间将他们分为3代,新生的对象放入到0代,如果一个对象能在第0代的垃圾回收过程中存活下来,GC就会将其放入到1代中,如果1代里的对象在第1代的垃圾回收过程中存活下来,则会进入到2代。当分配对象的个数减去释放对象的个数的差值大于700时,就会产生一次0代回收, 10次0代回收会导致一次1代回收, 10次1代回收会导致一次2代回收。通过gc.get_threshold() 和 gc.set_threshold 可以获取和设置触发分代回收的临界条件。
难度指数: ★★
重要指数: ★★★
PYTHONPATH 是环境变量,你可以在终端通过export命令对其进行设置,类似下面这样
export PYTHONPATH=/root/studyflask
这样设置的环境变量,是暂时的,终端关闭后就失效了,如果想让在这台机器上生效,可以/etc/profile,或者.bashrc中进行设置,这和你设置PATH环境变量是一样的方法。
设置PYTHONPATH的目的是为了改变sys.path 的值,影响python程序执行时import模块的过程。像上面那样设置PYTHONPATH后,打开交互式解释器
>>> import sys
>>> sys.path
['', '/root/studyflask', '/root/.pyenv/versions/3.6.5/lib/python36.zip', '/root/.pyenv/versions/3.6.5/lib/python3.6', '/root/.pyenv/versions/3.6.5/lib/python3.6/lib-dynload', '/root/.pyenv/versions/3.6.5/lib/python3.6/site-packages']
你可以看到,刚刚设置的目录放在了sys.path列表的第2个索引位置,当程序执行import命令引入模块时,会先从当前文件目录进行查找,招不到时,从/root/studyflask目录进行查找,以此类推。
除了设置PYTHONPATH可以修改sys.path外,你也可以在程序里直接对sys.path进行修改,或者创建.pth文件,在site-packages目录新建一个.pth文件,并在文件中加入搜索模块的路径
/root/test
那么这个搜索路径也会出现在sys.path中。通过设置PYTHONPATH环境变量,会添加路径到sys.path列表中靠前的位置,而通过编写.pth文件 添加的路径会放在sys.path靠后的位置。
关于搜索路径,建议你阅读python修改sys.path的三种方法
难度指数: ★★★
重要指数: ★★★
我们需要关心的是sys.path里的内容,python脚本在执行时,会将当前路径插入到sys.path列表中索引为0的位置,因此这个目录是sys.path中最先被搜索的目录,你自己代码里的模块就是在这里被搜索到的。
sys.path的第二部分是安装python时内置进去的,比如
/Library/Frameworks/Python.framework/Versions/3.6/lib/python36.zip
/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6
/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/lib-dynload
/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/site-packages
sys.path的第三部分是你安装的第三方模块
更详细的讲解,参见import 原理
难度指数: ★★
重要指数: ★★
想要理解python包的概念,首先要理解python模块的概念。为了提高代码的复用率,我们将一组相关的python对象放在一个.py 文件中,他们可能是一些函数,或者是一些类,这个脚本就是一个模块。
包的概念,是建立在模块的概念之上的。实践中,会按照模块的用途放进不同的文件夹中,比如和数据库相关的模块都放在db文件夹中,和项目配置相关的模块会被放进conf文件夹中,这些文件夹中都会有一个__init__.py 文件,这些文件夹,就是所谓的包。Python 允许用户把目录当成模块看待。
难度指数: ★★★
重要指数: ★★★
这个题目过于开放,因此也就没有所谓的标准答案,你只要回答出一些关键点即可,如果面试官就某一个关键点继续提问,这就要看你对关键点的理解层次了
难度指数: ★★
重要指数: ★★★
自省就是能够获得自身的结构和方法,给开发者可以灵活的调用,给定一个对象,返回该对象的所有属性和函数列表,或给定对象和该对象的函数或者属性的名字,返回对象的函数或者属性实例。
你可以使用dir来查看一个对象的详细信息,包括他的属性和拥有的方法。python专门提供了一个inspect模块,提供了很多自省的能力,inspect.getsource甚至可以返回一个函数的源代码。
难度指数: ★★
重要指数: ★★★
在python中,单下划线有很特殊的作用
参见文章 下划线(_)在python中的作用
难度指数: ★★★
重要指数: ★★★★
协程并不是python专有的概念,而是一个计算机概念。从概念上讲,python的生成器就是协程,为啥这样说呢,我们来看一下wiki上对协程的介绍:
协程(英语:coroutine)是计算机程序的一类组件,推广了协作式多任务的子程序,允许执行被挂起与被恢复。
生成器就符合协程的定义,关键点在于生成器允许执行被挂起和被恢复。在生成器里,代码执行到yield之后,会将程序的控制权交出去,在交出控制权后,仍然可以被恢复,再次执行。这一点,函数是做不到的,函数遇到return语句后,就退出了,无法被恢复。
协程,始终是在一个线程内被调度,这也是协程在并发方面优于线程的原因,因为协程减少了线程的上下文切换,那么协程是如何实现的呢,我有一篇文章专门讲解使用生成器实现协程,参见理解python的协程
难度指数: ★★★
重要指数: ★★★★
在一个内部函数中,对外部作用域的变量进行引用,(并且一般外部函数的返回值为内部函数),那么内部函数就被认为是闭包,以下面的代码为例
import time
def cost(func):
def wrapper():
t1 = time.time()
result = func()
t2 = time.time()
print(f"{func.__name__}执行时长: {t2-t1}")
return result
return wrapper
def test():
time.sleep(1.5)
new_test = cost(test)
new_test()
函数wrapper内使用了外部作用域(函数cost所形成的作用域)中的func,且外部函数(cost函数)的返回值是内部函数(wrapper函数),函数wrapper就是一个闭包。
闭包的概念,牵扯到变量作用域的概念,也是构建装饰器的基础。闭包这种特性主要有两种作用
下面是网络上流传最广的例子
origin = [0, 0] # 坐标系统原点
def create(pos=origin):
def player(direction, step):
new_x = pos[0] + direction[0] * step
new_y = pos[1] + direction[1] * step
pos[0] = new_x
pos[1] = new_y
return pos
return player
player = create() # 创建棋子player,起点为原点
print(player([1, 0], 10)) # 向x轴正方向移动10步
print(player([0, 1], 20)) # 向y轴正方向移动20步
print(player([-1, 0], 10)) # 向x轴负方向移动10步
create函数返回的是player函数,函数执行一次后,会保存当前所在的位置,因此下一次执行player时,是从上一次停留位置开始的。之所以能够做到保留上一次停留的位置,是因为player是一个闭包,它内部使用了外部作用域的pos对象,而pos对象是调用create函数时默认传入的origin。
在函数式编程中,这是一种非常有效的设计方案,不过我不建议你太深入的研究和学习,原因在于面向对象编程在解决这类问题时能够提供更好的解决方案。
设计一个函数,可以从文本文件中提取包含特定关键词的数据
def make_filter(keep):
def the_filter(file_name):
file = open(file_name)
lines = file.readlines()
file.close()
filter_doc = [i for i in lines if keep in i]
return filter_doc
return the_filter
filter = make_filter("pass") # filter是函数 the_filter
filter_result = filter("result.txt")
make_filter函数返回函数the_filter,the_filter函数会从文件中提取包含pass关键字的数据。
仔细看上面所举的两个例子,有没有发现他们和装饰器很像?其实它们从形式上和装饰器一模一样,只不过外部函数传入的参数是字符串,列表等基础类型的数据,如果传入的是函数对象,那么就是装饰器。
难度指数: ★
重要指数: ★★
这是一个送分题,假设你有多个项目,有的项目用Python2, 有的项目用python3, 即便python版本相同,同一个第三方库的版本也可能会不同,在同一台电脑上,如何让他们使用各自的python解释器和不同版本的第三方库呢?
virtualenv 能否为不同的项目创建各自的虚拟环境,在这个虚拟环境中,你的项目有自己的python解释器,有自己的第三方库安装环境,这是一个独立的环境,与其他虚拟环境隔离。
面试官可能会继续提问,创建虚拟环境的原理是什么,原理其实非常简单,想要进入虚拟环境,必须执行命令source venv/bin/activate, 那么我们来看一下venv/bin/activate 这个文件里的内容,其中有这么一段
VIRTUAL_ENV="/root/test_env/venv"
export VIRTUAL_ENV
_OLD_VIRTUAL_PATH="$PATH"
PATH="$VIRTUAL_ENV/bin:$PATH"
export PATH
这个文件中,将虚拟环境的目录添加到PATH的最前面,source命令让这个配置生效,那么对于当前你所打开的终端来说,当你再次执行python命令时,就从PATH里的路径去寻找python解释器,而由于你所创建的虚拟环境路径被插入到PATH的最前面,因此首先会到这里寻找,在venv目录下,有一个完整的python环境,这就是虚拟环境的秘密。
每一个venv目录下都有一个完整的python环境,在进入虚拟环境后,PATH被临时修改,使得当前的虚拟环境里的python总是第一个被找到。
难度指数: ★★
重要指数: ★★★
首先,我们要回答一些基本的代码编写规范
然后再回答一些代码的技巧
关于如何写出优质的python代码,参见文章编写优质python代码的10个技巧
难度指数: ★★★★
重要指数: ★★★
这个问题,要从两个方面进行回答
关于第一点,你可以参考5.2 ,内存池机制,是为了加速python的执行效率,对于Python对象,如整数,浮点数和List,都有各自的内存池,对于整数,从-5到255,这些小整数都是常驻内存的。
python的字符串nicode来表示, 但一个unicode字符最多占用4个字节,这样做有一些浪费,python在这方面做了优化,简单来说,一个python字符串里的字符,在内存中究竟占用多少字节,根本上取决于字符串里占用字节数最多的那一个。如果字符串里的字符都是ascii码表里的字符,那么每个字符就只占一个字节,如果字符串里出现了一个汉字,则每个字符占用2个字节,字符串节省内存的方式,参见文章Python在存储字符串时如何节省内存
对于列表,和元组,python也有优化,空的元组,永远只有一个;为了减少内存碎片,加快分配速度,python会重用旧的元组,如果一个元组不再被使用且元组的长度小于20,那么python不会直接释放它。
python里的列表和C++中的vector很像,看似有无限的空间可以使用,但其实,他们总是预先分配一些容量,当存储的数据超过容量时,则采取一定的算法增加容量,这样做可以避免过于频繁的申请内存,又能保证插入效率,关于列表和元组的内存优化,参见文章python 列表, 元组内存分配优化
难度指数: ★
重要指数: ★
pass主要作用就是占位,让代码整体完整,假设你定义了一个函数,但一时没有想好其中的逻辑,或者一个编写if语句时并没有想好符合条件后的操作,都可以使用pass来占位。
其实这样的题目只是为了考察你对于整个python语言生态的了解而已,不是很重要的题目
难度指数: ★
重要指数: ★
虽然没有问缺点,但也要回答哦
优点:
缺点:
难度指数: ★
重要指数: ★
assert()方法,用于判断一个表达式,在表达式条件为False 的时候触发异常,终止程序的运行。你最好只在debug的时候使用断言,千万不要在生产环境里用。
a = 4
b = 3
assert a < b # 抛异常,退出运行
print("ok")
QQ交流群: 211426309