Python Interview

1.类在实例化对象的时候函数的调用顺序依次是__call__==>__new__==>__init__
当类使用括号,既 Person(25)的时候,实际上就是调用了元类的__call__方法,然后__call__会去调用__new__方法创建一个空的对象,再调用__init__初始化这个对象,最后返回对象

也就是说,一个类在实例化的时候实际上是做了三件事情:
第一:触发元类中(造出这个类的类)的__call__方法
第二:通过__new__产生一个空对象
第三:通过__init__初始化这个对象
第四:返回这个对象
2.使用元类、普通类的__new__方法实现多线程模式下的单例模式


import threading


#元类方式
class SingletonType(type):
    _threading_lock = threading.Lock()

    def __call__(cls, *args, **kwargs):
        with cls._threading_lock:
            if not hasattr(cls, '_instance'):
                cls._instance = super().__call__(cls, *args, **kwargs)
            return cls._instance



# 普通类使用__new__实现
class Singleton(object):
    _threading_lock = threading.Lock()

    def __new__(cls, *args, **kwargs):
        with cls._threading_lock:
            if not hasattr(cls, "_instance"):
                cls._instance = super().__new__(cls, *args, **kwargs)
            return cls._instance

    def __init__(self):
        pass


# 非多线程下的单例模式,基于类装饰器实现,注意不能用super也不能直接hasattr
def singleton(cls):
instances = {}
def wrap(*args, **kwargs):
if cls not in instances:
instances[cls] = cls(*args, **kwargs)
return instances[cls]
return wrap

@singleton
class A(object):
pass
 
#私有变量命名方式
_foo:一种约定,用来指定变量私有.程序员用来指定私有变量的一种方式.不能用from module import * 导入,其他方面和公有一样访问;

__foo:这个有真正的意义:解析器用_classname__foo来代替这个名字,以区别和其他类相同的命名,它无法直接像公有成员一样随便访问,通过对象名._类名__xxx这样的方式可以访问.
这里有个关于生成器的创建问题面试官有考: 问: 将列表生成式中[]改成() 之后数据结构是否改变? 答案:是,从列表变为生成器

>>> 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 0x0000028F8B774200>
# python装饰器

from functools import wraps

def a(fun):
    @wraps(fun)
    def wrapped(*args, **kwargs):
        print('123')
        fun(*args, **kwargs)
    return wrapped


@a
def b():
    print('789')
# python新式类和经典类的区别
1)首先,写法不一样: class A: pass class B(object): pass 2)在多继承中,新式类采用广度优先搜索,而旧式类是采用深度优先搜索。 3)新式类更符合OOP编程思想,统一了python中的类型机制。 class A(): def __init__(self): pass def save(self): print "This is from A" class B(A): def __init__(self): pass class C(A): def __init__(self): pass def save(self): print "This is from C" class D(B,C): def __init__(self): pass fun = D() fun.save() 经典类的答案: This is from A (深度优先 D->B->A->C) 新式类的答案: This is from C (广度优先 D->B->C->A -> object)
线程全局锁(Global Interpreter Lock), 即Python为了保证线程安全而采取的独立线程运行的限制,说白了就是一个核只能在同一时间运行一个线程,实际上就是保护垃圾回收线程等解释器级别线程的线程安全
对于io密集型任务,python的多线程起到作用,但对于cpu密集型任务,python的多线程几乎占不到任何优势,还有可能因为争夺资源而变慢。

对于CPU密集型,解决办法就是多进程。
对于IO密集型,多线程可以有效解决,但是一个线程大概占用4m内存,一旦开启10000个线程就是40g内存,不仅需要内存过大,而且从用户态切换到核心态消耗过多系统时间,
此时就可以使用协程来解决,1个线程有100个协程,1个CPU有100个线程,就能解决10000个任务,进而解决内存和系统的消耗问题(协程对于CPU密集型无用,甚至可能增加时间消耗)
协程原理:https://mp.weixin.qq.com/s?__biz=MzIxMjY5NTE0MA==&mid=2247483720&idx=1&sn=f016c06ddd17765fd50b705fed64429c&scene=25#wechat_redirect

