Python-常见面试题

杀马特。学长 韩版系。学妹 提交于 2020-03-11 08:59:59

什么是Python

  • Python是一种解释型语言,也就是说,它和C语言以及C的衍生语言不通,Python代码在运行之前不需要编译
  • Python是一种动态类型语言,指的是,你在声明变量时不需要指定变量的类型
  • Python让困难的事变的容易,因此程序员可以专注于算法和数据结构的设计,而不用处理底层的细节
  • Python用途非常广泛–网络应用,自动化,科学建模,大数据应用等等,它也常被用作“胶水语言”,用于帮助其他语言和组件改善运行状况

Python支持的数据类型

数字、字符串、元组、字典、列表

下划线的作用

  • _xxx:表示的是protected类型,即只允许其本身和子类进行访问
  • __xxx:表示的是private类型
  • __xxx__:表示的特列方式,如__init__

如何生成一个不可变集合

使用frozenset函数,将一个列表变成一个不可变的集合,如下:

s = frozenset([1, 2, 3])

is与==

is对比地址,==是对比值

多线程与多进程

对比维度 多进程 多线程
数据共享、同步 数据共享复杂,需要用到IPC;数据是分开的、同步简单 因为共享进程数据,所以共享简单,但也因为这个导致同步复杂
内存、CPU 占用内存多,切换复杂,CPU利用率低 占用内存小,切换简单,CPU利用率高
创建、销毁、切换 创建销毁、切换复杂,速度很慢 创建销毁、切换简单,速度很快
编程、调试 编程简单、调试简单 编程复杂、调试复杂
可靠性 进程之间不会相互影响 一个线程挂掉可能将会导致进程崩溃
分布式 适用于多核、多机分布式,如果一台机器不够,扩展到其他多台机器比较简单 适用于多核分布式
通信方式 管道、命名管道、信号量、信号、消息队列、共享内存和套接字 锁机制、信号量机制和信号机制

什么是协程

从编程的角度来看,协程的思想本质上就是控制流的主动让出和恢复机制!一个程序可以包含多个协程,可以对于一个进程包含多个线程,因而下面来我们来比较线程和协程。我们知道多个线程相对独立,有自己的上下文,切换受到系统控制;而协程也相对独立,有自己的上下文,但是其切换由自己控制,由当前协程切换到其他协程由当前协程控制!协程有如下几个特点:

  • 协同,因为是由程序员写的调度策略,其通过协作而不是通过抢占来进行切换
  • 在用户态完成创建、切换和销毁
  • generator经常用来实现协程

协程的优势:

  • 最大的优势就是协程极高的执行效率。因为协程之间的切换是由程序自身控制,因此,没有线程切换的开销,和多线程相比,线程数量越多,协程的性能优势越明显。
  • 第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多!

协程的缺点:

  • 无法利用多核资源:协程的本质是单线程,它不能同时将单个CPU的多个核用上,协程需要和进程配合才能运行在多CPU上。当然我们日常所编写的绝大部分应用都没有这个必要,除非是CPU密集型应用
  • 进行阻塞(Blocking)操作时(如IO)会阻塞掉整个程序

Restful Api

Restful Api有哪些设计规则和规范呢?

  • 资源。资源就是网络上的一个实体,一短文本,一张图片或一首歌曲。资源总是需要一个载体来反应它的内容。文本可以是TXT,也可以用HTML或者XML、图片可以是JPG或者PNG格式。JSON是目前最常用的资源表现形式
  • 统一接口。Restful风格的数据元操(create、read、update、delete)分别对应HTTP方法:GET用来获取资源,POST用来创建资源,PUT用来修改资源,DELETE用来删除资源。这样就统一了数据操作的接口
  • URI。可以用一个URI指向资源,即每个URI都对应一个特定的资源。要获取这个资源只需访问它对应的URI即可,因此URI就成了每一个资源的地址或标识符。
  • 无状态。所谓无状态就是所有的资源都可以使用URI来定位,而且这个定位与其他资源无关,也不会因为其他资源的变化而变化。

三次握手

  • 客户端发送位码为SYN=1,随机产生seq数据包到服务器
  • 服务器接收到SYN=1,知道客户端要求建立联机。向客户端发送ack(ack=客户端的seq+1),SYN=1,ACK=1,以及随机产生的seq数据包
  • 客户端接收到ack,检测是否正确。正确情况下,客户端会发送一个ack(ack=服务端的seq+1),ACK=1。服务端接收并验证正确后建立连接

