人生苦短,我用python。
用python已多年,真的很方便,已经不愿意切回java或c/c++了。
相信未来python还会火一段时间。
做python工程师也有几年了,下面总结一下我的面经,如果能掌握下面的内容,相信面试python工程师很简单。
要做python工程师,首先要会用python,其次要搞懂python的一些内部实现原理。下面总结一下与python语言有关的一些面试(备注:我的python版本3.6.8,下面的代码均在我的环境下测试过)。
用python实现单例模式
方法一:__new__()函数
class SingleB:
_instance= None
def __new__(cls, *args, **kwargs):
if not cls._instance:
cls._instance= super(SingleB, cls).__new__(cls, *args, **kwargs)
return cls._instance
def __init__(self):
pass
sc= SingleB()
sd= SingleB()
print(sc is sd)
方法二:装饰器
import functools
def singleton(cls):
_instance_dict = {}
@functools.wraps(cls)
def wrapper(*args, **kwargs):
if cls not in _instance_dict:
_instance_dict[cls] = cls(*args, **kwargs)
return _instance_dict[cls]
return wrapper
@singleton
class SingleA:
def __init__(self):
self._age = 18
self._sex = 'female'
@property
def age(self):
return self._age
@age.setter
def age(self, age):
self._age = age
sa = SingleA()
se = SingleA()
print(sa is se)
方法三:模块
这个方法比较简单也比较取巧,就是把类定义和类实例对象写到一个py文件里,在别的py文件里import这个类实例对象就可以了,这样就可以保证只有一个类实例对象,这种方法其实比较取巧。
方法四:classmethod类方法
代码如下,自己体会:
import threading
class Singleton(object):
_instance_lock = threading.Lock()
def __init__(self):
pass
@classmethod
def instance(cls, *args, **kwargs):
if not hasattr(Singleton, "_instance"):
with Singleton._instance_lock:
if not hasattr(Singleton, "_instance"):
Singleton._instance = Singleton(*args, **kwargs)
return Singleton._instance
obj = Singleton.instance()
print(obj)
方法五:元类
代码如下,自己体会:
import threading
class SingletonMetaClass(type):
_instance_lock = threading.Lock()
def __call__(cls, *args, **kwargs):
if not hasattr(cls, "_instance"):
with SingletonMetaClass._instance_lock:
if not hasattr(cls, "_instance"):
cls._instance = super(SingletonMetaClass, cls).__call__(*args, **kwargs)
return cls._instance
class Foo(metaclass=SingletonMetaClass): #代码执行到这里的时候实际上是执行了其元类的__new__()方法
def __init__(self):
pass
obj1 = Foo() #实际上是调用其元类的__call__()方法
obj2 = Foo()
print(obj1 is obj2)
python元类
元类不常用,但还是要了解。python里一切皆对象,类也是对象,既然类是对象,那么可以对对象做的任何操作,也可以对类做同样的操作,比如赋值,传参等等,甚至也可以在运行时动态生成一个类,这里就不展开描述了,总之就把类当做一个对象就行。那么类是由什么产生的呢?答案是元类。具体怎么做呢?先用type函数来说明一下。type函数实际上就是一个元类,可以生成一个类。方法如下:
myClass = type('myOwnClass', (), {})
myobj = myClass()
print(myobj)
上面这段代码的结果是:
<__main__.myOwnClass object at 0x000000000217B470>
解释一下type函数的参数:第一个是要创建的类名称,第二个是要创建的类的父类元组,第三个是要创建的类的属性字典。
type函数可以创建一个类,但是其实我们一般不这么写,在python3中,一般这么写:
def meta_class_func(class_name, class_parents, class_attr):
# class_name 会保存类的名字 Foo
# class_parents 会保存类的父类 object
# class_attr 会以字典的方式保存所有的类属性
# 调用type来创建一个类
return type(class_name, class_parents, class_attr)
class Foo(metaclass=meta_class_func):
bar = 'bar'
f = Foo()
print(f)
这里metaclass用了一个函数,主要为了说明这个属性可以传入一个函数,只要这个函数能生成一个类就行。实际工作中如果要用元类,一般是下面这种写法:
class TestMetaClass(type):
# __new__ 是在__init__之前被调用的特殊方法
# __new__是用来创建对象并返回之的方法
# 而__init__只是用来将传入的参数初始化给对象
# 这里,创建的对象是类,我们希望能够自定义它,所以我们这里改写__new__
def __new__(cls, class_name, class_parents, class_attr):
# 想做什么可以在这里做
# 调用type来创建一个类
return type(class_name, class_parents, class_attr)
class Foo(metaclass=TestMetaClass):
bar = 'bar'
f = Foo()
print(f)
python里面的数据类型都有哪些
dict,set,list,这三个都是可变类型
int,float,str,tuple,这四个都是不可变类型
不可变类型可以作为dict的key和set的元素,可变类型不能作为dict的key和set的元素
python装饰器
下面是一个不带参数的装饰器:
import functools
import time
def log(func):
print('hahahahhahaha')
@functools.wraps(func)
def wrapper(*args, **kwargs):
print('call function: %s' % func.__name__)
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print('function cost time is: %s' %(end-start))
return wrapper
@log
def test(p):
print(test.__name__ + " param: " + p)
python装饰器的作用是什么
python装饰器常见的有两个用处,一是输出或者记录函数执行信息,比如函数运行时间等;二是缓存作用,比如单例模式属于缓存已经生成的类实例对象,或者缓存函数执行结果,当函数执行时间较长,装饰器可以缓存参数相同的同一函数的执行结果。
迭代器与生成器
python的dict查询插入时间复杂度,set呢,dict和set的实现原理是什么?
dict和set的查询和插入时间复杂度都是O(1)的。dict和set的实现原理是hash table。
python闭包,nonlocal是什么意思
python函数是怎么传值的
生成器的实现原理
新式类的继承算法
python异步编程
python的sort函数和sorted函数的实现原理
协程、线程、进程的区别
进程是操作系统资源分配的最小单位,线程是操作系统调度的最小单位,协程是线程内的一段执行程序,不需要操作系统调度,协程的切换属于用户程序自主切换。一台服务器只有一个操作系统内核,但是可能有多个cpu,每个cpu现在又是多核多线程的,那么多线程程序有什么好处呢?多线程可以真正做到并行吗?我们需要知道服务器上是多进程并发的,就是宏观上有多个进程在执行,撇开内核进程不考虑,单纯考虑用户进程,多进程之间需要竞争内核调度,由于线程才是内核调度的最小单位,所以当一个进程的子线程数越多的时候,理论上来讲,这个进程获得内核调度并在cpu上执行的概率越大。
那么在多个cpu多核cpu多线程cpu的服务器上,从硬件角度来讲是存在并行执行多个线程的能力的,假如这多个并行执行的线程属于同一个进程,那么这就达到了真正意义上的并发。
GIL
python是解释型语言,执行python代码时需要python解释器,python解释器有多种实现,常见以及常用的是CPython,GIL其实是CPython里引入的概念,JPython就没有GIL。但我们平常绝大多数人用的都是CPython,所以GIL问题不可避免。
说了半天,GIL是什么?Global Interpreter Lock,全局解释器锁。官方解释是:
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)
原来是防止多线程并发执行python字节码的一个Mutex(互斥锁),主要原因是CPython的内存管理器(比如引用计数器)不是线程安全的,搞个GIL就能保证线程安全了,所以GIL虽然像是一个bug般的存在,却也是必不可少的。
GIL的初衷是既支持多线程,又保证代码是线程安全的。但是GIL的存在在某种程度抵销了多线程的好处。长期以来,很多代码又已经依赖了GIL的线程安全特性,所以现在骑虎难下,尾大不掉。
由于GIL的存在,python中的多线程在执行时需要先获取GIL,而一个python解释器就一把锁,这不相当于多线程无法并行执行嘛。
目前来看,一个线程要想执行,必须先获取GIL,那么多线程要想实现真正并行,目前CPython解释器可以优化的地方就是释放GIL。释放GIL的时机,一个是线程在执行时遇到耗时IO,那么别的释放GIL的策略和时机目前CPython是怎么实现的呢?已知的是随着python版本的更新GIL也在不断优化。比如,当前执行线程在执行一段时间后主动释放GIL或者当前线程在执行一定数量的python字节码后主动释放GIL,以让别的线程有机会拿到GIL。但如果是这种策略的话,试想一下,如果是cpu密集型程序,多个线程争抢GIL,有可能GIL会被一个线程反复的拿到,从而平白无故的浪费了线程切换以及抢占GIL的时间,而这段时间没有执行有效代码且别的线程也没有拿到过GIL,从而总的执行效率并没有比单线程有所提升,反而是下降了。于是我们有了如下的预期:Python的多线程在多核CPU上,只对于IO密集型计算产生正面效果;而当有至少一个CPU密集型线程存在,那么多线程效率会由于GIL而大幅下降。
总结,释放GIL的时机:
1、遇到耗时io
2、python3.2之前,当cpu密集型计算时,有一个专门的ticks进行计数(说是计时也可以,tick是python解释器的一个指令运行时间),一旦ticks数值达到100(CPython解释器定期check),这个时候释放GIL,线程之间开始竞争GIL(说明:ticks这个数值可以进行设置来延长或者缩减获得GIL的线程使用cpu的时间)
3、python3.2之后,timeout
GIL机制的改进:
– 将切换颗粒度从基于opcode计数改成基于时间片计数
– 避免最近一次释放GIL锁的线程再次被立即调度
– 新增线程优先级功能(高优先级线程可以迫使其他线程释放所持有的GIL锁)
python内存管理
引用计数、垃圾回收、内存池。
python中的array
如果我们的list中只会有数字,那么array的效率往往比list更好,array和list常见的一个问题是append满的时候怎么办?如果预先申请的存储空间满了,再append的时候,会申请两倍于现有存储空间的数组空间,将原数据拷贝到新空间,在添加新元素。
python中list的append的实现原理
a = []
nums = [2]
a.append(nums)
print(id(nums))
print(id(a[0]))
这段代码我跑的结果是:
42025928
42025928
所以结论是:list a 中append的元素是引用
__new__()函数与__init__()函数的区别是什么
__new__()函数是在类生成实例之前调用的一个函数,而__init__()函数是类生成了一个实例之后用来初始化这个实例的函数
linux系统负载高如何排查及解决
如何看服务器负载:top命令
负载有三个数,分别是什么意思:在top命令显示的结果的上半部分,load average: 0.06, 0.60, 0.48,这代表的是机器负载,三个数据分别表示近1分钟、5分钟、15分钟时间内的任务队列的平均长度。
top命令显示的结果的上半部分,最后两行是内存信息,其中“22052k buffers”,表示用作内核缓存的内存量。“123988k cached”,表示缓冲的交换区总量(内存中的内容被换出到交换区,而后又被换入到内存,但使用过的交换区尚未被覆盖,该数值即为这些内容已存在于内存中的交换区的大小。相应的内存再次被换出时可不必再对交换区写入)。
top命令下半部分,进程信息区:
PPID:父进程id,第一个P表示parent,总是记不清这个P对应的英文是什么。
PR:优先级,priority
NI:nice值,负值表示高优先级,正值表示低优先级
TIME:进程使用的CPU时间总计,单位秒
TIME+:进程使用的CPU时间总计,单位1/100秒
VIRT:进程使用的虚拟内存总量,单位kb。VIRT=SWAP+RES
SWAP:进程使用的虚拟内存中,被换出的大小,单位kb
RES :进程使用的、未被换出的物理内存大小,单位kb,RES=CODE+DATA
CODE:可执行代码占用的物理内存大小,单位kb
DATA:可执行代码以外的部分(数据段+栈)占用的物理内存大小,单位kb
SHR:共享内存大小,单位kb
nFLT:页面错误次数,fault
nDRT:最后一次写入到现在,被修改过的页面数,dirty
S:进程状态。
D=不可中断的睡眠状态
R=运行
S=睡眠
T=跟踪/停止
Z=僵尸进程
COMMAND:命令名/命令行
默认情况下仅显示比较重要的PID、USER、PR、NI、VIRT、RES、SHR、S、%CPU、%MEM、TIME+、COMMAND列。可以通过下面的快捷键来更改显示内容。
更改显示内容通过 f 键可以选择显示的内容。按 f 键之后会显示列的列表,按 a-z 即可显示或隐藏对应的列,最后按回车键确定。
按 o 键可以改变列的显示顺序。按小写的 a-z 可以将相应的列向右移动,而大写的 A-Z 可以将相应的列向左移动。最后按回车键确定。
按大写的 F 或 O 键,然后按 a-z 可以将进程按照相应的列进行排序。而大写的 R 键可以将当前的排序倒转。
说一下负载:假设我们是单cpu核心服务器,既然是任务队列的平均长度,那么显然,多了也不好,少了也不好,多了比如1,说明cpu有处理不完的任务。少了比如0.1那么cpu太闲了必然是资源浪费,在分布式服务的情况下,如果有服务器长期处于0.1的负载下,那么这台机器可以考虑下线了,当然前提是下线后别的服务器负载不会出问题。一般有经验的系统管理员都会设置一个比较有参考价值的线0.7,考虑如下几种情况:
需要调查原因:如果你的系统负载长期在0.7附近徘徊,那么你需要花点儿时间调查一下有没有什么异常原因导致这样的负载。
现在就要立马修复:如果你的负载经常能到1.0的话,就得立刻采取措施将负载降到0.7以下,不然半夜或大周末的系统报警,你就得立马翻出电脑解决掉。
如果超过1,小心饭碗不保,不危言耸听。
以上,当服务器是多cpu核心的情况下,请自己计算平均每个cpu核心的负载。然后根据情况处理。
Load问题:
(1)、系统load高一定是性能有问题吗?
不一定,Load高也许是因为在进行cpu密集型的计算
(2)、系统Load高一定是CPU能力问题或数量不够吗?
不一定,Load高只是代表需要运行的队列累计过多了。但队列中的任务实际可能是耗Cpu的,也可能是耗i/0及其他因素的。
(3)、系统长期Load高,首先要增加CPU吗?
不一定,Load只是表象,不是实质。增加CPU个别情况下会临时看到Load下降,但治标不治本。
(4)、负载高怎么查瓶颈在哪里呢?
cpu、io还是内存问题?我们依次来看看。
vmstat,Virtual Meomory Statistics(虚拟内存统计),可对系统的虚拟内存、进程、CPU活动进行监控。是对系统的整体情况进行统计,不足之处是无法对某个进程进行深入分析。
iostat,io状态的统计,这个工具可能需要安装。iostat参数说明如下:
关于cpu的参数:
%user:CPU处在用户态下的时间占比
%nice:CPU处在带NICE值的用户态下的时间占比
%system:CPU处在内核态下的时间占比
%iowait:CPU等待输入输出完成时间的占比
%steal:管理程序维护另一个虚拟处理器时,虚拟CPU的无意识等待时间占比
%idle:CPU空闲时间占比
关于设备参数的解释:
tps:该设备每秒的传输次数
kB_read/s:每秒从设备读取的数据量
kB_wrtn/s:每秒向设备写入的数据量
kB_read: 读取的总数据量
kB_wrtn:写入的总数据量
mysql的binlog日志有哪三种格式
mysql的binlog日志有三种格式,分别为Statement,MiXED,以及ROW
Statement:每一条会修改数据的sql都会记录在binlog中。
Row:不记录sql语句上下文相关信息,仅保存哪条记录被修改。所以如果带条件的update操作,以及整表删除,alter表等操作,ROW格式会产生大量日志。
Mixed:是以上两种level的混合使用,一般的语句修改使用statment格式保存binlog,如一些函数,statement无法完成主从复制的操作,则采用row格式保存binlog,MySQL会根据执行的每一条具体的sql语句来区分对待记录的日志形式,也就是在Statement和Row之间选择一种。新版本的MySQL中队row level模式也被做了优化,并不是所有的修改都会以row level来记录,像遇到表结构变更的时候就会以statement模式来记录。至于update或者delete等修改数据的语句,还是会记录所有行的变更。
解释一下mysql的redo日志和undo日志
AMQP,Advanced Message Queuing Protocol,高级消息队列协议,是一个进程间传递异步消息的应用层网络协议,模型如下图:
交换机是用来转发消息的,根据不同的路由规则和交换机类型来转发消息。有下面四种交换机类型:
直连型交换机(direct exchange)是根据消息携带的路由键(routing key)将消息投递给对应绑定键的队列。
扇型交换机(funout exchange)将消息路由给绑定到它身上的所有队列,而不理会绑定的路由键。类似广播、订阅等。
主题交换机与直连交换机略有差异,是对rooting key和binding key做的模糊匹配,binding key中可以存在两种特殊字符“”与“#”,用于做模糊匹配,其中“”用于匹配一个单词,“#”用于匹配多个单词(可以是零个)。
头交换机,headers 类型的 Exchange 不依赖于 routing key 与 binding key 的匹配规则来路由消息,而是根据发送的消息内容中的 headers 属性进行匹配。
Rabbitmq
rabbitmq是amqp的一个实现,一个消息中间件,有server端和client端之分。kombu是rabbitmq的client端的一个实现,由于跟celery配合比较融洽,以及支持很多python底层库,现被广泛应用。当然了,kombu不仅支持rabbitmq,还支持别的消息中间件,通过将很多消息中间件抽象成一个类来实现无缝衔接和简单易用。