1. 可变与不可变类型
2. 谈谈浅拷贝与深拷贝
3. __new__和__init__的区别
4. 谈谈设计模式
5. 列表推导式和生成器的优劣
6. 什么是装饰器,想在函数之后进行装饰,怎么做?
7. 使用装饰器的单例模式和其他方法(如new方法,或者单文件实现的单例)相比,有何区别?
8. 谈谈线程与进程的区别
9. 谈谈Python垃圾回收机制
10. Python中的@property有什么作用? 如何实现成员变量的只读属性?
11. *args and **kwargs如何使用?
12. with语句应用场景是?有什么好处?能否简述实现原理
【进阶题目】
13. 标准库线程安全的队列是哪一个?不安全的是哪一个?
14. python适合的场景有哪些?当遇到计算密集型任务怎么办?
15. python高并发解决方案?
不可变对象 -- 该对象所指向的内存中的值不能被改变。当改变某个变量时候,由于其所指的值不能被改变,
相当于把原来的值复制一份后再改变,这会开辟一个新的地址,变量再指向这个新的地址。
可变对象 -- 该对象所指向的内存中的值可以被改变。变量(准确的说是引用)改变后,实际上是其所指的值
直接发生改变,并没有发生复制行为,也没有开辟新的出地址,通俗点说就是原地改变。
不可变:int,float,str,tuple
可变: list,dict,set
而引用类型的对象大小不固定,所以只将它们的内存地址(它是固定的)存放在栈,由于内存地址指向
实际对象,所有引用类型是按引用访问
深拷贝实现:直接复制对象本身,新的对象是独立的,拥有不一样的内存地址,对原对象的修改不影响新对象。
*题外话*:设计模式详解 -> https://www.cnblogs.com/Liqiongyu/p/5916710.html
简述:是经过总结、优化后的可重用解决方案。它是一种代码设计思想,不同的模式解决不同的问题,
跟编程语言不相关。
a. 工厂模式: 是一个在软件开发中用来创建对象的设计模式。(详解工厂模式:https://www.cnblogs.com/lizhitai/p/4471952.html)
意图:定义一个用于创建对象的接口,让子类决定实例化哪一个类。Factory Method 使一个类的实例化延迟到其子类。
适用性:
当一个类不知道它所必须创建的对象的类的时候。
当一个类希望由它的子类来指定它所创建的对象的时候。
当类将创建对象的职责委托给多个帮助子类中的某一个,并且你希望将哪一个帮助子类是代理者这一信息局部化的时候。
代码实现:
class Person: #基类(超类):提供一个抽象的接口来创建一个特定类型的对象
def __init__(self, name, gender):
self.name = name
self.gender = gender
def getName(self):
return self.name
def getGender(self):
return self.gender
class Male(Person): #子类1:继承自基类的子类
def __init__(self, name, gender):
super().__init__(name, gender)
print(self.gender, name)
class Female(Person): #子类2:继承自基类的子类
def __init__(self, name, gender):
super().__init__(name, gender)
print(self.gender, name)
class Factory: #工厂类(简易版可以只提供一个工厂函数)
def getPerson(self, name, gender): #工厂方法:是创建类的入口,用于创建用户想要的类
if gender == 'M':
return Male(name, 'male')
if gender == 'F':
return Female(name, 'female')
factory = Factory()
male = factory.getPerson("Chetan", "M")
female = factory.getPerson('Charle', 'F')
b. 单例模式:
意图:保证程序运行时全局环境中只有一个该类的实例
适用性:无论何时何处调用该类,都能使用同一个该类的实例对象的时候。
代码实现:
class Singleton: #一个通用的单例超类,其他类继承即可(也可通过装饰器实现)
def __new__(cls, *args, **kw):
if not hasattr(cls, '_instance'):
cls._instance = object.__new__(cls)
return cls._instance
class SingleSpam(Singleton):
def __init__(self, s):
self.s = s
def __str__(self):
return self.s
s1 = SingleSpam('spam')
print(id(s1), s1)
s2 = SingleSpam('spa')
print(id(s2), s2)
print(id(s1), s1) #s1被s2替换,永远只有一个SingleSpam实例对象
c. 装饰模式:
意图:动态地给一个对象添加一些额外的职责。就增加功能来说,装饰模式相比生成子类更为灵活。
适用性:
1. 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
2. 处理那些可以撤销的功能
代码实现:
class foo(object): #原类
def f1(self):
print("original f1")
def f2(self):
print("original f2")
class foo_decorator(object): #装饰类
def __init__(self, decorate):
self._decorate = decorate
def f1(self): #给原类方法添加功能
print("decorated f1") #可以是logging
self._decorate.f1()
def __getattr__(self, name):
return getattr(self._decoratee, name)
u = foo()
v = foo_decorator(u) #通过将实例作为参数传入的方式,动态修改对象的方法
v.f1()
v.f2()
d. 迭代器模式:
意图:提供一种方法顺序访问一个聚合对象中各个元素, 而又不需暴露该对象的内部表示。
适用性:
1. 访问一个聚合对象的内容而无需暴露它的内部表示。
2. 支持对聚合对象的多种遍历。
3. 为遍历不同的聚合结构提供一个统一的接口(即, 支持多态迭代)。
代码实现:
class concreIterator():
def __init__(self, container):
self._a = container
self.index = -1
def first(self):
return self._a[0] if self._a else None
def isDone(self):
if self.index + 1 >= len(self._a):
return True
return False
def next(self):
try:
ret = self._a[self.index+1]
self.index += 1
return ret
except:
return None
def currItem(self):
return self._a[self.index]
i = concreIterator([1,2,3,4,6])
while not i.isDone():
print(i.next())
print(i.next()) # None
# 使用magic方法构建迭代器
class iterable2:
def __init__(self,container):
self._a = container
self._index= -1
def __iter__(self):
return self
def __next__(self):
try:
ret = self._a[self._index+1]
self._index += 1
return ret
except:
raise StopIteration
print(iterable2([1,2,3]))
for i in iterable2([1,2,3]):
print(i)
...(共有二十多种,不必说完)
推导式:它将所有元素一次性加载到内存中,适合数量小的元素集合表示
生成器:只能遍历使用,每次返回一个元素,没有下一个元素时抛出异常。适合大数据量的处理。
装饰器是一个函数,这个函数的主要作用是包装另一个函数或类,动态修改其行为。
要在函数后进行装饰,只需要新增一个函数或类,将原函数作为参数传入新函数/类的方法中,
在其中执行原函数,并在执行前或后添加想要的功能即可。
使用装饰器实现不会重新初始化对象,它是直接返回之前的对象;
而其他方法均会再执行一次init方法,这也是装饰器实现单例的好处。
从四个方面来分析:
调度方不同:进程是操作系统调度的基本单位,而线程是cpu调度的基本单位。
开销不同:创建一个进程需要系统单独分配新的地址空间,代价昂贵;而创建新的线程可以直接
使用进程的内存空间,所以进程开销大于线程。
通信方式不同:进程间通信一般通过HTTP或RPC方式;而同一进程下的不同线程间是共享全局变量的,通信更简便。
健壮性不同:单个进程死掉不会影响其他进程,而单个线程死掉会导致整个进程死掉。
垃圾回收机制属于python内存管理机制中的一种。
它包含三种机制:
1. 引用计数
2. 标记清除
3. 分代回收
9.1. 引用计数
是一种直观,简单的垃圾收集技术。当内存中某个对象的引用计数为0时,
说明没有任何引用指向该对象,该对象就成为要被回收的垃圾了。其缺点是
不能处理一种特殊情况:循环引用,这种情况需要另一种机制,即【标记清除】
9.2. 标记清除
属于一种追踪式收集法,与引用计数不同,一般在【已申请内存空间达到阈值】时触发。
若垃圾回收后仍然内存不足,就报错。
原理:从位于内存中的一个根集开始,找到所有可达的对象集合,剩下不可达的对象都是待回收的垃圾。
过程:1.从根集开始,标记所有可达对象(这一步将进行循环引用的的拆除,然后可以识别真正的垃圾对象)
2.遍历所有对象,回收未标记对象(不可达对象)占用的内存。(不可达:用户无法调用,且引用计数不为0的对象)
带来新问题:当内存中的对象较多时,而真正需要的回收的对象又很少的时候,对这些可达对象的检测是很耗时的,
若生存周期较长的对象占比较大,那么每次垃圾回收就是低效率高耗时的操作,有较大优化空间,这时候又引入
【分代回收】机制。
分代回收策略着重于提升垃圾回收的效率。
当对象很多时,垃圾检测将耗费大量的时间而真的垃圾回收花不了多久。
对于这种多对象程序,我们可以把一些进行垃圾回收频率相近的对象称为“同一代”的对象。
垃圾检测的时候可以对频率较高的“代”多检测几次,反之,进行垃圾回收频率较低的“代”
可以少检测几次。这样就可以提高垃圾回收的效率了。至于如何判断一个对象属于什么代,
python中采取的方法是通过其生存时间来判断。如果在好几次垃圾检测中,该变量都是
reachable的话,那就说明这个变量越不是垃圾,就要把这个变量往高的代移动,要减少
对其进行垃圾检测的频率。
代码演示python的垃圾回收(着重于循环引用问题的解决):
import sys,gc
gc.set_debug(gc.DEBUG_STATS | gc.DEBUG_LEAK) #开启gc调试
a = []
b = []
a.append(b)
print(sys.getrefcount(a)) # 2,但a真实的引用计数应该是2-1=1 ,这个函数获取的计数包含一个引用副本
b.append(a)
print(sys.getrefcount(a)) # 3
print(a) # [[[...]]]
del a,b #显式删除变量,对应变量引用计数应该清零,但由于循环引用,其引用计数仍然为一。
# print(sys.getrefcount(a)) #显式删除后无法再调用变量,此步异常
unreachable_count = gc.collect() #执行垃圾回收(用的就是标记-清除算法),得到所有的不可达对象(垃圾)
print(666, gc.garbage) # [] 查看待回收的垃圾对象列表
print(unreachable_count) #检测此刻内存中的不可达对象的数量,所谓不可达:用户无法调用,且引用计数不为0,这也是内存泄露的原因之一
print(gc.collect()) # 0,即垃圾回收成功
@property装饰器就是负责把一个方法变成属性调用,通常用在属性的get方法和set方法,
通过设置@property可以实现实例成员变量的直接访问,又保留了参数的检查。
另外通过设置get方法而不定义set方法可以实现成员变量的只读属性。
代码示例:
class student:
@property
def birth(self):
return self._birth #使用一个内部变量来存储数据
@birth.setter #这个装饰器使得age可以被修改,否则就是read_only
def birth(self,v):
if v > 2019:
raise ValueError('birth cannot greate than 2019!')
self._birth = v
@property
def age(self):
self._age = 2019-self._birth
return self._age
s = student()
s.birth = 2011
print(s.age)
s.age = 22 #异常! 因age没有定义携带setter装饰器的方法,所以不能修改
*args代表位置参数,它会接收任意多个参数并把这些参数作为元组传递给函数。
**kwargs代表的关键字参数,允许你使用没有事先定义的参数名,kwagrs是一个dict;
另外,位置参数一定要放在关键字参数的前面。
场景:with语句适用于对资源进行访问的场合,确保不管使用过程中是否发生异常都会执行必要的“清理”操作,
释放资源,比如文件使用后自动关闭、线程中锁的自动获取和释放等。
好处:不需要人为释放资源。
原理:with 主要用到一个概念:上下文管理器。
12.1. 程序运行时,将with后紧跟的对象(一般是个class示例)称作上下文管理器。这个class必须
定义两个方法,__enter__()方法和__exit__(),前者的返回对象赋值给as后面变量。
12.2. 运行中如果发生了异常,那么将会把异常的类型,值和追踪传递给__exit__()方法。如果__exit__()
方法返回值为true,那么这个异常将会被抑制,否则这个异常将会被重新抛出。
12.3. 如果没有发生异常,也会调用__exit__()方法,但是传入的参数为None, None, None。
通常也是在这里放入代码进行如文件流/会话的关闭等操作。
标准库中的queue.Queue是线程安全的,基础类型list,dict,set,tuple都不是线程安全队列,因为没有锁机制。
适合场景:
14.1. web开发,有非常优秀的web开发框架,如django,flask,aiohttp,且文档完善。
14.2. linux运维。有完善的执行shell脚本的库。
14.3. 网络爬虫。是最适合写爬虫的语言,没有之一;有丰富的访问网页和提取HTML文档数据的库。
14.4. 对并发要求不高的场景。GIL使得python只适合应用于对IO密集型任务的并发处理,对计算密集型任务的并发无能为力。
处理计算密集型任务:
使用多进程来执行,进程之间相互独立,各自拥有系统分配的CPU资源和独立内存空间,实现并行
(但CPU必须是多核或多颗CPU,现代计算机均符合此条件)。
高并发有两种任务场景:
这种任务场景直接使用多线程库threading即可解决。
更高效:线程有上下文切换开销,使用协程更好,如asyncio、gevent库
提高这种任务的执行效率只能使用并行,而并行只能通多进程(multiprocessing)来实现。
以下是Gevent代码示例,更多关于gevent参考:https://blog.csdn.net/feixiaoxing/article/details/79056535:
import gevent
from gevent.lock import BoundedSemaphore
sem = BoundedSemaphore() #通过信号量来实现锁机制
def f1():
for i in range(5):
sem.acquire() #获得锁
print('this is ' + str(i))
sem.release() # 释放锁
gevent.sleep(2) #模拟IO事件操作,协程执行遇到IO事件时,将进行控制权转让
def f2():
for i in range(5):
sem.acquire()
print('that is ' + str(i))
# gevent.sleep(1)
sem.release()
t1 = gevent.spawn(f1) #孵化任务
t2 = gevent.spawn(f2)
gevent.joinall([t1,t2]) #执行一系列协程任务
'''
总结:gevent是基于协程的网络库,通过事件循环机制来实现并发编程(区别于并行)。
当某个协程遇到IO事件时,会将控制权转让给主线程,主线程再将控制权
交给IO事件已经完成的协程,若都没完成,主线程将一直持有控制权。
需注意:不管是threading还是gevent都不能利用多核优势,都只适合处理IO密集型的任务。
'''