Python面试简介及并行并发,,今天的分享内容大体如



今天的分享内容大体如下:

一. 面试

1. 什么是面试

2. 优秀的面试

二. Python综述

1. Python设计哲学及版本变迁

2. Python发展现状及其他语言使用场景

3. GIL

4. 内存管理和垃圾回收

5. 面试示例

三、并发并行

1. 进程

2. 线程

3. 协程

4. 并发编程

一、面试

什么是面试

面:当面/面谈/面聊, 试:考试,当前社会上各大企业和公司招人环节中最重要的一道流程关卡;目前面试已经衍生了多种操作形式:直面、电面、视频面、在线coding;

一轮面试的过程中,面试官期待什么呢?面试又希望找到什么样的人呢?

...

面试官通过面试找到技能能力符合岗位预期、综合素质(价值观、协作、沟通、逻辑思维等)优秀的候选人;

2. 优秀的面试

了解了面试官的出发点后,站在应聘者的角度,我们又怎样去看待呢?一轮优秀的面试过程是怎么样的呢?---优秀的面试应该是“能够最大化向面试官展示自己的技术能力和综合素质符合所应聘的岗位”,候选人本人是面试过程中的主人翁,面试过程中应该要学会以下几点:

1). 控场:应聘者本身是面试过程中的主角,如果能无形中大体控制整个面试过程的流程走向,将其朝向预期的方向发展,一般也就离成功不远了。大多数公司单轮面试时间会在20-60分钟左右(少于20分钟基本希望不大,大于1个小时的属于高级岗位或者面试过程中涉及到算法编程相关),面试官问的问题越多,碰到应聘者知识盲区的可能性也就越大,所以如果在保证质量的情况下,最大化的让面试官问到自己熟悉的领域对于大多数应聘者而言是需要思考和准备的;---忌一问一答或卡壳沉默

2). 亮点:面试过程中最好能在部分问题的回答上突出自己的亮点,何为亮点,即在一定程度上答案或者思路超出面试官对应聘者本身的期待;给予面试官深刻的记忆点,这些记忆点会无形中淡化当轮面试中候选人发挥不好的部分,很多时候会决定者应聘者的面试的成败;该部分主要依靠应聘者面试前的素材积累和面试过程中的语言技巧;

如何突出亮点呢?这边提一个思路,从时空线的角度去整理;

时间线:问题点的前世今生未来;

空间线:问题点当前相关的技术;

面试中可以不会,但不能不知道:不会和不知道是有区别的,不会是正常的,IT领域技术非常广泛,一个人的精力有限,不可能十八班武艺样样精通;但一些应聘领域常见的技术概念不知道则是不可取的,这可能会让面试官产生怀疑:应聘者之前是否经常闭门造车,不怎么关注当前领域业界的发展,候选人可能以后上升发展的空间可塑性有限等等;

二、Python综述

接下来讲面试过程中的第一个部分--Python语言综述;大家来到这边都选择了Python作为自己工作的第一门语言,是出于什么原因呢?

在面试的过程中,有时候会被问到,你怎么看待Python,什么情景下用Python,Python和其他语言有什么区别等等类似的问题,那我们怎么回答比较好呢?下面我将带大家过一些Python知识点,大家在听的过程中可以思考下怎么在回答中将这些内容在面试回答中展现出来;

Python设计哲学及版本变迁

要想深入的去理解Python这门语言,我们需要了解Python的设计哲学,Python作者Guido Van Rossum是出于什么原则来设计Python体系的呢?这点大家打开Python交互式模式,输入import this

   Beautiful is better than ugly. 优美胜于丑陋

   Explicit is better than implicit. 明了胜于晦涩

   Simple is better than complex. 简单胜过复杂

   Complex is better than complicated. 复杂胜过凌乱

   Flat is better than nested.      扁平胜于嵌套

   Sparse is better than dense.      间隔胜于紧凑

   Readability counts.           可读性很重要

