python开发场景面试题

python面试相关

常规

  1. 正则表达式

    • ^:行首匹配
    • $:行尾匹配
    • .:匹配任意一个字符(除了\n
    • (xyz):匹配小括号内的xyz
    • []:匹配[]中列举的字符
    • \d:匹配数字,既0-9
    • \D:匹配非数字,既不是数字
    • \s:匹配空白,既空格,tab键
    • \S:匹配非空白
    • \w:匹配单词字符,既a-zA-Z0-9
    • \W:匹配非单词字符
    • a*:匹配前一个字符出现0次或无限次,既可有可无
    • +:匹配前一个字符出现1次或无限次,既至少有1次
    • ?:匹配前一个字符出现1次或者0次,要么有1次要么有0次
    • {m}:匹配前一个字符出现m
    • x{n,}:匹配至少n个x
    • {m,n}:匹配前一个字符出现从mn
    • [\u4E00-\u9FA5]:匹配汉字
  2. python有哪些数据类型

    数字,字符串,列表,元组,字典,集合

  3. python2和python3的区别

    • Py2采用Ascll编码,Py3采用utf8
    • py2中的Print不加括号,Py3加括号
    • Py2 input函数输入的是数字,Py3中输入的是字符串
    • Py2 中range返回的是一个列表,Py3中返回的是一个可迭代对象
  4. 列表和字典的区别:

    • 列表通过索引来获取值,字典通过键获取值
    • 字典的原理是hash算法,占用内存不同,字典占用内存较大
    • 字典查找和插入的速度极快
  5. 元组的实现原理

    1
    2
    3
    4
    myTuple=(1,2,3,4)
    id(myTuple[0]) #1652911120
    a=1
    id(a) # 1652911120

    元组第一位元素的地址和整形变量a的地址是一样的,说明他们都指向常量1所在的地址空间,常量是不可重写的,所以元组的元素不可重写,但元组却可以重新赋值,此特性和C++的指针常量完全一样:指向的内存地址区域不可重写,但却可以重新指向其他内存区域

  6. 类和对象的区别:

    • 类是具有相同属性和服务的一组对象的集合,类是抽象的,其内部包括属性和服务两个主要部分,而对象用于表示现实中该类事物的个体,对象是系统中用来描述客观事物的一个实体,它是构成系统的一个基本单位,类与对象的关系就如模具和铸件的关系,类的实例化结果就是对象,对象是类的一个具体。
    • 类是一个抽象的概念,它不存在于现实中的时间/空间里,类只是为所有的对象定义了抽象的属性与行为
  7. 面向对象和面向过程:

    • 面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了
    • 面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。
  8. python面向对象中super是干啥用的:

    • 继承父类的方法属性;
    • super在新式类中才有,在python2中要写object,python3中不用
    • super 是用来解决多重继承问题的,直接用类名调用父类方法在使用单继承的时候没问题,但是如果使用多继承,会涉及到查找顺序(MRO)、重复调用(钻石继承)等种种问题
  9. python中的魔法方法__init____new__的区别:

  10. 简述一下ORM:

    ORM,全拼Object-Relation Mapping,意为对象-关系映射,实现了数据模型与数据库的解耦,通过简单的配置就可以轻松更换数据库,而不需要修改代码只需要面向对象编程,orm操作本质上会根据对接的数据库引擎,翻译成对应的sql语句

  11. git发Request大概流程:

    新建分支,分支开发,撰写提交信息,与主干同步,合并 commit,推送代码到远端,请求代码合并 merge-request

  12. 类中self的作用:

    方便使用对象自身的属性

  13. 两个栈实现一个队列:栈A用来做入队列,栈B用来作出队列,当栈B为空时,栈A全部出栈到栈B,栈B再出栈(既出队列)

  14. MySQL中序列的使用:auto_increament

  15. TCP断开连接的过程:

    • 什么情况下进入time_wait状态?

      当关闭一个Socket连接时,主动关闭一端的Socket将进入TIME_WAIT状态,而被动关闭一方则转入CLOSED状态

      具体过程如下:

      1. 客户端发送FIN报文,进入FIN_WAIT_1状态
      2. 服务端收到FIN报文,发送ACK确认,进入CLOSE_WAIT状态
      3. 客户端收到FIN的确认报文,进入FIN_WAIT_2状态
      4. 服务端发送FIN报文,进入LAST_ACK状态
      5. 客户端收到FIN报文,发送FIN_ACK,同时进入TIME_WAIT状态,启动TIME_WAIT定时器,超时时间设置为2MSL
      6. 服务端收到FIN_ACK,进入CLOSED状态
      7. 客户端在2MSL时间内没有收到服务端的任何响应,TIME_WAIT超时后,进入CLOSED状态
  16. nginx:

    niginx是一款轻量级的Web 服务器/反向代理服务器, 其特点是占有内存少,并发能力强,事实上nginx的并发能力确实在同类型的网页服务器中表现较好. 轻量级高并发服务器Nginx。

    Nginx使用基于事件驱动架构,使得其可以支持数以百万级别的TCP连接,高度的模块化和自由软件许可证使得第三方模块层出不穷(这是个开源的时代啊~)

    Nginx是一个跨平台服务器,可以运行在Linux,Windows,FreeBSD,Solaris,AIX,Mac OS等操作系统上。

    正向代理(翻墙软件):正向代理最大的特点是客户端非常明确要访问的服务器地址,屏蔽或者隐藏了真实客户端信息

    作用:

    1. 访问原来无法访问的资源,如Google
    2. 可以做缓存,加速访问资源
    3. 对客户端访问授权,上网进行认证
    4. 代理可以记录用户访问记录(上网行为管理),对外隐藏用户信息。

    反向代理:比如淘宝每天同时连接到网站的访问人数已经爆表,单个服务器远远不能满足人民日益增长的购买欲望了,此时就出现了一个大家耳熟能详的名词:分布式部署也就是通过部署多台服务器来解决访问人数限制的问题;淘宝网站中大部分功能也是直接使用Nginx进行反向代理实现的

    客户端是无感知代理的存在的,反向代理对外都是透明的,访问者并不知道自己访问的是一个代理。因为客户端不需要任何配置就可以访问。

    反向代理,“它代理的是服务端,代服务端接收请求”,主要用于服务器集群分布式部署的情况下,反向代理隐藏了服务器的信息。

    反向代理的作用:

    1. 保证内网的安全,通常将反向代理作为公网访问地址,Web服务器是内网
    2. 负载均衡,通过反向代理服务器来优化网站的负载,常用方法有哈希,轮询。
  17. MySQL对于千万条数据量有什么优化方法:

    1. 分库分表很明显,一个主表(也就是很重要的表,例如用户表)无限制的增长势必严重影响性能,分库与分表是一个很不错的解决途径,也就是性能优化途径,现在的案例是我们有一个1000多万条记录的用户表members,查询起来非常之慢,做法是将其散列到100个表中。
    2. 不停机修改mysql表结构,同样还是members表,前期设计的表结构不尽合理,随着数据库不断运行,其冗余数据也是增长巨大
  18. 死锁产生的必要条件?

    MySQL发生死锁了,怎么解决?互斥条件,请求和保持条件,不可剥夺条件,环路等待条件。

    解决:

    • 终止进程,终止(或撤销)系统中的一个或多个死锁进程,直至打破循环环路,使系统从死锁状态中解除出来
    • 抢占资源,从一个或多个进程中抢占足够数量的资源,分配给死锁进程,以打破死锁状态。
  19. redis基本数据类型

    列表,字符串,哈希,集合,有序集合

  20. get和post的区别

    get请求无消息体,只能携带少量数据 post请求有消息体,可以携带大量数据。

    携带数据的方式:get请求将数据放在url地址中,post请求将数据放在消息体中;

    GET请求提交的数据放置在HTTP请求协议头中,而POST提交的数据则放在实体数据中;

    GET方式提交的数据最多只能有1024字节,而POST则没有此限制。

  21. HTTP协议,请求头和响应头信息?

    HTTP(超文本传输协议)是一个基于请求与响应模式的、无状态的、应用层的协议,常基于TCP的连接方式,HTTP1.1版本中给出一种持续连接的机制,绝大多数的Web开发,都是构建在HTTP协议之上的Web应用。

    请求报文包含三部分:a、请求行:包含请求方法、URI、HTTP版本信息b、请求首部字段c、请求内容实体;

    响应报文包含三部分:a、状态行:包含HTTP版本、状态码、状态码的原因短语b、响应首部字段c、响应内容实体

  22. 输入一个网址的整个请求流程?

    1. 域名解析:分解出协议名、主机名、端口、对象路径等部分,得到IP地址
    2. 建立TCP连接,三次握手,把以上部分结合本机自己的信息,封装成一个HTTP请求数据包
    3. Web浏览器向Web服务端发送HTTP请求报文
    4. 服务器响应HTTP请求
    5. 浏览器解析HTML代码,并请求HTML代码中的资源(JS,CSS,图片)(这是自动向服务器请求下载的)
    6. 浏览器对页面进行渲染呈现给客户
    7. 断开TCP连接

    HTTP 1.1在继承了HTTP 1.0优点的基础上,也克服了HTTP 1.0的性能问题

  23. 长连接和短连接及其优缺点?

    HTTP的长连接和短连接本质上是TCP长连接和短连接。在HTTP/1.0中默认使用短连接。也就是说,客户端和服务器每进行一次HTTP操作,就建立一次连接,任务结束就中断连接

    从HTTP/1.1起,默认使用长连接,用以保持连接特性。使用长连接的HTTP协议,会在响应头加入这行代码:Connection:keep-alive 长连接多用于操作频繁,点对点的通讯,而且连接数不能太多情况。

    WEB网站的http服务一般都用短链接,因为长连接对于服务端来说会耗费一定的资源,而像WEB网站这么频繁的成千上万甚至上亿客户端的连接用短连接会更省一些资源

  24. 数据库的集中连接?

    1. django自带ORM框架和 django.db 三方库
    2. python中最著名的 SQLAlchermy ORM框架
  25. 多态以及多态怎样实现?

    • python中多态的实现:多个不同的类具有共同的方法f,各个类调用方法f,返回值不同。把方法f提取出来,封装为一个接口g。不同类的实例作为参数,传入接口g,得到不同返回值。
    • 多态性使用的前提:①类的继承关系 ②要有方法重写。
  26. 进程和线程和协程?

    • 进程是操作系统进行资源分配和调度的基本单位,多个进程之间相互独立。直白的讲:进程是应用程序的启动实例。
    • 线程是CPU进行资源分配和调度的基本单位,线程是进程的一部分,是比进程更小的能独立运行的基本单位,一个进程下的多个线程可以共享该进程的所有资源。缺点是一个线程的崩溃,可能会导致整个进程的崩溃。

    IO密集型用多线程。CPU密集型用多进程,因为如果IO操作少,用多线程的话,线程会共享一个全局解释器锁。不能充分发挥多线程的优势。

    • 协程,又称微线程,线程是系统级别的它们由操作系统调度,而协程则是程序级别的由程序根据需要自己调度。在一个线程中会有很多函数,我们把这些函数称为子程序,在子程序执行过程中可以中断去执行别的子程序,而别的子程序也可以中断回来继续执行之前的子程序,这个过程就称为协程。也就是说在同一线程内一段代码在执行过程中会中断然后跳转执行别的代码,接着在之前中断的地方继续开始执行,类似于yield操作。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。
    • 因此:协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态。
    • 协程的优点:
      • 无需线程上下文切换的开销,协程避免了无意义的调度,由此可以提高性能(但也因此,程序员必须自己承担调度的责任,同时,协程也失去了标准线程使用多CPU的能力)
      • 无需原子操作锁定及同步的开销
      • 方便切换控制流,简化编程模型
      • 高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理
    • 协程的缺点:
      • 无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上.当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。
      • 进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序。

    传统的生产者-消费者模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁。如果改用协程,生产者生产消息后,直接通过yield跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高。

  27. 数据库的存储引擎?

    • InnoDB 支持事务,MyISAM 不支持,这一点是非常之重要。事务是一种高级的处理方式,如在一些列增删改中只要哪个出错还可以回滚还原,而 MyISAM就不可以了;
    • MyISAM 适合查询以及插入为主的应用,InnoDB 适合频繁修改以及涉及到安全性较高的应用;
    • InnoDB 支持外键,MyISAM 不支持;
    • 对于自增长的字段,InnoDB 中必须包含只有该字段的索引,但是在 MyISAM表中可以和其他字段一起建立联合索引;
    • 清空整个表时,InnoDB 是一行一行的删除,效率非常慢。MyISAM 则会重建表;
  28. Linux软连接和硬链接:

    ln命令用于给文件创建链接

    • 软链接:类似于 Windows 系统中给文件创建快捷方式,即产生一个特殊的文件,该文件用来指向另一个文件,此链接方式同样适用于目录
    • 硬链接:我们知道,文件的基本信息都存储在 inode 中,而硬链接指的就是给一个文件的 inode 分配多个文件名,通过任何一个文件名,都可以找到此文件的 inode,从而读取该文件的数据信息
  29. python垃圾回收算法?

    python垃圾回收主要以引用计数为主,标记-清除分代清除为辅的机制,其中标记-清除和分代回收主要是为了处理循环引用的难题。

  30. 迭代器,生成器,装饰器,闭包?

    • 迭代器就是用于迭代操作的的对象,遵从迭代协议(内部实现了__iter__()和__next__()方法,可以像列表(可迭代对象,只有__iter__()方法)一样迭代获取其中的值,与列表不同的是,构建迭代器的时候,不像列表一样一次性把数据加到内存,而是以一种延迟计算的方式返回元素,即调用next方法时候返回此值。
    • 生成器本质上也是一个迭代器,自己实现了可迭代协议,与生成器不同的是生成器的实现方式不同,可以通过生成器表达式生成器函数两种方式实现,代码更简洁。生成器和迭代器都是惰性可迭代对象,只能遍历一次,数据取完抛出Stopiteration异常。区别:生成器一定是迭代器,但是迭代器不一定是生成器,因为创建一个迭代器只需要实现iter和next()方法就可以了,并不一定要使用yield实现。生成器的唯一注意事项就是:生成器只能遍历一次。
    • 装饰器:在不修改原函数的情况下,给原函数添加新功能。一共有俩个函数,外层函数返回的是内层函数的函数名,用的时候在被装饰的函数上面加上@装饰器名。
    • 闭包的概念就是当我们在函数内定义一个函数时,这个内部函数使用了外部函数的临时变量,且外部函数的返回值是内部函数的引用。Nonlocal关键字
  31. import搜索过程:

    1. 首先判断这个module是不是built-in即内建模块,如果是则引入内建模块,如果不是则在一个称为sys.path的list中寻找。
    2. sys.path在python脚本执行时动态生成,包括以下3个部分:
      • 脚本执行的位置,即当前路径
      • 环境变量中的PYTHONPATH, 即.bash_profile
      • 安装python时的依赖位置
  32. HTTP和HTTPS的区别:

    HTTP是互联网上应用最为广泛的一种网络协议,是一个客户端和服务器端请求和应答的标准(TCP),用于从WWW服务器传输超文本到本地浏览器的传输协议,它可以使浏览器更加高效,使网络传输减少。

    HTTPS:是以安全为目标的HTTP通道,简单讲是HTTP的安全版,即HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容就需要SSL。

    HTTPS协议的主要作用可以分为两种:一种是建立一个信息安全通道,来保证数据传输的安全;另一种就是确认网站的真实性

    HTTPS和HTTP的区别主要如下:

    1. https协议需要到ca申请证书,一般免费证书较少,因而需要一定费用。
    2. http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。
    3. http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
    4. http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。
  33. 红黑树

  34. cookie和session的区别:

    1. session 在服务器端,cookie 在客户端(浏览器)
    2. session 的运行依赖 session id,而 session id 是存在 cookie 中的,也就是说,如果浏览器禁用了 cookie ,同时 session 也会失效,存储Session时,键与Cookie中的sessionid相同,值是开发人员设置的键值对信息,进行了base64编码,过期时间由开发人员设置
    3. cookie安全性比session差。
  35. 对分布式的理解:

    在面对一些高并发,海量的数据任务时,一台服务器往往是不够的,而分布式就是让多台服务器协同工作,完成单台服务器无法处理的任务。即就是同一个业务拆成多个子业务,部署在不同服务器上。

  36. 线程同步的方法:

    1. 锁机制threading的Lock类

      用该类的acquire函数进行加锁,用realease函数进行解锁,当一个线程调用锁的acquire()方法获得锁时,锁就进入“locked”状态。

      每次只有一个线程可以获得锁。如果此时另一个线程试图获得这个锁,该线程就会变为“blocked”状态,称为“同步阻塞”, 直到拥有锁的线程调用锁的release()方法释放锁之后,锁进入“unlocked”状态。

      线程调度程序从处于同步阻塞状态的线程中选择一个来获得锁,并使得该线程进入运行(running)状态。

    2. 信号量

      信号量也提供acquire方法和release方法,每当调用acquire方法的时候,如果内部计数器大于0,则将其减1,如果内部计数器等于0,则会阻塞该线程,知道有线程调用了release方法将内部计数器更新到大于1位置。

    3. 条件判断:所谓条件变量,即这种机制是在满足了特定的条件后,线程才可以访问相关的数据。

      它使用Condition类来完成,由于它也可以像锁机制那样用,所以它也有acquire方法和release方法,而且它还有wait,notify,notifyAll方法。

    4. 同步队列:

      put方法和task_done方法,queue有一个未完成任务数量num,put依次num+1,task依次num-1.任务都完成时任务结束。

  37. 进程间通信:

    • 管道
    • 消息队列
    • 共享内存
    • 进程地址空间:父子进程代码共享,数据各自开辟空间,采用写时拷贝私有一份。
  38. redis如何做持久化:

    • 一种是RDB(快照)持久化(默认)(原理是将Reids在内存中的数据库记录定时dump到磁盘上的RDB持久化),RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。
      • 优点:是一个紧凑压缩的二进制文件,Redis加载RDB恢复数据远远快于AOF的方式。
    • 另外一种是AOF(append only file)持久化(原理是将Reids的操作日志以追加的方式写入文件),AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。
      • 优点:实时持久化。
  39. restful结构:(表征状态性转移)

    一种软件架构风格,描述的是在网络中client和server的一种交互形式,使得软件更简洁,更有层次,更易于实现缓存机制。

    规定:

    • 每一个url代表一种资源,客户端和服务器之间,传递这种资源的某种表现层
    • 客户端通过get,post,delete,update四个HTTP动词来对资源进行操作。
  40. tcp实现可靠传输:

    1. 确认和重传:接收方收到报文就会确认,发送方发送一段时间后没有收到确认就重传。
    2. 数据校验
    3. 数据合理分片和排序:IP数据报大于1500字节,大于MTU。这个时候发送方IP层就需要分片(fragmentation).把数据报分成若干片,使每 一片都小于MTU。而接收方IP层则需要进行数据报的重组。这样就会多做许多事情,而更严重的是,由于TCP的特性,当某一片数据传送中丢失时,接收方便无法重组数据报.将导致丢弃整个TCP数据报。tcp会按MTU合理分片,接收方会缓存未按序到达的数据,重新排序后再交给应用层。
    4. 流量控制:当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。
    5. 拥塞控制:当网络拥塞时,减少数据的发送
  41. flask,django面试题、https://blog.csdn.net/qq_41891803/article/details/81272575
    https://www.cnblogs.com/chongdongxiaoyu/p/9403399.html

  42. MVC,MVT:

    • MVC:软件的设计典范,用一种业务逻辑,数据,界面显示分离的方法组织代码,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。核心思想:解耦,让每一个模块独立的Model(模型):处理应用程序数据逻辑的部分,通常模型对象负责数据库存取数据。 View(视图):是应用程序中处理数据显示的部分,通常视图依据模型数据建立的。Controller(控制器):是应用程序处理用户交互的部分,通常控制器负责从视图读取数据,控制用户输入,并向模型发送数据。
    • MVT:本质上与MVC模式没有什么差别,也是各组件为了保持松耦合关系,知识定义上有些许不同。
      • model(模型):负责业务对象与数据库的对象(ORM)
      • Template(模板):负责如何把页面展示给用户
      • View(视图):负责业务逻辑,并在适当的时候调用Model和Template
  43. 暂无

  44. mysql 事务特征:

    1. 原子性(A):事务是最小单位,不可再分。
    2. 一致性(C):事务要求所有的DML语句操作的时候,必须保证同时成功或者同时失败
    3. 隔离性(I):事务A和事务B之间具有隔离性。
    4. 持久性(D):是事务的保证,事务终结的标志(内存的数据持久到硬盘文件中)
  45. docker了解不?

    Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的镜像中,然后发布到任何流行的 Linux或Windows 机器上,也可以实现虚拟化。

    Dockers的思想来源于集装箱,比如在一个大货轮上,各种各样的货物被集装箱标准化了,集装箱和集装箱之间不会互相影响。现在都流行云计算了,云计算就好比大货轮,docker就是集装箱。

    1. 不同的应用程序可能有不同的应用环境,比如开发的网站,依赖的软件环境并不一样,要是把他们安装在一个服务器上要调试很久,而且很麻烦。我们可以在服务器上创建不同的虚拟机在不同的虚拟机放置不同的应用,但是这样开销太大了。比如我电脑上装了一个虚拟机,运行起来都好卡。。而docker呢,可以实现虚拟机隔离应用环境的功能,且开销比较小。
    2. 比如开发软件用的是Ubuntu,但是运维管理用的是centos,那么在运维人员部署项目的时候,要是遇到有些东西不兼容,就会很麻烦,无形之中增加了工作量,但是,要是有docker你就可以将开发环境直接封装给运维人员,这就很方便,部署速度也很快。
    3. 在服务器负载方面,如果你单独开一个虚拟机,那么虚拟机会占用空闲内存的,docker部署的话,这些内存就会利用起来。
  46. python中使用下划线命名的规则:

    1. example:前后无下划线表示该变量、函数、成员公有,可以以任何形式访问
    2. _example:前置单下划线表示该内容受保护。如果是变量或函数在from_some_module import *这种情况下,不会被导入。如果是成员或者方法,仅允许类内部使用及该类的子类继承。
    3. __example:前置双下划线表示该变量、函数、成员私有,无法以任何方式被外部直接使用。类的私有成员及方法无法被子类继承,单对于本类来说,仍可以使用 a_instance._ClassName_MethodName()来调用
    4. __example__:前后双下划线的命名方式用于python里的特殊方法。
    5. example_:后置单下划线,用于避免和Python关键词冲突,无特殊含义,例如:int_

深信服

一、深信服python_笔经面经_牛客网 (nowcoder.com)

一面
  1. 自我介绍

  2. 手撕算法

    有1、2、3、4个数字,能组成多少个互不相同且无重复数字的三位数?都是多少?(10分钟让做完)

二面
  1. 项目,难点以及如何解决

  2. 手撕算法,输入字符串一维消消乐

  3. python mysql相关

    Python3 使用 PyMySQL 连接数据库,并实现简单的增删改查。

    操作流程:

    1. 先打开数据库连接:db = pymysql.connect()
    2. 获取操作游标:cursor = db.cursor()
    3. 执行sql语句:cur.execute(sql),用try包裹,这里可以添加多条SQL语句
    4. 获取所有结果:results = cursor.fetchall()
    5. 提交到数据库执行:db.commit(),用try包裹
    6. 执行错误时发送回滚:db.rollback(),用except包裹
    7. 最后关闭数据库连接:db.close()
  4. Py垃圾回收,内存机制

    垃圾回收

    python的内存管理是通过引用计数+清理来完成的。因此python的垃圾回收机制,很大一部分主要是处理引用计数无法解决的循环引用。

    1
    2
    3
    4
    typedef struct_object {
    int ob_refcnt; # 引用计数器
    struct_typeobject *ob_type;
    } PyObject;

    程序在运行的过程中会实时的更新ob_refcnt的值,来反映引用当前对象的名称数量。当某对象的引用计数值为0,那么它的内存就会被立即释放掉。

    以下情况是导致引用计数加一的情况:

    • 对象被创建:例如a=2
    • 对象被引用,b=a
    • 对象被作为参数,传入到一个函数中
    • 对象作为一个元素,存储在容器中

    下面的情况则会导致引用计数减一:

    • 对象别名被显示销毁 del
    • 对象别名被赋予新的对象
    • 一个对象离开他的作用域
    • 对象所在的容器被销毁或者是从容器中删除对象

    如果 a 是一个对象了,然后你赋值 b=a,那么这两个变量都指向同一个对象,

    一个变量和一个对象的关系叫做引用。在上面这个例子中,a 和 b 是对同一对象的两个引用;这样一个对象有不止一个引用,就也有了不止一个名字,所以就说这个对象有别名了。

    1. 标记清除算法:算法分为“标记”和“清除”两个阶段,首先标记所有需要回收的对象,在标记完成后统一回收所有被标记的对象。有两个不足:一是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发一次垃圾收集动作
    2. 复制算法:将内存分为两块,每次只使用其中的一块。当这一块内存用完了,就将还存活的对象复制到另一块上,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对一整块内存回收,内存分配时候也不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配即可,实现简单,运行高效。缺点是一次只能使用一部分内存,会有一些浪费。一般新生代会选择这种算法。
    3. 标记-整理算法:复制算法存在两个问题,1)会浪费50%的空间 2)如果被使用的内存中所有对象都100%存活的极端情况,就需要有额外的空间进行分配担保,因此老年代一般不能直接选用复制算法。有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”一样,后续步骤让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
    4. 分代回收算法:分代回收算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。比如新生代和老年代,不同代使用不同的回收算法。比如新生代使用复制算法,而老年代使用标记-清除或标记-整理算法

    内存机制

  5. MySql语句优化

  6. b树和b+树区别