Python 3.5 asyncio协程代码实现 (参考 https://blog.csdn.net/weixin_41599977/article/details/93656042)
import asyncio
# async关键字创建一个协程(函数)
async def myfunc(sleep_num):
print('123')
await asyncio.sleep(sleep_num) # await关键字表示异步阻塞,一般用于耗时操作上,先去执行其他协程,回头再继续本协程
print('456')

loop = asyncio.get_event_loop() # 创建事件循环
tasks = [loop.create_task(myfunc(sleep_num)) for sleep_num in range(5)] # 在事件循环中创建多个任务,任务用列表表示
loop.run_until_complete(asyncio.wait(tasks)) # 运行协程, python3.5 可用run_until_complete; python3.7以后可以用run
# 协程gevent实现
import gevent
def test(time):
    print(1)
    gevent.sleep(time)
    print(2)
def test2(time):
    print(3)
    gevent.sleep(time)
    print(4)
if __name__ == '__main__':
    gevent.joinall([
        gevent.spawn(test, 2),
        gevent.spawn(test2, 3)
    ])
gevent与asyncio的区别:
1、gevent无需手动await指定耗时操作,使用补丁替换函数库后,遇到阻塞gevent会自动切换协程(自动挡,但是效率较低一点,直接打猴子补丁)
2、asyncio需要手动await指定阻塞操作(手动挡,效率较高,遇到部分代码需要使用特定协程库,如aiohttp)
深浅拷贝

import copy
a = [1, 2, 3, 4, ['a', 'b']]  #原始对象

b = a  #赋值,传对象的引用
c = copy.copy(a)  #对象拷贝,浅拷贝
d = copy.deepcopy(a)  #对象拷贝,深拷贝

a.append(5)  #修改对象a
a[4].append('c')  #修改对象a中的['a', 'b']数组对象

print 'a = ', a
print 'b = ', b
print 'c = ', c
print 'd = ', d

输出结果:
a =  [1, 2, 3, 4, ['a', 'b', 'c'], 5]
b =  [1, 2, 3, 4, ['a', 'b', 'c'], 5]
c =  [1, 2, 3, 4, ['a', 'b', 'c']]
d =  [1, 2, 3, 4, ['a', 'b']]
Python的垃圾回收机制:
引用计数为主,标记清除和分代回收为辅

1、通过引用计数可以清除str、int类型等非容器对象,也可以清除大部分list等容器对象
但是引用计数除了占用一小部分内存外,另外有一个大问题,就是循环引用问题,比如
a = list()
b = list()
a.append(b)
b.append(a)
实际上a和b列表都是需要回收的,但是在循环引用情况下他们的引用计数都不为0,因此使用引用计数无法回收这种情况

2、因此对于容器对象,可以使用标记清除算法
将所有容器对象放到一个有向图中,从根节点出发,如果对象是可达的,则不回收,代数+1;如果是不可达的,则要进行回收

但是这种回收算法需要每次都扫描整个图,因此不是随时使用,什么时候使用呢?就是在达到阈值的时候才开始

3、因此需要使用分代回收,将容器进行分代,对于新生代,阈值较低,会更经常进行标记清除,对于老年代,说明该变量长期被使用到,它们阈值较高,标记清除次数比较少
python切片:https://blog.csdn.net/onlyongwang/article/details/82586921
Python有了GIL就是线程安全的吗?答案并不是。
python的GIL锁只保证了解释器级别的线程安全,
对于业务代码,要保证线程安全是需要加锁的
Python2 和 Python3 的区别:

.许多对象返回了生成器,节省了内存,如 range, map, dict.values

2. print 从 关键字变为了 函数

.除法可返回浮点型,如 /2的结果从 2变为 2.5.新增了异步框架库asyncio  和 yield from 关键字

.python3不再有unicode对象,默认str就是unicode,因此中文前不需要加u,如:u('中文')

.一些性能优化方面

操作系统

简单说一说进程和线程以及它们的区别。

进程是操作系统进行资源调度和分配的一个独立单位。
线程是进程的实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。
一个进程可以有多个线程,多个线程也可以并发执行
线程同步的方式有哪些?(https://www.jianshu.com/p/16e152170fb6)

1、互斥量(互斥锁):采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。
2、信号量:它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。
3、事件(信号):通过通知操作(notify)的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作。

第一种方式可与第三种配合使用,第一种方式仅可用于同个进程中的不同线程,否则会出现死锁现象,后两种方式可用于不同进程下的不同线程同步
进程间的通信方式  
1、管道,类似先进先出的队列,由一个进程写,另一进程读。

实际操作中,管道分为:无名管道、命名管道。无名管道只能实现父子或者兄弟进程之间的通信,有名管道(FIFO)可以实现互不相关的两个进程之间的通信。

管道:管道是单向的、先进先出的、无结构的、固定大小的字节流,它把一个进程的标准输出和另一个进程的标准输入连接在一起。写进程在管道的尾端写入数据,读进程在管道的道端读出数据。数据读出后将从管道中移走,
其它读进程都不能再读到这些数据。管道提供了简单的流控制机制。进程试图读空管道时,在有数据写入管道前,进程将一直阻塞。同样地,管道已经满时,进程再试图写管道,在其它进程从管道中移走数据之前,写进程将一直阻塞。
2、消息队列:是一个在系统内核中用来保存消 息的队列,它在系统内核中是以消息链表的形式出现的。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
3、信号量:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其它进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
4、共享内存:共享内存允许两个或多个进程访问同一个逻辑内存。这一段内存可以被两个或两个以上的进程映射至自身的地址空间中,一个进程写入共享内存的信息,
可以被其他使用这个共享内存的进程,通过一个简单的内存读取读出,从而实现了进程间的通信。如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。
共享内存是最快的IPC方式,它是针对其它进程间通信方式运行效率低而专门设计的。它往往与其它通信机制(如信号量)配合使用,来实现进程间的同步和通信。
5、套接字:套接字也是一种进程间通信机制,与其它通信机制不同的是,它可用于不同机器间的进程通信。
什么是缓冲区溢出?有什么危害?其原因是什么?

缓冲区溢出是指当计算机向缓冲区填充数据时超出了缓冲区本身的容量,溢出的数据覆盖在合法数据上。

危害有以下两点:

程序崩溃,导致拒绝额服务
跳转并且执行一段恶意代码
造成缓冲区溢出的主要原因是程序中没有仔细检查用户输入。
什么是死锁?死锁产生的条件?

在两个或者多个并发进程中,如果每个进程持有某种资源而又等待其它进程释放它或它们现在保持着的资源,在未改变这种状态之前都不能向前推进,称这一组进程产生了死锁。通俗的讲就是两个或多个进程无限期的阻塞、相互等待的一种状态。

死锁产生的四个条件(有一个条件不成立,则不会产生死锁)

互斥条件:一个资源一次只能被一个进程使用
请求与保持条件:一个进程因请求资源而阻塞时,对已获得资源保持不放
不剥夺条件:进程获得的资源,在未完全使用完之前,不能强行剥夺
循环等待条件:若干进程之间形成一种头尾相接的环形等待资源关系
分段式存储管理、分页式存储管理,两个的区别?(作用都是进行内存管理、内存分配,实际就是将进程中函数、变量的逻辑地址转为内存物理地址,方便将程序、变量存入内存运行)

1、分页式存储管理:分页存储管理是将一个进程的地址(逻辑地址)划分成若干个大小相等的区域,称为页,相应地,将物理内存空间划分成与页相同大小(为了保证页内偏移一致)的若干个物理块,称为块。
在为进程分配内存时,将进程中的若干页分别装入多个不相邻接的块中。 操作系统为每个进程建立一张页表,存放在进程PCB控制块中,具体如下: 页号 物理块号
0 2 1 15 2 14 3 1 当逻辑地址为(110)时,表示页号为1,页面偏移量为10, 则这个逻辑地址对应的物理地址为:15+10=25 (页面中页号1对应块号15) 2、分段式存储管理:在分段存储管理方式中,作业的地址空间被划分为若干个段,每个段是一组完整的逻辑信息,如有主程序段、子程序段、数据段及堆栈段等,每个段都有自己的名字,
都是从零开始编址的一段连续的地址空间,各段长度是不等的,由存入的数据量大小决定。 具体段表如下: 段号 段长 起始地址
0 1K 4096 1 4K 17500 2 2K 8192 当存入数据大小为1K,逻辑地址为(110)时,表示段号为1,页面偏移量为10, 首先1K小于段长2K,不会产生越界中断,则这个逻辑地址对应的物理地址为:17500+10=17510 两者的区别: 1.页的大小固定是由系统确定的,将逻辑地址划分为页号和页内地址是由机器硬件实现的。而段的长度是不固定的,决定与用户的程序长度,通常由编译程序进行编译时根据信息的性质来划分。 2.分页式存储管理的作业地址空间是一维的(只需要提供页内地址),分段式的存储管理的作业管理地址空间是二维的(需要提供段名和段内地址)。
原文地址:https://www.cnblogs.com/yiduobaozhiblog1/p/15682118.html