四次挥手

  • 客户端发送连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,seq=u(前面传递过来的数据的最后一个字节的序号加1),此时客户端进入FIN_WAIT_1状态
  • 服务端接收到连接释放报文,发出确认报文,ACK=1,sck=u+1,并且带上自己的序号seq=v,此时服务端进入CLOSE_WAIT状态。客户端接收到服务端发出的确认请求之后,客户端进入FIN_WAIT_2状态
  • 服务端将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于处理半关闭状态,服务端可能还会发出seq=w,服务端进入LAST_ACK状态
  • 客户端接收到服务端的连接释放报文之后,发出确认报文,ACK=1,ack=w+1,seq=u+1,此时客户端进入TIME-WAIT状态。服务端接收到客户端发出的确认,立即进入CLOSED状态。撤销连接,结束此次连接!

*args与**kwargs

如果我们不知道要往函数中传入多少个参数时,或者我们想往函数中以列表或者元组的形式传参时,那么就可以使用*args
如果我们不知道要往函数中传入多少个关键词参数时,或者我们想往函数中以字典的形式传参时,那么就可以使用**kwargs
*args与**kwargs是约定成俗的写法

xrange与range的区别

在Python2中,xrange会生成一个迭代器,range会生成一个列表,在生成很大的数字序列的时候,xrange会比range性能优很多,因为不需要一上来就开辟一块很大的内存空间!值得注意的是,在遍历数值较大的情况之下,xrange将会报错,range不会,如下:

start = 1000000000000000000000
end = 1000000000000000000010
print xrange(start, end)        # OverflowError
print range(start, end)         

在Python3中已将xrange更名为range,并不存在名称为xrange的方法,并且不会出现如上的报错问题

迭代器与生成器

什么是迭代器?迭代器是一个带状态的对象,它能在你调用next()方法时,返回容器的下一个值,任何实现了__iter____next__()(python2中是next())方法的对象都是迭代器,__iter__返回迭代器自身,__next__()返回容器的下一个值,如果没有更多元素了,将会引发StopIteration错误!
生成器是特殊的迭代器,不过这种迭代器更加优雅

负载均衡、正向代理、反向代理

负载均衡:负载均衡是一种服务器或网络设备的集群技术,负载均衡将特定的业务分担给多个服务器或网络设备,从而提高了业务处理能力,保证了业务的高可用性!
正向代理:作为媒介将互联网的资源传递给与之关联的客户端,代理跟客户端处于同一局域网下,对于服务端是透明的
反向代理:根据客户端的请求,从服务端获取相应的资源传递给与之关联的客户端,代理跟服务端处于同一局域网下,对客户端是透明的

什么是猴子补丁

“猴子补丁”就是在函数或者对象已经定义之后,再去改变它们的行为。举个例子:

import datetime

datetime.datetime.now = lambda: datetime.datetime(2012, 12, 12)

大部分情况之下,这是一种很不好的做法,函数在代码库中行为最好是都保持一致。

什么是跨域

浏览器对JavaScript同源策略的限制,请求的URL必须与浏览器上的URL地址处于同域上,也就是域名,端口,协议相同

什么是索引

索引是一种数据结构,可以快速的帮助我们进行数据的查找!索引的数据结构与具体存储引擎的实现有关,MySQL中常用的索引有哈希索引、B+树索引等,我们常用的InnoDB存储引擎的默认索引实现是B+树索引

哈希索引与B+树索引的区别或者优劣

实现原理:哈希索引底层就是哈希表,进行查找时,调用一次哈希函数就可以获取到对应的键值,之后进行回表查询获取到实际的数据;B+树索引底层都是多路平衡查找树,对于每一次查询都是从根节点出发,查找到叶子节点方可以获取所查键值,然后根据查询判断是否需要进行回表查询数据!

从原理可以看出他们的不同,哈希索引进行等值查询更快,却无法进行范围查询!因为哈希索引进过哈希函数建立索引之后,索引的顺序与原顺序无法保持一致,不能支持范围查询。B+树索引的所有节点皆遵循左节点小于父节点,右节点大于父节点,天然支持范围查询!

  • 哈希索引不支持索引进行排序,原理同上
  • 哈希索引不支持模糊查询和多列索引的最左前缀匹配
  • 哈希索引在任何时候都避免不了进行回表查询;而B+树索引在满足一些特定条件(聚簇索引、覆盖索引等)的时候,可以只通过索引完成查询
  • 哈希索引虽然在等值查询较快,但是不稳定,性能不可预测,如果某个键值存在大量重复时,发生哈希碰撞,此时效率可能极差;而B+树索引的查询比较稳定,所有的查询都是从根节点到叶子节点,且树的高度较低

什么是事务