二、深信服python一面二面面经_笔经面经_牛客网 (nowcoder.com)

一面
  1. 自我介绍
  2. 装饰器生成器多线程多进程
  3. 列表和元组,字典的底层实现
  4. 手撕算法——最长公共前缀
二面
  1. 自我介绍,项目介绍
  2. 项目中有没有偏开发的经验
  3. 你对python、c、c++哪种比较熟悉?说了java。问:你对java印象最深的方面有哪些?
  4. 对java和python多线程有什么理解?

三、深信服python一二面_笔经面经_牛客网 (nowcoder.com)

一面
  1. 自我介绍
  2. 排序(将列表排序)
  3. 链表相关(合并,排序,反转)
  4. 装饰器
  5. mysql
  6. 项目
二面
  1. 自我介绍
  2. 聊项目
  3. 两道算法题(递归和动态规划)

四、深信服Python开发一面二面 面经_笔经面经_牛客网 (nowcoder.com)

一面
  1. 自我介绍

  2. 手撕算法

    1. 列表分组

      image-20210923001324160

    2. LC232.用栈实现队列

    3. LC40.组合总和2

  3. Python了解哪些底层原理

  4. 如果进程内存使用过高,可能有几个G,在Python该如何查找具体是哪一个对象占用比较高(不会,就硬瞎说),通过哪个模块可以找到这些信息

  5. __new__()这种魔法函数实现单例,写伪代码

  6. 场景题:设计服务器,服务器只开了一个端口,但要在一分钟内处理上万的请求,用Python该处理这种上万级的请求

  7. 浏览器输入URL全过程

  8. Linux熟悉吗(不会)

  9. CLOSE-WAIT状态的含义,如果服务端出现过多C-W状态,该如何处理

  10. 请求的响应速度过慢,如何分析排除出具体原因(面试官提示说服务端的问题)

  11. 如何分析数据库慢查询,explan提示的字段有哪些,全表扫描的话会type字段会显示什么

  12. 如何监控Django层面操作的耗时,如何定位服务器问题

  13. 反问:面试表现,问岗位问技术栈