(大家可能有些人英语不太好,但没关系,直接Ctrl+C百度Google翻译一下)其中最重要的设计哲学“简单、明确、优雅”,Python作为一种高级开发语言,封装了大量的底层操作,极大的简化开发编程的难度,同时在在美观优雅也有自己独特的追求,如装饰器等;

1991年,第一个Python版本发布,2000年,第一个Python稳定版本2.7发布,2008年,Python3发布;2020年,Python2将不再维护;目前市场上主要以Python2.7和Python3.x为主;其中Python2到Python3语法有较大变化;

由于历史原因,当前市场上不少公司项目仍然是基于Python2.7.x开发,面试官有时会会问到面试者“Python3有哪些变化,和Python2有什么不同?”,Python3的详细变化有很多,我们每个人脑容量有限,并不需要全部都记下来,记忆中其中关键的几条即可,如以下几个方面:

1)Print用法

2)int和long,Python3中int相当于Python2中的int+long,即int不存在4字节数字溢出;

3)byte和str,Python3中对字符串编码做了优化,只有byteL类型和str类型;

4)函数注释新特性;

5)range相当于xrange,使用了迭代器进行循环迭代;

示例代码如下:

# coding=utf-8"""author = jamon"""if __name__ == "__main__":    import sys    sys.path.append("../")‘‘‘def run_on_python27():    """    Python27 代码示例    :return:    """    print "老男孩"‘‘‘def run_on_python3():    """    Python3 代码示例    :return: None    """    print("老男孩")    a = "老男孩"    b = a.encode("utf-8")    c = b.decode("utf-8")    print("a is {}, b is {}, c is {}".format(type(a), type(b), type(c)))def python3_new_feature(a: "first param", b: "第二个参数", c: float = 10) -> int:    print("函数注释新特性:", a, b, c)    return 0if __name__ == "__main__":    run_on_python3()    python3_new_feature("jiayou", "ok", ["test"])    print(python3_new_feature.__annotations__)

2. Python发展现状及其他语言使用场景

Python是一种脚本解释性语言,相对于C/C++等传统编程语言,会存在一定的性能损失,在一些特定的业务场景下,cpu密集型的高并发任务如复杂度较高的算法、反序列化等,可以针对该部分具体逻辑改用C++编写程三方库,然后使用Python进行调用,同时发挥Python简单低成本易维护和C++高性能优势;

Python当前在IT行业的工作主要分为以下几个部分:

a. web后台开发

b. 游戏后台开发

c. 全栈开发

d. 爬虫开发

e。测试开发

f. 运维开发

g. 其他,如人工智能、数据分析等;

3. GIL(Global Interpreter Lock) 全局解释锁

面试过程中经常会被问到“Python多线程有用过吗?” “Python的GIL是什么?”等等此类问题,其实考点都是GIL锁机制问题;

1991年Rossum设计Python时,当时世界上上午多核处理器的存在,出于简化CPython实现,更加容易的提供多线程支持,GIL应运而生,Python本身多线程由于GIL的存在,可以理解为伪多线程,当使用默认的CPython作为解释器时,python多线程大多时候并不能坐到真正的并行,难以发挥多核多处理器的全部性能;

Python的GIL之所以会影响多线程性能,是因为在多线程的情况下,只有当线程获得了一个全局锁的时候,那么该线程的代码才能运行,而全局锁只有一个,所以使用python多线程,在同一时刻只有一个线程在运行;

而GIL锁的释放时机有两种情况:

1)线程开始IO操作时,会先释放GIL;

2)计算任务时,解释器每隔100次opcode(可通过dis.dis()查看)或15ms释放GIL;

下面将测试Python在多线程和单线程小的效率对比,代码如下:

# coding=utf-8"""author = jamon参考博客:https://www.cnblogs.com/SuKiWX/p/8804974.html"""if __name__ == "__main__":    import sys    sys.path.append("../")from threading import Threadimport timedef my_counter():    i = 0    for _ in range(100000000):        i = i + 1    return Truedef single_thread():    """    单线程测试    :return:    """    thread_array = {}    start_time = time.time()    for tid in range(2):        t = Thread(target=my_counter)        t.start()        t.join()    end_time = time.time()    print("Total time: {}".format(end_time - start_time))def multi_thread():    """    多线程测试    :return:    """    thread_array = {}    start_time = time.time()    for tid in range(2):        t = Thread(target=my_counter)        t.start()        thread_array[tid] = t    for i in range(2):        thread_array[i].join()    end_time = time.time()    print("Total time: {}".format(end_time - start_time))if __name__ == "__main__":    single_thread()    multi_thread()

小结:

Python的多线程在多核处理器上,只对IO密集型计算产生正面效果,而当其中至少有一个CPU密集型任务线程存在,则多线程的效率由于GIL的存在将大幅下降;

如何避免GIL带来的消极影响:

1)用multiprocessing代替thread,多进程方式每个进程都有自己独立的GIL,进程之间不会发生GIL争抢;

2)用其他的解析器,GIL只存在于Python默认的CPython解析器,当使用JPython、IronPython等其他解析器时可避免GIL问题,但同时也失去了很多C语言有用模块的机会;

4.内存管理和垃圾回收

部分Python面试在深度这块可能会问到“Python会不会存在内存泄露?”“Python内存是如何管理的”等诸如此类的问题,因为在具体的企业项目开发过程中,随着用户规模的快速增长和业务逻辑的愈加复杂,代码缺陷和内存回收不善可能会引发内存性能瓶颈;

1)对象存储:Python里面万物皆对象,所有的对象都会在内存中开辟一块空间进行存储,对象的内存地址可以通过id()查看,对于整数和较小的字符,Python会缓存对象,不会创建多个相同对象;容器对象,存储的其他对象,仅仅是对象的引用,并不是对象本身;

2)对象回收:一个对象会记录自身被引用的个数,每增加一个引用计数+1,减少一个引用计数-1;注意当对象被作为参数传递给函数时,引用计数会+2,因为函数调用时内部有两个属性引用;查看引用计数sys.getrefcount,当对象间出现相互循环引用时,此时对象将不能通过引用计数器进行销毁。循环引用代码见附件:

  分代回收: Python里面将对象回收分为三代,默认一个对象创建出来后,处于0代,经历过一定的检测次数后,依然存活,则迁移到下一代;垃圾回收的顺序为:

  0代垃圾回收一定次数,会触发0代和1代回收;

  1代垃圾回收一定次数,会触发0代、1代和2代回收;

  gc.get_threshold()查看垃圾回收机制触发条件和回收条件;

  gc.set_threshold(700, 10,10), 设置垃圾回收机制条件,700指新增对象数-消亡对象数>=700时触发垃圾检测;第一个10只10次0代检测后触发一次1代检测,第二个10指10次1代检测后触发2代检测;

  循环引用一般有两种解决方法:

  手动回收垃圾:gc.collect(),参考示例代码

  弱引用:weakref.ref