事务是一系列操作,要符合ACID特性。最常见的理解是:事务中的操作要么全部成功,要么全部失败!

ACID是什么?请详细介绍一下

A=Atomicity

原子性,就是上面所说的要么全部成功,要么全部失败,不可能只执行了一部分操作!

C=Consistency

一致性,系统总是从一个一致性的状态转移到另外一个一致性的状态,不会存在中间状态

I=Isolation

隔离性,通常来说,一个事务在提交之前对于其他事务是不可见的

D=Durability

持久性,一旦事务提交,那么永远就是这样子了,哪怕系统崩溃,也不会影响这个事务的结果

如果有多个事务在同时进行会怎么样

多事务的并发一般会造成以下几个问题:

  • 脏读:A事务读取到了B事务未提交的内容,而B事务后面进行了回滚
  • 不可重复读:当设置A事务只能读取B事务已经提交的部分,会造成A事务的两次查询结果不一样,因为期间B事务进行了提交操作
  • 幻读:A事务读取了一个范围的内容,与此同时B事务插入了一条数据,造成“幻读”

MySQL引擎

在MySQL客户端中,可以使用以下命令查看MySQL支持的引擎

mysql> show engines;

MySQL支持多种存储引擎,比如InnoDBMyISAMMemoryArchive等等,在大多数情况下,直接选择InnoDB存储引擎是最合适的,InnoDB也是MySQL默认的存储引擎。

InnoDBMyISAM的区别:

  • InnoDB支持事务,MyISAM不支持事务
  • InnoDB支持行级锁,MyISAM支持表级锁
  • InnoDB支持MVCC,而MyISAM不支持
  • InnoDB支持外键,MyISAM不支持外键
  • InnoDB不支持全文检索,MyISAM支持

MySQL中char与varchar的区别

  • 定长与变长的区别:char是定长,即长度固定;varchar是变长,即长度可变。当插入的字符串长度超过所设置的长度时,将会视情况来处理,如果是严格模式,将会拒绝插入并提示错误信息;如果是宽松模式,则会截取后插入。如果插入的字符串小于设置的长度时,char将会在后面补充空格,直至满足设置的长度;varchar则不会如此,插入多少个就存多少个
  • 存储容量的不同:char最多能存放字符个数为255;varchar则最多能存放65532个字符

MySQL字段为什么建议设置为not null

null值占用更多的字节,且会在程序中造成很多与预期不符的情况

MySQL性能优化

  • 查询缓存:MySQL服务器默认都开启了查询缓存
  • Limit 1:只获取一条数据时,加了Limit 1可以提高效率
  • 创建索引:为经常用来查询的字段创建索引
  • Join查询创建索引:Join查询的字段应创建索引
  • 千万不要 ORDER BY RAND()
  • 尽量避免使用select *
  • 同一字段的where查询,使用in而不适用or
  • 使用正确的数据类型:例如手机号通常使用字符串类型进行存储,如果查询时传递的是一个数字类型,尽管mysql会对其转义,但这依旧会增加查询时间

超大分页该怎么处理

我们先来举几个查询的例子:

select * from product limit 10, 20   0.016秒
select * from product limit 100, 20   0.016秒
select * from product limit 1000, 20   0.047秒
select * from product limit 10000, 20   0.094秒
select * from product limit 400000, 20   3.229秒
select * from product limit 866613, 20   37.44秒 

可以看出,随着我们查询的偏移量越来越大,查询所用的时间也越来越久!此时我们可以利用的覆盖索引来加速分页查询,我们知道如果索引查询中的语句只包含了那个索引列(覆盖索引),那么速度就会很快,下面我们再来看一下上面的查询:

select * from product where id >= (select id from product limit 866613, 1) limit 20

查询时间为0.2秒,这简直是一个质的飞跃

MySQL中drop、delete和truncate的区别

  • drop直接删除掉表;truncate删除表中数据,再插入时自增ID又从1开始;delete删除表中数据,可以加where字句。
  • delete语句执行删除的过程是每次从表中删除一行,并且会同时将该行的删除操作作为事务记录在日志中保存以便进行回滚操作;truncate则一次性的从表中删除所有的数据并不把单独的删除操作记录记入日志保存,未备份情况下,删除的行是不能回复的,执行速度相对delete快!
  • truncate后,这个表和索引所占用的空间会回复到初始大小;delete操作不会减少表和索引所占用的空间;drop会将表所占用的空间全部释放掉
  • 应用范围:truncate只能针对table,而delete可以使table和view

Redis支持哪些数据结构

字符串、字典、列表、集合、有序集合

Redis分布式锁