二面
  1. 如何考虑工作地点
  2. Python使用经验,其他语言掌握程度
  3. 算法题:消消乐,遇到连续3个一样的字符就消去,输出给定字符串消消乐后的最终结果
  4. Python回收内存管理机制,会不会出现回收不及时,程序运行过程中内存泄露该如何排查
  5. 程序里的每一个进程最多可以申请多少的空间
  6. 多线程过多协程,如果卡死,想要定位出错的点,怎样判断整个进程的状况
  7. namespaceentrypoint
  8. __slots__()是用来定义什么的
  9. Python协程原理(放弃)
  10. Django框架设计思维,设计机制
  11. Mysql中B树和B+树的使用场景和区别
  12. 定义一张表,这张表最多可以存多少条数据,这是由什么决定的
  13. 项目中如何保证数据库高可用,是否用到了主从机制
  14. 介绍项目,用户量增加后,系统原型可以从哪些地方进行改进,有参考业界其他类似需求的常见做法吗(无),项目中得到的最大收获
  15. 介绍研究生期间接触的其他技术,介绍论文
  16. 反问:面试表现(1.语言层面系统加强 2.技术视野还需加强),岗位技术栈

五、(1条未读通知) 深信服python开发一,二面_笔经面经_牛客网 (nowcoder.com)