import gcimport objgraphimport sysimport weakrefdef quote_demo():    class Person:        pass    p_xxx = Person() # 1    print(sys.getrefcount(p_xxx))    def log(obj):        print(sys.getrefcount(obj))    log(p_xxx)    p2 = p_xxx    print(sys.getrefcount(p_xxx))    del p2    print(sys.getrefcount(p_xxx))def circle_quote():    # 循环引用    class Dog:        pass    class Person():        pass    p = Person()    d = Dog()    print(objgraph.count("Person"))    print(objgraph.count("Dog"))    p.pet = d    d.master = p    # 删除 p, d之后, 对应的对象是否被释放掉    del p    del d    print(objgraph.count("Person"))    print(objgraph.count("Dog"))def solve_cirecle_quote():    # 1. 定义了两个类    class Person:        def __del__(self):            print("Person对象, 被释放了")        pass    class Dog:        def __del__(self):            print("Dog对象, 被释放了")        pass    p = Person()    d = Dog()    p.pet = d    d.master = p    p.pet = None  # 强制置 None    del p    del d    gc.collect()    print(objgraph.count("Person"))    print(objgraph.count("Dog"))def sovle_circle_quote_with_weak_ref():    # 1. 定义了两个类    class Person:        def __del__(self):            print("Person对象, 被释放了")        pass    class Dog:        def __del__(self):            print("Dog对象, 被释放了")        pass    p = Person()    d = Dog()    p.pet = d    d.master = weakref.ref(p)    del p    del d    gc.collect()    print(objgraph.count("Person"))    print(objgraph.count("Dog"))

面试问题:什么情况下使用Python语言开发比较好?/你对Python怎么看?/。。。

  回答思路:

  时间线:

  1991年,第一个稳定的Python版本;

  2000年,Python2.7稳定版本发布;

  2008年,Python3发布;

  2020年,Python2不再维护;

  空间线:

  Python设计哲学;

  Python2、3特性;

  和其他编程语言(如c++)对比;

  语言选型考虑因素;

  思路串联:设计哲学(Python本质特性)-> 如何选型(正面回答,取其精华) -> 埋下一个问题引子

  示例答案:Python自1991年Rossum编写发布出第一个版本以来,便一直遵循着其设计哲学”简单、明确、优雅“,设计哲学的根本出发点就决定了Python这门语言的编程开发的易用性和工作高效性,同样的业务需求实现,可能传统编程语言C++程序猿需要1周工期,而Python程序猿很有可能三天不到就能完成,而且潜在隐藏的问题会少很多,因为Python本身目前有非常庞大的开源第三方库,同时封装了很多复杂的语言底层操作如内存分配与释放,指针的相关操作等;

  在具体的项目实践过程中,开发语言的选型需要考虑到几个方面:项目业务本身的特点适合哪种语言、公司历史的技术栈和人员技术储备倾向于哪种语言、选型的语言当前领域内的生态体系和发展前景(比如是否不在维护);如果项目非计算密集型项目,其中对于算法性能等要求不是非常非常高,相对而言是业务功能和流程逻辑较为复杂,此时Python语言就天然具备很大的优势,碰到其中少部分算法等计算性能瓶颈的地方可以用c++等语言重写,然后Python调用,这样可以兼顾Python开发低成本和C++高性能两方面的有点;

  具体选用Python后,新项目推荐使用python3,2008年Python3发布后,十几年来Python3生态圈三方库已经非常完备和齐全,而官方已宣布Python2在2020年将不再维护,另外Python3本身性能也相较Python2有一定的提升;

  回答思路2:

  时间线:

  截止2014年,哈佛、麻省理工、伯克利、卡内基等美国顶尖高校将Python作为教学语言;

  2018年开始,Python雄踞各大年度最佳编程语言排行榜榜首(IEEE, TIOBE)

  空间线:

  Python特性:动态、解释、脚本、跨平台...

  解释器:cpython,jpython,ipython,jit

  Python2,3特性

  思路串联:Python特性(解释、动态、脚本)-> 当前发展 -> 解释器及性能优化 -> 埋下一个问题引子

  示例答案2:Python语言和C++, Java等其他编译型语言不一样,它是一种动态解释型脚本语言,代码在执行时会一行一行的解释成CPU能理解的机器码。同时它又是跨平台的,可以允许在windows,linux,mac等系统上,同样的程序逻辑,可能C语言需要1000行代码,java有100行,而Python实现可能只需要20行,这极大程度上提升了企业的项目开发效率。

  记得之前看到一篇报道说,到2014年为止,哈佛、麻省理工、伯克利等顶尖高校都已经使用Python作为教学语言,而最近这一两年来Python也多次被各大机构评为年度最佳编程语言。对于企业项目后台开发而言,不是说一定需要使用Python开发,但至少应该是一个首选考虑项,当然Python本身从根本上来说性能是比不上C/C++这些编译型语言,但现在语言本身的性能差距在实际开发的过程中,相对业务功能合理性设计、代码编写架构质量等等,语言底层本身造成的性能损失越来越可以忽略不计;针对于一些特定的功能模块,Python当前也出现了pypy,JIT等大幅提高Python程序运行效率的相关技术,能够满足大多数功能需求业务场景。

  具体选用Python后,新项目推荐使用python3,2008年Python3发布后,十几年来Python3生态圈三方库已经非常完备和齐全,而官方已宣布Python2在2020年将不再维护,另外Python3本身性能也相较Python2有一定的提升;

  回答思路3:

  时间线:

  20世纪60年代,UNIX诞生,贝尔实验室发明B语言(过于简单,数据无类型)

  1972年,C语言诞生(面向过程),重写UNIX;

  1995年,Java出现;(面向对象)

  2000年,第一个稳定的Python2版本发布;

  空间线:

  语言的本质

  Python行业发展(人工智能、云计算、大数据...)

  Python开源生态圈

  Python2,3特性

  思路串联:语言进化 -> 语言本质 -> Python当前发展(应用产业及开源生态圈)-> 留下一个问题引子(此思路不说Python缺陷,留下两个下一问题引子)

  示例答案3:恩...这个问题我是这么看的,上世纪60年Unix诞生时,贝尔实验室是使用的B语言,解决了面向机器的汇编语言极其复杂问题,然后到72年,面向过程的C语言出现,再到95年Java诞生,面向对象的思想极大的提升了程序猿的开发效率。2000年第一个稳定的Python2版本,20年来其简单高效的特质大大降低了研发人员的学习和开发成本;

  编程语言的本质是人与机器沟通的工具,将人类希望机器做的事翻译成机器本身能够理解的指令;语言发展进化的历史也已经表明,越符合人类自身思维逻辑及习惯的编程语言将越受到大众欢迎。目前从云端、客户端,到物联网终端,python应用无处不在,同时也是人工智能首先的编程语言(Python在在人工智能上的优势至今无人能够撼动)。Python当前也已经具备了非常完备丰富的开源三方生态圈(比如web框架tornado,sanic, 运维监控zabbix,游戏引擎firefly等等不胜枚举)。对于大多数企业新后台项目开发,个人倾向于推荐Python作为首选语言。

  具体选用Python后,新项目推荐使用python3,2008年Python3发布后,十几年来Python3生态圈三方库已经非常完备和齐全,而官方已宣布Python2在2020年将不再维护,另外Python3本身性能也相较Python2有一定的提升;