先使用setnx来争抢锁,抢到之后再用expire给锁加一个过期时间防止锁忘记了释放。但是,这样并不完善,如果在执行setnx之后,执行expire之前进程意外crash或者要重启维护了,那么这个锁将永远得不到释放!我们可以使用set指令,并指定相应的参数把setnxexpire合成一条指令来用

假如Redis里面有1亿个key,其中有10W个key是以固定已知的前缀命名的,如何将他们全部找出来?

使用keys指令可以扫除指定模式的key列表,但是因为redis是单线程的,keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复!这个时候我们可以使用scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复几率,需进行去重,整体花费时间会比直接使用keys指令长

如果有大量的key需要设置同一时间过期,一般需要注意什么?

如果大量的key过期时间设置的过于集中,到了那个过期的时间点,Redis可能会出现短暂的卡顿现象,一般需要在时间上加一个随机值,使得过期时间分散一点

Redis缺点

  • 数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。
  • Redis较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。为避免这一问题,运维人员在系统上线时必须确保有足够的空间,这对资源造成了很大的浪费。

什么是LRU算法

LRU(Least Recently Used),即最近最少使用,是一种缓存置换算法。在使用内存作为缓存的时候,缓存的大小一般是固定的。当缓存被占满,这个时候继续往缓存里面添加数据,就需要淘汰一部分老的数据,释放内存空间来存储新的数据。这个时候就可以使用LRU算法了。其核心思想是:如果一个数据在最近一段时间没有被用到,那么将来被使用到的可能性也很小,所以就可以被淘汰掉。

修改redis占用内存大小

  • 通过配置文件修改
# 设置redis最大占用内存大小为100MB
maxmemory 100mb
  • 通过命令修改
# 设置redis最大占用内存大小为100MB
127.0.0.1:6379> config set maxmemory 100mb

# 获取redis能使用的最大内存大小
127.0.0.1:6379> config get maxmemory

redis相较于memcached有哪些优势

  1. redis支持的数据类型比memcached多
  2. redis的速度比memcached快很多
  3. redis可以持久化其数据

redis中一个字符串类型的值能存储最大容量是多少?

512M

程序题

获取指定路径下的所有文件(包含子孙文件)

import os

def get_files(parent_path):
    for child in os.listdir(parent_path):
        child_path = os.path.join(parent_path, child)
        if os.path.isdir(child_path):
            get_files(child_path)
        else:
            print child_path

下面代码会输出什么

def f(x, l=[]):
    for i in range(x):
        l.append(i * i)
    print l
    
f(2)
f(2, [3, 2, 1])
f(3)

################ 结果如下 ################
[0, 1]
[3, 2, 1, 0, 1]
[0, 1, 0, 1, 4]

解析:第一个函数调用十分明显,for循环先后将0和1添加至了空列表l中。l是变量的名字,指向内存中存储的一个列表;第二个函数调用在一块新的内存中创建了新的列表。l这时指向了新生成的列表。之后再往新列表中添加0、1;第三个函数调用的结果就有些奇怪了。它使用了之前内存地址中存储的旧列表。这就是为什么它的前两个元素是0和1了。

函数参数传递

a = 1
def func_1(a):
    a = 2

func_1(a)
print(a)
    
l = []
def func_2(l):
    l.append(1)
    
func_2(l)
print(l)

################ 结果如下 ################
1
[1]

为什么会出现两种截然不同的效果。下面我们加一些打印:

a = 1
def func_1(a):
    print("func_1, first", id(a))
    a = 2
    print("func_1, second", id(a), id(2))

print("first", id(a))
func_1(a)
print("second", id(a))
print(a)

    
l = []
def func_2(l):
    print("func_2, first", id(l))
    l.append(1)

print("first", id(l))
func_2(l)
print("second", id(l))
print(l)

################ 结果如下 ################
('first', 140670366951256)
('func_1, first', 140670366951256)
('func_1, second', 140670366951232, 140670366951232)
('second', 140670366951256)
1
('first', 4322204704)
('func_2, first', 4322204704)
('second', 4322204704)
[1]

请看案例1的打印,进入函数时,a的内存地址还是引用上面a的地址,当经过重新赋值以后,内存地址变成了2的内存地址,而函数外面的a还是原来的地址,所以不发生改变!

生成器题目

下面程序打印结果为何:

>>> array = [1, 4, 5]
>>> g = (x for x in array if array.count(x) > 0)
>>> array = [2, 4, 8]
>>> print(list(array))

最终打印结果为[4]。意不意外,惊不惊喜!这是因为在生成器表达式中,in子句在声明时执行,而条件子句是在运行时执行

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!