一面
  1. 实习项目
  2. 如何学习django
  3. django的优缺点
  4. 竞赛项目描述(粤港澳),难点,有无后续优化
  5. python数据结构
  6. 元组和列表的异同
  7. 手撕快排
二面
  1. 了解深信服吗?
  2. 未来的工作方向和兴趣是什么?
  3. 手撕,消消乐(只要求水平面上)
  4. python的内存管理机制
  5. 发生内存泄漏如何排查
  6. 简述进程,线程,和协程(需要加强)

手撕算法解法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
def test1():
'''
有1、2、3、4个数字,能组成多少个互不相同且无重复数字的三位数?都是多少?
'''
def solve1():
count = 0
res = []
for i in range(1,5):
for j in range(1,5):
for k in range(1,5):
if i !=k and j!=k and i!=j:
res.append([i,j,k])
count += 1
print("共有 %d 种"% count)
# print(lres)

def solve2():
nums = [1,2,3,4]
res = []
temp = []
def backrct(idx):
if len(temp) == 3:
res.append(temp.copy())

for i in range(idx,4):
if nums[i] in temp: continue
temp.append(nums[i])
backrct(idx)
temp.pop()
backrct(0)
return res
solve1()
res = solve2()
print(len(res),res)

if __name__ == "__main__":
test1()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Sol:
def longestCommonPrefix(self,strs):
if len(strs) == 0:
return ""

strs.sort(key=lambda x:len(x)) # 按字符串的长度排序,最小的排前面
min_len = len(strs[0])

temp = strs[0]

res = ""
# 最长的公共字符串就是整个序列中最小的那个
# 字符依次递增
for i in range(1,min_len+1):
flag = True

# 依次与所有字符串比较
for cur_str in strs:
if temp[:i] != cur_str[:i]:
flag = False
break
if flag == True:
res = temp[:i]
return res

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class Sol:
def __init__(self,nums) -> None:
self.nums = nums
self.lens = len(nums)
def sorted1(self):
'''使用冒泡'''
for i in range(self.lens):
for j in range(0,self.lens-i-1): # 这里需要注意
if self.nums[j]>self.nums[j+1]:
print(i,j)
self.nums[j],self.nums[1+j] = self.nums[1+j],self.nums[j]

return self.nums

def quickSort(self,left,right):
low,high = left,right # 记录临时边界

if low < high:
temp = self.nums[low] # 使用第一个元素作为哨兵变量

while low < high:

while low < high and self.nums[high] >= temp: # 从后向前找第一个小于temp的数
high -= 1
self.nums[low] = self.nums[high] # 原来的low已经被保存了

while low < high and self.nums[low] < temp: # 从前向后找第一个大于temp的数
low += 1

self.nums[high] = self.nums[low] # 原来的high已经在上一步被换了

nums[low] = temp # 哨兵变量归位

self.quickSort(left,low) # 遍历左区间
self.quickSort(low+1,right) # 遍历右区间
return self.nums

三二

链表翻转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Solution:
# 返回ListNode
def ReverseList(self, pHead):
# write code here
if pHead == None:
return None
qHead = ListNode(0) # 使用头插法
while pHead:
t = ListNode(pHead.val)
t.next = qHead.next
qHead.next = t
# print(pHead.val)
pHead = pHead.next
return qHead.next

链表合并

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode:
if l1 == None and l2 == None:
return None
elif l1 != None and l2 == None:
return l1
elif l1 == None and l2 != None:
return l2
if l1.val < l2.val:
res = ListNode(l1.val)
l1 = l1.next
else:
res = ListNode(l2.val)
l2 = l2.next


