Python面试题个人总结
文章目录
提示:以下是本篇文章正文内容,下面案例可供参考
一、什么是闭包?
闭包概念:在一个外函数中定义了一个内函数,内函数里运用了外函数的临时变量,并且外函数的返回值是内函数的引用。这样就构成了一个闭包
闭包条件:
1、在一个外函数中定义了一个内函数
2、内函数里运用了外函数的临时变量
3、并且外函数的返回值是内函数的引用
一般情况下,如果一个函数结束,函数的内部所有东西都会释放掉,还给内存,局部变量都会消失。但是闭包是一种特殊情况,如果外函数在结束的时候发现有自己的临时变量将来会在内部函数中用到,就把这个临时变量绑定给了内部函数,然后自己再结束
闭包函数实例
# outer是外部函数 a和b都是外函数的临时变量
def outer( a ):
b = 10
# inner是内函数
def inner():
#在内函数中 用到了外函数的临时变量
print(a+b)
# 外函数的返回值是内函数的引用
return inner
if __name__ == '__main__':
# 在这里我们调用外函数传入参数5
#此时外函数两个临时变量 a是5 b是10 ,并创建了内函数,然后把内函数的引用返回存给了demo
# 外函数结束的时候发现内部函数将会用到自己的临时变量,这两个临时变量就不会释放,会绑定给这个内部函数
demo = outer(5)
# 我们调用内部函数,看一看内部函数是不是能使用外部函数的临时变量
# demo存了外函数的返回值,也就是inner函数的引用,这里相当于执行inner函数
demo() # 15
修改闭包变量的实例
#修改闭包变量的实例
# outer是外部函数 a和b都是外函数的临时变量
def outer( a ):
b = 10 # a和b都是闭包变量
c = [a] #这里对应修改闭包变量的方法2
# inner是内函数
def inner():
#内函数中想修改闭包变量
# 方法1 nonlocal关键字声明
nonlocal b
b+=1
# 方法二,把闭包变量修改成可变数据类型 比如列表
c[0] += 1
print(c[0])
print(b)
# 外函数的返回值是内函数的引用
return inner
if __name__ == '__main__':
demo = outer(5)
demo() # 6 11
使用场景
使用装饰器简化代码,使代码更加优雅,同时尽可能避免使用全局变量
斐波那契数列的优化装饰器写法
from functools import wraps
def memo(func):
cache={}
@wraps(func)
def wrap(*args):
if args not in cache:
cache[args]=func(*args)
return cache[args]
return wrap
@memo
def fib(i):
if i<2: return 1
return fib(i-1)+fib(i-2)
print(fib(10))
二、装饰器
装饰器概念: 使用装饰器可以在不用更改原函数的代码前提下给函数增加新的功能
装饰器的原型
import time
def showtime(func):
def wrapper():
start_time = time.time()
func()
end_time = time.time()
print('spend is {}'.format(end_time - start_time))
return wrapper
def foo():
print('foo..')
time.sleep(3)
foo = showtime(foo)
foo()
不带参数的装饰器:(装饰器,被装饰函数都不带参数)
import time
def showtime(func):
def wrapper():
start_time = time.time()
func()
end_time = time.time()
print('spend is {}'.format(end_time - start_time))
return wrapper
@showtime #foo = showtime(foo)
def foo():
print('foo..')
time.sleep(3)
@showtime #doo = showtime(doo)
def doo():
print('doo..')
time.sleep(2)
foo()
doo()
带参数的被装饰的函数
import time
def showtime(func):
def wrapper(a, b):
start_time = time.time()
func(a,b)
end_time = time.time()
print('spend is {}'.format(end_time - start_time))
return wrapper
@showtime #add = showtime(add)
def add(a, b):
print(a+b)
time.sleep(1)
@showtime #sub = showtime(sub)
def sub(a,b):
print(a-b)
time.sleep(1)
add(5,4)
sub(3,2)
带参数的装饰器(装饰函数)
实际是对原有装饰器的一个函数的封装,并返回一个装饰器(一个含有参数的闭包函数),
当使用@time_logger(3)调用的时候,Python能发现这一层封装,并将参数传递到装饰器的环境去
import time
def time_logger(flag = 0):
def showtime(func):
def wrapper(a, b):
start_time = time.time()
func(a,b)
end_time = time.time()
print('spend is {}'.format(end_time - start_time))
if flag:
print('将此操作保留至日志')
return wrapper
return showtime
@time_logger(2) #得到闭包函数showtime,add = showtime(add)
def add(a, b):
print(a+b)
time.sleep(1)
add(3,4)
类装饰器:一般依靠类内部的__call__方法
import time
class Foo(object):
def __init__(self, func):
self._func = func
def __call__(self):
start_time = time.time()
self._func()
end_time = time.time()
print('spend is {}'.format(end_time - start_time))
@Foo #bar = Foo(bar)
def bar():
print('bar..')
time.sleep(2)
bar()
常用的内置装饰器:
@property: 可以把一个实例方法变成其同名属性,以支持实例访问
@staticmethod:改变一个方法为静态方法,静态方法不需要传递隐性的第一参数,静态方法的本质类型就是一个函数 一个静态方法可以直接通过类进行调用,也可以通过实例进行调用
@classmethod:修饰的方法不需要实例化,不需要 self 参数,但第一个参数需要是表示自身类的 cls 参数,可以来调用类的属性,类的方法,实例化对象等
三、深拷贝和浅拷贝
浅拷贝: 是对一个对象父级(外层)的拷贝,并不会拷贝子级(内部)
使用浅拷贝的时候,分为两种情况
- 第一种,如果最外层的数据类型是可变的,比如说列表,字典等,浅拷贝会开启新的地址空间去存放
- 第二种,如果最外层的数据类型是不可变的,比如元组,字符串等,浅拷贝对象的时候,还是引用对象的地址空间
浅拷贝示例
import copy
# 定义一个列表,其中第一个元素是可变类型
list1 = [[1, 2], 'fei', 66]
# 进行浅copy
list2 = copy.copy(list1)
# 对象地址是否相同
print(id(list1))
print(id(list2))
"""
对象的地址是不同的
2313807246344
2313789591048
"""
# 第一个元素地址是否相同
print(id(list1[0]))
print(id(list2[0]))
"""
结果是相同的
2313807270216
2313807270216
"""
# 第二个元素地址是否相同
print(id(list1[1]))
print(id(list2[1]))
"""
结果是相同的
2313789572576
2313789572576
"""
# 改变第一个值,查看复制对象变化
list1[0][0] = 2
print(list2)
"""
结果是复制对象也发生了变化[[2, 2], 'fei', 66]
"""
# 改变第二个值,查看复制对象变化
list1[1] = 'ge'
print(list2)
"""
结果是复制对象没发生变化[[2, 2], 'fei', 66]
"""
深拷贝: 深拷贝对一个对象是所有层次的拷贝(递归),内部和外部都会被拷贝过来
使用深拷贝也分两种情况:
- 第一种,最外层数据类型可变。这个时候,内部和外部的都会拷贝过来
- 第二种,外层数据类型不可变,如果里面是可变数据类型,会新开辟地址空间存放。如果内部数据类型不可变,才会如同浅拷贝一样,是对地址的引用
深拷贝示例
import copy
# 定义一个列表,其中第一个元素是可变类型
list1 = [[1, 2], 'fei', 66]
# 进行深copy
list2 = copy.deepcopy(list1)
# 对象地址是否相同
print(id(list1))
print(id(list2))
"""
对象的地址是不同的
2313807246344
2313789591048
"""
# 改变第一个值,查看复制对象变化
list1[0][0] = 2
print(list2)
"""
结果是复制对象始终没有发生变化[[1, 2], 'fei', 66]
"""
# 改变第二个值,查看复制对象变化
list1[1] = 'ge'
print(list2)
"""
结果是复制对象始终没有发生变化[[2, 2], 'fei', 66]
"""
分析: 当我们声明list1 = [[‘a’, ‘b’], 1, 2]时,计算机做了这些事:
1、在内存中开辟一段空间,用来存放字符a,b和数字1,数字2
2、在内存开辟一段空间,用来存放一个列表,我们称为list2。list2中存放两个指针,分别指向字符a和b
3、在内存开辟一段空间,用来存放一个列表lsit1,list1中存放三个指针,分别指向list2,数字1和数字2
当执行浅拷贝lsit3 = copy.copy(list1)时,计算机开辟一段内存给list3,list3保存三个指针,分别指向list2,数字1和数字2。当执行list1[0][0] = 'c’时,list2对象修改了,而list3的第一个指针指向list2,所以我们看到list3的第一个元素也变了。
当执行深拷贝lsit4 = copy.copy(list1)时,计算机开辟一段内存给list4,和浅拷贝不同的是,计算机同时开辟一段空间给新的列表,我们称为lsit5。list5中保存2个指针,分别指向字符a和b。同时list4保存三个指针,分别指向list5,数字1和数字2。此时,当执行list1[0][0] = 'c’时,list2对象修改了,但list4的第一个指针指向list5,list5并没有修改,所以list4没有改变。
总结:深拷贝会把可变对象也拷贝一份,而浅拷贝不会
四、os与sys的区别
区别: os模块负责程序与操作系统的交互,提供了访问操作系统底层的接口;sys模块负责程序与python解释器的交互,提供了一系列的函数和变量,用于操控python的运行时环境
os常用方法
- os.remove(‘path/filename’) 删除文件
- os.listdir(‘dirname’) 列出指定目录的文件
- os.getcwd() 取得当前工作目录
- os.path.dirname(‘path/filename’) 去掉文件名,返回目录路径
- os.path.join(path1[,path2[,…]]) 将分离的各部分组合成一个路径名
- os.path.split(‘path’) 返回( dirname(), basename())元组
- os.path.exists() 是否存在
sys常用方法
- sys.argv 命令行参数List,第一个元素是程序本身路径
- sys.exit(n) 退出程序,正常退出时exit(0)
- sys.path 返回模块的搜索路径,初始化时使用PYTHONPATH环境变量的值
五、self参数理解
概念理解: self是指当前被调用的对象,当你需要调用当前对象的方法或者属性时,要用self. 来进行调用。
class Person:
def __init__(self, name):
self.name = name
print(self) #打印self内容
def think(self):
return ("{} is thinking".format(self.name))
lisi = Person("test") #实例化一个对象
print(lisi) #可以发现 self内容与实例的内容是一致的
print(Person.think)
print(Person.think(lisi))
print(lisi.think)
六、简述面向对象中__new__和__init__区别
__init__概念: 是当实例对象创建完成后被调用的,然后设置对象属性的一些初始值,通常用在初始化一个类实例的时候,比如添加一些属性, 做一些额外的操作。是一个实例方法
__new__概念: 是在实例创建之前被调用的,因为它的任务就是创建实例然后返回该实例对象,是个静态方法
class Person:
def __new__(cls, name, age):
print("__new__ called")
return super(Person, cls).__new__(cls)
def __init__(self, name, age):
print("__init__ called")
self.name = name
self.age = age
def __str__(self):
return "Person: %s(%s)" % (self.name, self.age)
if __name__ == '__main__':
test = Person("bobo", 26)
print(test)
# 输出:
# __new__ called
# __init__ called
# Person: bobo(26)
__new__使用场景: 常用的使用__new__实现单例,重载__new__方法,实现自定义metaclass
七、单例模式
概念解析: 单例模式是一种常用的软件设计模式,该模式的主要目的是确保某个类只有一个实例存在;如若有成千上万的用户需要发送短信,那么我们就需要创建成千上万的发送短信的对象,实际上只需要调用对象的方法进行发送短信罢了,这个时候可以采用单例模式,节省资源
class Singleton:
def __new__(cls):
if not hasattr(cls, 'instance'):
cls.instance = super(Singleton, cls).__new__(cls)
return cls.instance
obj1 = Singleton()
obj2 = Singleton()
obj1.att1 = 'value'
print(obj1.att1)
print(obj2.att1)
八、生成器
概念理解: 迭代器(Iterator)是一种更宽泛的概念,生成器(generator Iterator)是一种迭代器,但反过来不成立。迭代器是任何实现了 next 方法的对象(object),可以通过 next(iterator) 对其进行迭代,迭代结束时会抛出 StopIteration 异常。
生成器条件分析:
- Generator Function:含有 yield 关键字的函数,会返回一系列值,可以使用 next() 对其返回值进行迭代。
- Generator Iterator:generator function 返回的对象。可以进行一次性地迭代。 Generator
- Expression:可以求值为 generator iterator 的表达式。使用小括号和 for 来定义
yield 原理: 它会临时挂起当前函数,记下其上下文(包括局部变量、待决的 try catch 等),将控制权返回给函数调用者。当下一次再调用其所在 generator function 时,会恢复保存的上下文,继续执行剩下的语句,直到再遇到 yield 或者退出为止;我们常见的 return 是与之相对的关键字,但 return 会结束函数调用,销毁上下文(弹出栈帧),将控制权返回给调用者
九、多线程与多进程
进程: 进程是程序的一个执行实例,每个运行中的程序,可以同时创建多个进程,但至少要一个。每个进程都提供执行程序所需的所有资源,都有一个虚拟的地址空间,可执行的代码,操作系统的接口,唯一的进程ID,等等。进程可以包含线程,并且每个进程必须至少一个线程,每个进程启动时都会先创建一个线程,即主线程,然后主线程在创建其他子线程
线程: 线程有时被称为轻量级进程,是程序执行流的最小单元。线程是进程的一个实体,是被系统独立调度和分派的基本单位,线程自己不独立拥有系统资源,它与同属于一个进程的其他线程共享该进程所拥有的全部资源。每一个应用程序都至少有一个进程和一个线程。在单个程序中同时运行多个线程完成不同的被划分成一块一块的工作,称为多线程
进程与线程的区别
- 同一个进程中的线程共享同一内存空间,但进程之间的内存空间是独立的。
- 同一个进程中的所有线程的数据是共享的,但进程之间的数据是独立的
- 对主线程的修改可能会影响其他线程的行为,但是父进程的修改(除了删除以外)不会影响其他子进程
- 线程是一个上下文的执行指令,而进程则是与运算相关的一簇资源
- 同一个进程的线程之间可以直接通信,但是进程之间的交流需要借助中间代理来实现
- 创建新的线程很容易,但是创建新的进程需要对父进程做一次复制。
- 一个线程可以操作同一进程的其他线程,但是进程只能操作其子进程。
- 线程启动速度快,进程启动速度慢(但是两者运行速度没有可比性)
全局解释器锁GIL GIL的全称是Global Interpreter Lock(全局解释器锁),是Python设计之初为了数据安全所做的决定。Python中的某个线程想要执行,必须先拿到GIL。可以把GIL看作是执行任务的“通行证”,并且在一个Python进程中,GIL只有一个。拿不到通行证的线程,就不允许进入CPU执行。GIL只在CPython解释器中才有,GIL 保证CPython进程中,只有一个线程执行字节码。甚至是在多核CPU的情况下,也只允许同时只能有一个CPU 上运行该进程的一个线程
使用join()方法主线程等待子线程执行完毕
import time
import threading
def doWating():
print('start waiting:', time.strftime('%H:%M:%S'))
time.sleep(3)
print('stop waiting', time.strftime('%H:%M:%S'))
t = threading.Thread(target=doWating)
t.start()
time.sleep(1)
print('start join')
t.join()
print('end join')
使用setDaemon(True)把所有的子线程都变成主线程的守护线程,当主线程结束后,守护子线程也会随之结束,整个程序也跟着退出
import threading
import time
def run():
print(threading.current_thread().getName(), "开始工作")
time.sleep(3) # 子线程停2s
print("子线程工作完毕")
for i in range(3):
t = threading.Thread(target=run,)
t.setDaemon(True) # 把子线程设置为守护线程,必须在start()之前设置
t.start()
time.sleep(1) # 主线程停1秒
print("主线程结束了!")
print(threading.active_count()) # 输出活跃的线程数
直接继承Thread类,并重写run方法
线程锁
class MyThread(threading.Thread):
def __init__(self, thread_name, lk):
super(MyThread, self).__init__(name=thread_name)
self.lk = lk
def run(self):
print("%s 正在运行中。。。。" % self.name)
# self.lk.acquire()
time.sleep(10)
# self.lk.release()
print("%s结束" % current_thread())
if __name__ == '__main__':
lk = threading.Lock()
for i in range(10):
MyThread("thread-" + str(i), lk).start()
类名:BoundedSemaphore。这种锁允许一定数量的线程同时更改数据,它不是互斥锁。比如地铁安检,排队人很多,工作人员只允许一定数量的人进入安检区,其它的人继续排队
import time
import threading
def run(n, se):
se.acquire()
print("run the thread: %s" % n)
time.sleep(1)
se.release()
# 设置允许5个线程同时运行
semaphore = threading.BoundedSemaphore(5)
for i in range(20):
t = threading.Thread(target=run, args=(i,semaphore))
t.start()