作业1:Python会不会存在内存泄露的情况?作业2:你之前的项目中出现过Python性能方面的问题吗?

三、并发并行

并发相关的问题在面试中是一个很容易被问到的模块,比如”协程是什么?怎么理解协程?和线程进程有什么区别...“ 接下来我将给大家梳理一下进程、线程、协程等并发相关的知识考点。

1. 进程:

大家前面的课程学过进程相关的一些概念,那什么是进程呢?

进程是系统进行资源分配和调度的基本单位,是操作系统动态执行的基本单元,fork多进程,子进程将复制父进程的数据空间及堆栈,此时有涉及到一个考点--copy on write(写时复制),即只有在子进程中有更改相应段的行为时,才会将父进程的内容复制给子进程;子进程在复制父进程变量时,复制的是逻辑地址,父子进程相同的逻辑地址在内存中实际对应的物理地址是不一样的;

什么是逻辑地址和物理地址呢?

逻辑地址是cpu生成的地址,包括页号、页偏移,可以理解为相对地址的概念,和基址结合,可以确定其在内存中对应的物理单元地址;而物理地址指内存单元看到的地址;

进程复制和切换的代价是比较高的,Python语言中多进程比较适合CPU密集型任务,此类任务多进程的方式能够最大化利用到当前多核处理器的并行优势;但同时进程的频繁创建和删除也消耗了过多的计算机资源;