p = res
# 遍历
while l1 or l2:
if l1 == None and l2 != None:
p.next = l2
break
elif l2 == None and l1 != None:
p.next = l1
break
else:
# print("++")
if l1.val < l2.val:
p.next = ListNode(l1.val) # 链接
p = p.next # 指向尾部
l1 = l1.next
else:
p.next = ListNode(l2.val)
p = p.next
l2 = l2.next
return res

链表排序

看到这题的第一反应就是利用数组的sort函数来自动排序然后更新链表中的节点值,这个思路很简单,就不画图解说,直接上代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Solution:
def sortInList(self , head ):
# write code here
#获取头部节点
h = head
#创建一个排序数组
l = []
#当节点存在的时候,依次添加节点值到数组
while h:
l.append(h.val)
h = h.next
l.sort() #利用sort函数对数组进行升序排列
#创建新链表
h = head
i = 0 #索引从0开始
#把排完序的数组重新赋值给新的链表,此时的链表的所有节点都是按升序排列的
while h:
h.val = l[i]
h = h.next
i += 1
return head

四二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class MyQueue:

def __init__(self):
"""
Initialize your data structure here.
"""
self.master = list() # 作为入栈队列
self.aux = list() # 作为出栈队列



def push(self, x: int) -> None:
"""
Push element x to the back of queue.
"""
self.master.append(x)


def pop(self) -> int:
"""
Removes the element from in front of queue and returns that element.
"""
if len(self.aux) != 0:
return self.aux.pop()
else:
while self.master:
temp = self.master.pop()
self.aux.append(temp)
return self.aux.pop()


def peek(self) -> int:
"""
Get the front element.
"""
if len(self.aux) != 0:
return self.aux[-1]
else:
while self.master:
temp = self.master.pop()
self.aux.append(temp)
return self.aux[-1]



def empty(self) -> bool:
"""
Returns whether the queue is empty.
"""
return True if len(self.master)+len(self.aux) == 0 else False

四三

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Solution:
def combinationSum2(self, candidates: list(), target: int):
ress = []
res = []
candidates = sorted(candidates)
n = len(candidates)
def backtract(i):
if sum(res) == target and res not in ress:
ress.append(res.copy())
return
for j in range(i,n):
# 如果索引不同的位置,节点相同则跳过,去重
if j != i and candidates[j] == candidates[j-1]:
continue
if sum(res) + candidates[j] > target:
break

res.append(candidates[j])

backtract(j+1) # 从下一个节点开始
res.pop()

backtract(0)
return ress

消消乐

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
def test1():
'''
只删除2个相邻的字母

给出由小写字母组成的字符串 S,重复项删除操作会选择两个相邻且相同的字母,并删除它们。
在 S 上反复执行重复项删除操作,直到无法继续删除。
在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。

输入:"abbaca"
输出:"ca"
解释:
例如,在 "abbaca" 中,我们可以删除 "bb" 由于两字母相邻且相同,这是此时唯一可以执行删除操作的重复项。
之后我们得到字符串 "aaca",其中又只有 "aa" 可以执行重复项删除操作,所以最后的字符串为 "ca"。
'''
S = 'abbaca'
st = []
# TODO 以下这种方式只能删除
for i in S:
if len(st) == 0:
st.append(i)
elif i == st[-1]: # 当前字符等于队列中最后一个字符,则弹出
st.pop()
else:
st.append(i)
print("".join(st))

def test2():
'''
相邻相同的字符一起消掉
在前面的基础上加个判断,判断元素是否与上一个被消除的元素一样(相邻多个元素相同也删除)
'''
# a = "abcccbxezzzrf7788fn"
# a = 'abcccdcececfc'
a = "1jj11j1"
s = []

# 前一个被消除的元素
del_str = ""
for ch in a:
if len(s) == 0:
if del_str == "":
s.append(ch) # 首次直接添加
else:
if ch == del_str: # 如果当前字符和前一个被删除的字符相同,则提前结束
continue
else:
s.append(ch)
del_str = "" # 初始化del_Str
else:
if ch == del_str:
continue # 如果当前字符和前一个字符相同,则跳过
elif ch == s[-1]: # 如果前一个元素和栈顶元素湘潭
del_str = s.pop(-1)
else:
# 入栈
s.append(ch)
del_str = ""

print("".join(s))

语法知识

装饰器

谈装饰器前,还要先要明白一件事,Python 中的函数和 Java、C++不太一样,Python 中的函数可以像普通变量一样当做参数传递给另外一个函数,例如:

1
2
3
4
5
6
7
def foo():
print("foo")

def bar(func):
func()

bar(foo)

正式回到我们的主题。装饰器本质上是一个 Python 函数或类,它可以让其他函数或类在不需要做任何代码修改的前提下增加额外功能,装饰器的返回值也是一个函数/类对象。

它经常用于有切面需求的场景,比如:插入日志、性能测试、事务处理、缓存、权限校验等场景,装饰器是解决这类问题的绝佳设计。有了装饰器,我们就可以抽离出大量与函数功能本身无关的雷同代码到装饰器中并继续重用。概括的讲,装饰器的作用就是为已经存在的对象添加额外的功能

先来看一个简单例子,虽然实际代码可能比这复杂很多:

1
2
def foo():
print("i am foo")

现在有一个新的需求,希望可以记录下函数的执行日志,于是在代码中添加日志代码:

1
2
3
def foo():
print('i am foo')
logging.info("foo is running")

如果函数bar()bar2() 也有类似的需求,怎么做?再写一个 loggingbar 函数里?这样就造成大量雷同的代码,为了减少重复写代码,我们可以这样做,重新定义一个新的函数:专门处理日志 ,日志处理完之后再执行真正的业务代码。

1
2
3
4
5
6
7
8
def use_logging(func):
logging.warn("%s is running" % func.__name__)
func()

def foo():
print('i am foo')

use_logging(foo)

这样做逻辑上是没问题的,功能是实现了,但是我们调用的时候不再是调用真正的业务逻辑 foo 函数,而是换成了 use_logging 函数,这就破坏了原有的代码结构, 现在我们不得不每次都要把原来的那个 foo 函数作为参数传递给 use_logging 函数,那么有没有更好的方式的呢?当然有,答案就是装饰器。

简单装饰器
1
2
3
4
5
6
7
8
9
10
11
12
def foo():
print("i am foo")

def use_logging(func):
def wrapper():
print("%s is running" % func.__name__)
return func() # 把 foo 当做参数传递进来时,执行func()就相当于执行foo()
return wrapper
# 因为装饰器 use_logging(foo) 返回的时函数对象 wrapper,这条语句相当于 foo = wrapper
# 执行foo()就相当于执行 wrapper()
foo = use_logging(foo) # 这个变量名和下面的必须一样
foo()

use_logging 就是一个装饰器,它一个普通的函数,它把执行真正业务逻辑的函数 func 包裹在其中,看起来像 foo 被 use_logging 装饰了一样,use_logging 返回的也是一个函数,这个函数的名字叫 wrapper

在这个例子中,函数进入和退出时 ,被称为一个横切面,这种编程方式被称为面向切面的编程。

@符号

如果你接触 Python 有一段时间了的话,想必你对@ 符号一定不陌生了,没错 @ 符号就是装饰器的语法糖,它放在函数开始定义的地方,这样就可以省略最后一步再次赋值的操作。

1
2
3
4
5
6
7
8
9
10
11
12
def use_logging(func):

def wrapper():
logging.warn("%s is running" % func.__name__)
return func()
return wrapper

@use_logging
def foo():
print("i am foo")

foo()

如上所示,有了 @ ,我们就可以省去foo = use_logging(foo)这一句了,直接调用 foo() 即可得到想要的结果。

装饰器在 Python 使用如此方便都要归因于 Python 的函数能像普通的对象一样能作为参数传递给其他函数,可以被赋值给其他变量,可以作为返回值,可以被定义在另外一个函数内。

*args,**kwargs

可能有人问,如果我的业务逻辑函数 foo 需要参数怎么办?比如:

1
2
def foo(name):
print("i am %s" % name)

我们可以在定义 wrapper 函数的时候指定参数:

1
2
3
4
def wrapper(name):
logging.warn("%s is running" % func.__name__)
return func(name)
return wrapper

这样 foo 函数定义的参数就可以定义在 wrapper 函数中。这时,又有人要问了,如果 foo 函数接收两个参数呢?三个参数呢?更有甚者,我可能传很多个。当装饰器不知道 foo 到底有多少个参数时,我们可以用 *args 来代替:

1
2
3
4
def wrapper(*args):
logging.warn("%s is running" % func.__name__)
return func(*args)
return wrapper

如此一来,甭管 foo 定义了多少个参数,我都可以完整地传递到 func 中去。这样就不影响 foo 的业务逻辑了。这时还有读者会问,如果 foo 函数还定义了一些关键字参数呢?比如:

1
2
def foo(name, age=None, height=None):
print("I am %s, age %s, height %s" % (name, age, height))

这时,你就可以把 wrapper 函数指定关键字函数:

1
2
3
4
5
def wrapper(*args, **kwargs):
# args是一个数组,kwargs一个字典
logging.warn("%s is running" % func.__name__)
return func(*args, **kwargs)
return wrapper
带参数的装饰器

装饰器还有更大的灵活性,例如带参数的装饰器,在上面的装饰器调用中,该装饰器接收唯一的参数就是执行业务的函数 foo

装饰器的语法允许我们在调用时,提供其它参数,比如@decorator(a)。这样,就为装饰器的编写和使用提供了更大的灵活性。

比如,我们可以在装饰器中指定日志的等级,因为不同业务函数可能需要的日志级别是不一样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def use_logging(level):
def decorator(func):
def wrapper(*args, **kwargs):
if level == "warn":
logging.warn("%s is running" % func.__name__)
elif level == "info":
logging.info("%s is running" % func.__name__)
return func(*args)
return wrapper

return decorator

@use_logging(level="warn")
def foo(name='foo'):
print("i am %s" % name)

foo()

# 与以下内容等价

a = use_logging("info")
b = a(foo)
b("123")

上面的 use_logging 是允许带参数的装饰器。它实际上是对原有装饰器的一个函数封装,并返回一个装饰器。

我们可以将它理解为一个含有参数的闭包。当我 们使用@use_logging(level="warn")调用的时候,Python 能够发现这一层的封装,并把参数传递到装饰器的环境中。

1
@use_logging(level="warn")`等价于`@decorator
类装饰器

装饰器不仅可以是函数,还可以是类,相比函数装饰器,类装饰器具有灵活度大、高内聚、封装性等优点。使用类装饰器主要依靠类的__call__方法,当使用 @ 形式将装饰器附加到函数上时,就会调用此方法。

functools.wraps

使用装饰器极大地复用了代码,但是他有一个缺点就是原函数的元信息不见了,比如函数的docstring__name__、参数列表,先看例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 装饰器
def logged(func):
def with_logging(*args, **kwargs):
print func.__name__ # 输出 'with_logging'
print func.__doc__ # 输出 None
return func(*args, **kwargs)
return with_logging

# 函数
@logged
def f(x):
"""does some math"""
return x + x * x

logged(f)

不难发现,函数 f 被with_logging取代了,当然它的docstring__name__就是变成了with_logging函数的信息了。

好在我们有functools.wrapswraps本身也是一个装饰器,它能把原函数的元信息拷贝到装饰器里面的 func 函数中,这使得装饰器里面的 func 函数也有和原函数 foo 一样的元信息了。

1
2
3
4
5
6
7
8
9
10
11
12
13
from functools import wraps
def logged(func):
@wraps(func)
def with_logging(*args, **kwargs):
print func.__name__ # 输出 'f'
print func.__doc__ # 输出 'does some math'
return func(*args, **kwargs)
return with_logging

@logged
def f(x):
"""does some math"""
return x + x * x
装饰器顺序

一个函数还可以同时定义多个装饰器,比如:

1
2
3
4
5
@a
@b
@c
def f ():
pass

它的执行顺序是从里到外,最先调用最里层的装饰器,最后调用最外层的装饰器,它等效于

1
f = a(b(c(f)))

生成器

通过列表生成式,我们可以直接创建一个列表。但是,受到内存限制,列表容量肯定是有限的。而且,创建一个包含100万个元素的列表,不仅占用很大的存储空间,如果我们仅仅需要访问前面几个元素,那后面绝大多数元素占用的空间都白白浪费了。

所以,如果列表元素可以按照某种算法推算出来,那我们是否可以在循环的过程中不断推算出后续的元素呢?这样就不必创建完整的list,从而节省大量的空间。在Python中,这种一边循环一边计算的机制,称为生成器:generator。

要创建一个generator,有很多种方法。第一种方法很简单,只要把一个列表生成式的[]改成(),就创建了一个generator:

1
2
3
4
5
6
>>> L = [x * x for x in range(10)]
>>> L
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>> g = (x * x for x in range(10))
>>> g
<generator object <genexpr> at 0x1022ef630>

创建Lg的区别仅在于最外层的[]()L是一个list,而g是一个generator。

我们可以直接打印出list的每一个元素,但我们怎么打印出generator的每一个元素呢?

如果要一个一个打印出来,可以通过next()函数获得generator的下一个返回值:

1
2
3
4
5
6
>>> next(g)
0
>>> next(g)
1
>>> next(g)
4

我们讲过,generator保存的是算法,每次调用next(g),就计算出g的下一个元素的值,直到计算到最后一个元素,没有更多的元素时,抛出StopIteration的错误。

当然,上面这种不断调用next(g)实在是太变态了,正确的方法是使用for循环,因为generator也是可迭代对象。

如果一个函数定义中包含yield关键字,那么这个函数就不再是一个普通函数,而是一个generator:

1
2
3
4
5
6
7
def fib(max):
n, a, b = 0, 0, 1
while n < max:
yield b
a, b = b, a + b
n = n + 1
return 'done'

这里,最难理解的就是generator和函数的执行流程不一样。函数是顺序执行,遇到return语句或者最后一行函数语句就返回。而变成generator的函数,在每次调用next()的时候执行,遇到yield语句返回,再次执行时从上次返回的yield语句处继续执行。

多进程

要让Python程序实现多进程(multiprocessing),我们先了解操作系统的相关知识。

Unix/Linux操作系统提供了一个fork()系统调用,它非常特殊。普通的函数调用,调用一次,返回一次,但是fork()调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回。

子进程永远返回0,而父进程返回子进程的ID。这样做的理由是,一个父进程可以fork出很多子进程,所以,父进程要记下每个子进程的ID,而子进程只需要调用getppid()就可以拿到父进程的ID。

Python的os模块封装了常见的系统调用,其中就包括fork,可以在Python程序中轻松创建子进程:

1
2
3
4
5
6
7
8
9
import os

print('Process (%s) start...' % os.getpid())
# Only works on Unix/Linux/Mac:
pid = os.fork()
if pid == 0:
print('I am child process (%s) and my parent is %s.' % (os.getpid(), os.getppid()))
else:
print('I (%s) just created a child process (%s).' % (os.getpid(), pid))

运行结果如下:

1
2
3
4
Process (876) start...
I (876) just created a child process (877).
I am child process (877) and my parent is 876.

由于Windows没有fork调用,上面的代码在Windows上无法运行。而Mac系统是基于BSD(Unix的一种)内核,所以,在Mac下运行是没有问题的,推荐大家用Mac学Python!

有了fork调用,一个进程在接到新任务时就可以复制出一个子进程来处理新任务,常见的Apache服务器就是由父进程监听端口,每当有新的http请求时,就fork出子进程来处理新的http请求。

multiprocessing

如果你打算编写多进程的服务程序,Unix/Linux无疑是正确的选择。由于Windows没有fork调用,难道在Windows上无法用Python编写多进程的程序?

由于Python是跨平台的,自然也应该提供一个跨平台的多进程支持。multiprocessing模块就是跨平台版本的多进程模块。

multiprocessing模块提供了一个Process类来代表一个进程对象,下面的例子演示了启动一个子进程并等待其结束:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from multiprocessing import Process
import os

def run_proc(name):
print("run child process %s (%s)..." %(name,os.getpid()) )


if __name__ == "__main__":
print("parent process %s." % os.getpid())

p = Process(target=run_proc,args=("test1",))
print("Child process will start")
p.start()
p.join()
print("child process end")

创建子进程时,只需要传入一个执行函数和函数的参数,创建一个Process实例,用start()方法启动,这样创建进程比fork()还要简单。

join()方法可以等待子进程结束后再继续往下运行,通常用于进程间的同步。

Pool

pass

子进程
进程间通信