2.线程

线程是操作系统进行运算调度的最小单位,是进程的实际运作单位。而线程又分为内核线程和用户线程。内核线程又称轻量级线程,由操作系统内核创建和撤销;用户线程是在语言这一层级进行处理,它的创建、销毁和调度不需要内核支持;

多任务程序设计中多线程的方式相较多进程的优势主要有几点:一是无需复制申请新的资源,所有子线程和父线程会共享资源;二是线程间通信速度远远快于进程间通信,效率更高;

3.协程:

当前多线程的情况下,线程间的切换一般是根据时间片或者执行的代码数达到一定的阈值后进行切换,虽然线程间的切换代价相较进程已经小了很多,但当线程较多且生存周期较长时,频繁的线程切换代价累计起来渐渐的难以忽略,此时一种多任务运作模式应运而生。

协程,又称微线程,也即纤程。协程和线程一样都是一段子程序,但协程的特点是一个线程在执行,只有当该子程序内部发生中断或阻塞时,才会交出线程的执行权交给其他子程序,在适当的时候再返回来接着执行。与多线程相比,协程的优势主要有两点:一是省去了大量线程切换的开销,不需要频繁做线程切换;二是由于是单线程执行,此时共享资源便不需要加锁,执行效率比多进程高很多;

当然为了利用多核处理器的优势,生产环境中使用协程一般推荐多进程+协程的方式,既充分利用多核,又可以发挥协程高效的方式;Python对协程的支持用generaotr的yield可以一定程度上实现;

 4.并发编程:

在生产环境中有些时候我们遇到这样的场景:如数据库有做分表操作n张表,需求是需要查询特定的数据,而这些数据实际却分布于n张表中,查询的结果需要统一返回给调用者,调用者本身是不知道有分表查询的操作,那么怎样高效完成这一操作呢?

传统的实现方式时串行执行,即循环查询每一张表,依次合并所有查询的结果;这种方式随着循环次数的增多效率将直线下降;此时实际上我们可以多任务并发的方式解决这类问题;

示例代码如下:

# coding=utf-8import mathfrom multiprocessing import Poolimport timedef parallel_compute(call_func, param_list=[], process_num=10, need_return=True):    """    批量并行调用某一函数, 每次调用传递参数不同, 返回结果进行合并;    执行的函数中有DB操作时,注意需要在函数内部重新实例化DB连接实例,否则易出现未知错误    :param call_func: 待进行的函数    :param param_list: [(), (), ...], 参数长度(并发的进程数)    :param process_num: int, 同时并行的子进程数, 默认最大为10    :param need_return: 是否需要返回计算结果    :return: {            1: result2, 2: result2, 3:...        }    """    if not param_list:        return    s_time = time.time()    results = {}    task_num = len(param_list)    loop_num = int(math.ceil(task_num/float(process_num)))    for t in range(0, loop_num):        real_num = len(param_list[t*process_num:(t+1)*process_num])        pool = Pool(processes=real_num)        for i in range(0, real_num):            results[t*process_num+i] = pool.apply_async(call_func, param_list[t*process_num+i])        pool.close()        pool.join()        e_time = time.time()        print("parallel_compute {0} {1} tasks took {2}s"              .format(str(call_func.__name__), process_num, e_time - s_time))    if not need_return:        return    for k, v in results.items():        results[k] = v.get()    return resultsif __name__ == "__main__":    pass    a = "23"    try:        assert 3 is None    except Exception:        print("bbbbbbbbb")    print("ttttttt")

作业3:协程怎么理解?/进程线程有什么区别?/高并发怎么实现?...



Python面试简介及并行并发

评论关闭