Process之间肯定是需要通信的,操作系统提供了很多机制来实现进程间的通信。Python的multiprocessing模块包装了底层的机制,提供了QueuePipes等多种方式来交换数据。

我们以Queue为例,在父进程中创建两个子进程,一个往Queue里写数据,一个从Queue里读数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from multiprocessing import Process,Queue
import os,time,random

def write(q):
print("Process to write: %s" % os.getpid())
for value in ["A","B","C"]:
print("Put %s to queue..." % value)
q.put(value)
time.sleep(random.randint(1,4))

def read(q):
print("Process to read: %s" % os.getpid())
while True:
value = q.get(True)
print("get %s from queue. "% value)

if __name__ == "__main__":
# 父进程创建Queue,传给子进程
q = Queue()
pw = Process(target=write,args=(q,))
pr = Process(target=read,args=(q,))

pw.start()
pr.start()

pw.join() # 等待pw结束:
pr.terminate() # pr进程里是死循环,无法等待其结束,只能强行终止:

Windows没有fork调用,因此,multiprocessing需要“模拟”出fork的效果,父进程所有Python对象都必须通过pickle序列化再传到子进程去,所以,如果multiprocessing在Windows下调用失败了,要先考虑是不是pickle失败了。

总结

在Unix/Linux下,可以使用fork()调用实现多进程。

要实现跨平台的多进程,可以使用multiprocessing模块。

进程间通信是通过QueuePipes等实现的。

多线程

多任务可以由多进程完成,也可以由一个进程内的多线程完成。

由于线程是操作系统直接支持的执行单元,因此,高级语言通常都内置多线程的支持,Python也不例外,并且,Python的线程是真正的Posix Thread,而不是模拟出来的线程。

Python的标准库提供了两个模块:_threadthreading_thread是低级模块,threading是高级模块,对_thread进行了封装。绝大多数情况下,我们只需要使用threading这个高级模块。

启动一个线程就是把一个函数传入并创建Thread实例,然后调用start()开始执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import time,threading

# 新线程执行的代码
def loop():
print("thread %s is running..." % threading.currentThread().name)
n = 0
while n < 5:
n += 1
print("thread %s >>> %s" % (threading.current_thread().name,n))
time.sleep(1)
print("Thread %s ended." % threading.current_thread().name)

t = threading.Thread(target=loop,name="LoopThread")
t.start()
t.join()
print("thread %s ended" % threading.current_thread().name)

由于任何进程默认就会启动一个线程,我们把该线程称为主线程,主线程又可以启动新的线程,Python的threading模块有个current_thread()函数,它永远返回当前线程的实例。

主线程实例的名字叫MainThread,子线程的名字在创建时指定,我们用LoopThread命名子线程。名字仅仅在打印时用来显示,完全没有其他意义,如果不起名字Python就自动给线程命名为Thread-1Thread-2……

LOCK(可用于通信)

多线程和多进程最大的不同在于,多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响,而多线程中,所有变量都由所有线程共享,所以,任何一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容给改乱了。

来看看多个线程同时操作一个变量怎么把内容给改乱了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import time, threading

# 假定这是你的银行存款:
balance = 0

def change_it(n):
# 先存后取,结果应该为0:
global balance
balance = balance + n
balance = balance - n

def run_thread(n):
for i in range(2000000):
change_it(n)

t1 = threading.Thread(target=run_thread, args=(5,))
t2 = threading.Thread(target=run_thread, args=(8,))
t1.start()
t2.start()
t1.join()
t2.join()
print(balance) # 输出19

我们定义了一个共享变量balance,初始值为0,并且启动两个线程,先存后取,理论上结果应该为0,但是,由于线程的调度是由操作系统决定的,当t1、t2交替执行时,只要循环次数足够多,balance的结果就不一定是0了。

原因是因为高级语言的一条语句在CPU执行时是若干条语句,即使一个简单的计算:

1
balance = balance + n

也分两步:

  1. 计算balance + n,存入临时变量中;
  2. 将临时变量的值赋给balance

两个线程同时一存一取,就可能导致余额不对,你肯定不希望你的银行存款莫名其妙地变成了负数,所以,我们必须确保一个线程在修改balance的时候,别的线程一定不能改。

如果我们要确保balance计算正确,就要给change_it()上一把锁,当某个线程开始执行change_it()时,我们说,该线程因为获得了锁,因此其他线程不能同时执行change_it(),只能等待,直到锁被释放后,获得该锁以后才能改。由于锁只有一个,无论多少线程,同一时刻最多只有一个线程持有该锁,所以,不会造成修改的冲突。创建一个锁就是通过threading.Lock()来实现:

1
2
3
4
5
6
7
8
9
lock = threading.Lock()
def run_thread(n):
for i in range(20000000):
# 先获取锁
lock.acquire()
try:
change_it(n)
finally:
lock.release() # 用完释放

当多个线程同时执行lock.acquire()时,只有一个线程能成功地获取锁,然后继续执行代码,其他线程就继续等待直到获得锁为止。

锁的好处就是确保了某段关键代码只能由一个线程从头到尾完整地执行,坏处当然也很多,首先是阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了。其次,由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁,导致多个线程全部挂起,既不能执行,也无法结束,只能靠操作系统强制终止。

多核CPU

如果你不幸拥有一个多核CPU,你肯定在想,多核应该可以同时执行多个线程。

如果写一个死循环的话,会出现什么情况呢?

我们可以监控到一个死循环线程会100%占用一个CPU。

如果有两个死循环线程,在多核CPU中,可以监控到会占用200%的CPU,也就是占用两个CPU核心。

要想把N核CPU的核心全部跑满,就必须启动N个死循环线程。

试试用Python写个死循环:

1
2
3
4
5
6
7
8
9
10
import threading,multiprocessing

def loop():
x = 0
while True:
x += 1

for i in range(multiprocessing.cpu_count()):
t = threading.Thread(target=loop)
t.start()

启动与CPU核心数量相同的N个线程,在4核CPU上可以监控到CPU占用率仅有102%,也就是仅使用了一核。

但是用C、C++或Java来改写相同的死循环,直接可以把全部核心跑满,4核就跑到400%,8核就跑到800%,为什么Python不行呢?

因为Python的线程虽然是真正的线程,但解释器执行代码时,有一个GIL锁:Global Interpreter Lock,任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。

ThreadLocal

在多线程环境下,每个线程都有自己的数据。一个线程使用自己的局部变量比使用全局变量好,因为局部变量只有线程自己能看见,不会影响其他线程,而全局变量的修改必须加锁

但是局部变量也有问题,就是在函数调用的时候,传递起来很麻烦:

1
2
3
4
5
6
7
8
9
10
11
12
13
def process_student(name):
std = Student(name)
# std是局部变量,但是每个函数都要用它,因此必须传进去:
do_task_1(std)
do_task_2(std)

def do_task_1(std):
do_subtask_1(std)
do_subtask_2(std)

def do_task_2(std):
do_subtask_2(std)
do_subtask_2(std)

每个函数一层一层调用都这么传参数那还得了?用全局变量?也不行,因为每个线程处理不同的Student对象,不能共享。

如果用一个全局dict存放所有的Student对象,然后以thread自身作为key获得线程对应的Student对象如何?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
global_dict = {}

def std_thread(name):
std = Student(name)
# 把std放到全局变量global_dict中:
global_dict[threading.current_thread()] = std
do_task_1()
do_task_2()

def do_task_1():
# 不传入std,而是根据当前线程查找:
std = global_dict[threading.current_thread()]
...

def do_task_2():
# 任何函数都可以查找出当前线程的std变量:
std = global_dict[threading.current_thread()]
...

这种方式理论上是可行的,它最大的优点是消除了std对象在每层函数中的传递问题,但是,每个函数获取std的代码有点丑。

有没有更简单的方式?

ThreadLocal应运而生,不用查找dictThreadLocal帮你自动做这件事:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import threading

# 创建全局ThreadLocal对象:
local_school = threading.local()

def process_stuedent():
# 获取当前线程管理的Stuednt
std = local_school.student
print("Hello, %s (in %s)" % ( std, threading.current_thread().name))

def process_thread(name):
# 绑定THreadLocal的stuednt
local_school.student = name
process_stuedent()

t1 = threading.Thread(target=process_thread,args=("Alice",),name="THreadA")
t2 = threading.Thread(target=process_thread,args=("Bob",),name="ThreadB")

t1.start()
t2.start()
t2.join()
t1.join()

全局变量local_school就是一个ThreadLocal对象,每个Thread对它都可以读写student属性,但互不影响。你可以把local_school看成全局变量,但每个属性如local_school.student都是线程的局部变量,可以任意读写而互不干扰,也不用管理锁的问题,ThreadLocal内部会处理。

可以理解为全局变量local_school是一个dict,不但可以用local_school.student,还可以绑定其他变量,如local_school.teacher等等。

ThreadLocal最常用的地方就是为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。

小结

一个ThreadLocal变量虽然是全局变量,但每个线程都只能读写自己线程的独立副本,互不干扰。ThreadLocal解决了参数在一个线程中各个函数之间互相传递的问题。

多线程通信

要实现对多个线程进行控制,其实本质上就是消息通信机制在起作用,利用这个机制发送指令,告诉线程,什么时候可以执行,什么时候不可以执行,执行什么内容。

线程中通信方法大致有如下三种:

  • threading.Event
  • threading.Condition
  • queue.Queue
Event事件

Python提供了非常简单的通信机制 Threading.Event,通用的条件变量。多个线程可以等待某个事件的发生,在事件发生后,所有的线程都会被激活

关于Event的使用也超级简单,就三个函数

1
2
3
4
5
6
7
8
9
10
event = threading.Event()

# 重置event,使得所有该event事件都处于待命状态
event.clear()

# 等待接收event的指令,决定是否阻塞程序执行
event.wait()

# 发送event指令,使所有设置该event事件的线程执行
event.set()

举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import time
import threading

class MyThread(threading.Thread):
def __init__(self, name,event):
super().__init__()
self.name = name
self.event = event

def run(self):
print("Thread: {} start at {}.".format(self.name,time.ctime(time.time())))
# 等待event.set()后,才能往下执行
self.event.wait() # 阻塞
print("Thread: {} finish at {}.".format(self.name,time.ctime(time.time())))


threads = []
event = threading.Event()
# 定义五个线程
[threads.append(MyThread(str(i), event)) for i in range(1,5)]

# 重置event,使得event.wait()起到阻塞作用
event.clear()


# 启动所有线程
[t.start() for t in threads]

print('等待5s...')
time.sleep(5)

print('唤醒所有线程...')
event.set()

执行一下,看看结果

1
2
3
4
5
6
7
8
9
10
11
12
Thread: 1 start at Sun May 13 20:38:08 2018
Thread: 2 start at Sun May 13 20:38:08 2018
Thread: 3 start at Sun May 13 20:38:08 2018
Thread: 4 start at Sun May 13 20:38:08 2018

等待5s...

唤醒所有线程...
Thread: 1 finish at Sun May 13 20:38:13 2018
Thread: 4 finish at Sun May 13 20:38:13 2018
Thread: 2 finish at Sun May 13 20:38:13 2018
Thread: 3 finish at Sun May 13 20:38:13 2018

可见在所有线程都启动(start())后,并不会执行完,而是都在self.event.wait()止住了,需要我们通过event.set()来给所有线程发送执行指令才能往下执行。

Condition

Condition和Event 是类似的,并没有多大区别。

同样,Condition也只需要掌握几个函数即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
cond = threading.Condition()

# 类似lock.acquire()
cond.acquire()

# 类似lock.release()
cond.release()

# 等待指定触发,同时会释放对锁的获取,直到被notify才重新占有琐。
cond.wait()

# 发送指定,触发执行
cond.notify()

举个网上一个比较趣的捉迷藏的例子来看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import time,threading

class Hider(threading.Thread):
def __init__(self,cond,name):
super(Hider,self).__init__()
self.cond = cond
self.name = name

def run(self):
time.sleep(1) # 确保先运行Seeker中的方法
self.cond.acquire() # 获取锁

print(self.name + ":我已经把眼睛蒙上了")
self.cond.notify() # 唤醒他

self.cond.wait() # 阻塞
print(self.name + ":我找到你了哦 ~")
self.cond.notify() # 唤醒他

self.cond.release() # 释放锁
print(self.name + ":我赢了")

class Seeker(threading.Thread):
def __init__(self,cond,name):
super(Seeker,self).__init__()
self.cond = cond
self.name = name

def run(self):
self.cond.acquire()

self.cond.wait() # 阻塞,释放锁
print(self.name + ":我已经藏好了,你快来找我吧")
self.cond.notify() # 唤醒他
self.cond.wait() # 阻塞自己
self.cond.release()
print(self.name + ":被你找到了,哎~")

cond = threading.Condition()
seeker = Seeker(cond,"seeker")
hider = Hider(cond,"hider")
seeker.start()
hider.start()

通过cond来通信,阻塞自己,并使对方执行。从而,达到有顺序的执行。

Queue队列

最后一个,队列,它是本节的重点,因为它是我们日常开发中最使用频率最高的。

从一个线程向另一个线程发送数据最安全的方式可能就是使用 queue 库中的队列了。创建一个被多个线程共享的 Queue 对象,这些线程通过使用put()get() 操作来向队列中发送和获取元素。

同样,对于Queue,我们也只需要掌握几个函数即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from queue import Queue
# maxsize默认为0,不受限
# 一旦>0,而消息数又达到限制,q.put()也将阻塞
q = Queue(maxsize=0)

# 默认阻塞程序,等待队列消息,可设置超时时间
q.get(block=True, timeout=None)

# 发送消息:默认会阻塞程序至队列中有空闲位置放入数据
q.put(item, block=True, timeout=None)

# 等待所有的消息都被消费完
q.join()


# 通知队列任务处理已经完成,当所有任务都处理完成时,join() 阻塞将会解除
q.task_done()

以下三个方法,知道就好,一般不需要使用

1
2
3
4
5
6
7
8
# 查询当前队列的消息个数
q.qsize()

# 队列消息是否都被消费完,返回 True/False
q.empty()

# 检测队列里消息是否已满
q.full()

函数会比之前的多一些,同时也从另一方面说明了其功能更加丰富。

列表和元组的实现原理

列表是一个线性的集合,它允许用户在任何位置插入、删除、访问和替换元素。
列表实现是基于数组或基于链表结构的。当使用列表迭代器的时候,双链表结构比单链表结构更快。
有序的列表是元素总是按照升序或者降序排列的元素。

python中的列表的英文名是list,因此很容易和其它语言(C++, Java等)标准库中常见的链表混淆。事实上CPython的列表根本不是列表(可能换成英文理解起来容易些:python中的list不是list)。在CPython中,列表被实现为长度可变的数组

从细节上看,Python中的列表是由对其它对象的引用组成的连续数组。

指向这个数组的指针及其长度被保存在一个列表头结构中。

这意味着,每次添加或删除一个元素时,由引用组成的数组需要该标大小(重新分配)。

幸运的是,Python在创建这些数组时采用了指数分配,所以并不是每次操作都需要改变数组的大小。

但是,也因为这个原因添加或取出元素的平摊复杂度较低。
不幸的是,在普通链表上“代价很小”的其它一些操作在Python中计算复杂度相对过高。

利用 list.insert(i,item) 方法在任意位置插入一个元素——复杂度O(N)
利用 list.pop(i) 或 list.remove(value) 删除一个元素——复杂度O(N)

那么 Python 列表的数据结构是怎么样的?

1
2
3
4
5
typedef struct {
PyObject_VAR_HEAD
PyObject **ob_item; // 指向列表元素的指针数组
Py_ssize_t allocated; // 是指申请的内存的槽的个数
} PyListObject;

元组

1
2
3
4
5
6
7
8
typedef struct {
struct _object *_ob_next;
struct _object *_ob_prev; // 双向环状链表中上一个和下一个,Python内部将对象放到链表中便于进行内存管理。

Py_ssize_t ob_refcnt; // 引用计数器,即:有多少变量使用了这个列表对象。
Py_ssize_t ob_size; // 元素个数
PyObject *ob_item[1]; // 存储元组中的元素 [指针,]
} PyTupleObject;

tuple 和 list 相似,本质也是一个数组,但是空间大小固定。不同于一般数组,Python 的 tuple 做了许多优化,来提升在程序中的效率。

作者

bd160jbgm

发布于

2021-09-13

更新于

2021-09-24

许可协议