宝子们,是不是每次面Python开发,都觉得自己基础贼扎实,结果面试官随口抛来几道基础题,当场就支支吾吾、翻车现场?
我前阵子跟做技术面试官的朋友唠嗑,他说最近面了20多个Python程序员,从应届生到3年经验的都有,拿10道高频基础题考他们,结果80%的人答错3道以上,甚至有一半人连5道都答不对。
更扎心的是,这些题都不是什么偏门的八股文,全是工作中天天用、面试必问的核心点,比如装饰器、深浅拷贝、GIL锁这些。很多人都是“会用但说不出原理”,或者记了个一知半解,被面试官追问两句就露馅了。
今天就把这10道“重灾区”面试题整理出来,每道题都讲清常见错误答案、正确答案+可运行代码、通俗原理解析,全程大白话,不整学术腔,刷完这篇,下次面试再遇到这些题,直接对答如流,让面试官刮目相看!
话不多说,直接上硬菜,建议收藏+点赞,面试前翻出来快速过一遍,稳得一批~
题目1:说说Python装饰器的核心原理,写一个最简单的装饰器
题目描述
解释装饰器的本质,并用代码实现一个基础的装饰器,实现函数执行时打印“函数开始执行”的功能。
常见错误答案
- 1. 装饰器就是一个函数,把另一个函数包起来就行,没啥特别的;
- 2. 装饰器直接写个函数嵌套,内部函数执行目标函数,最后返回值就行,不用考虑参数;
正确答案+代码演示
装饰器的核心是闭包,本质是一个接收函数作为参数、返回一个新函数的高阶函数,可以在不修改原函数代码、不改变原函数调用方式的前提下,给原函数增加额外功能,支持装饰有参/无参函数,还能叠加使用。
# 定义最简单的装饰器
deflog_decorator(func):
# 用*args和**kwargs接收任意参数,保证装饰器的通用性
defwrapper(*args, **kwargs):
# 给原函数增加的额外功能:打印执行提示
print("函数开始执行啦~")
# 执行原函数,并接收返回值(避免原函数有返回值时丢失)
result = func(*args, **kwargs)
return result
# 核心:返回内部的wrapper函数,而不是直接执行
return wrapper
# 用@语法糖装饰原函数,等价于 add = log_decorator(add)
@log_decorator
defadd(a, b):
return a + b
# 调用方式不变,还是原函数的调用方式
if __name__ == "__main__":
print(add(2, 3)) # 输出:函数开始执行啦~ 5
原理解析
把装饰器比作手机壳:原函数是裸机,手机壳(装饰器)不用拆手机零件(修改原函数代码),不用改变手机的使用方式(原函数调用方式),就能给手机增加防摔、美观的功能(给原函数加额外逻辑)。
而闭包就是手机壳的内部结构,能牢牢“包裹”住裸机(原函数),并且保留裸机的特性(原函数的参数、返回值),wrapper函数就是实际发挥作用的“壳体”,*args和**kwargs则是适配各种手机型号(任意参数函数)的通用设计。
题目2:说说Python生成器和迭代器的区别,分别写一个示例
题目描述
解释迭代器和生成器的定义,说明二者的关系和区别,并用代码实现一个迭代器、一个生成器。
常见错误答案
- 1. 生成器就是迭代器,二者没区别,只是叫法不一样;
- 2. 迭代器用
yield创建,生成器用__iter__和__next__创建; - 3. 生成器比迭代器快,因为不用占内存,迭代器占内存。
正确答案+代码演示
迭代器:是实现了迭代器协议(__iter__()和__next__()方法)的对象,是一个可遍历的容器,需要手动实现迭代逻辑,创建后会一次性生成所有数据,占用内存。
生成器:是特殊的迭代器(自带迭代器协议,无需手动实现),通过yield关键字创建,惰性求值(按需生成数据,一次只生成一个,用完即丢),几乎不占用内存,代码更简洁。
简单说:生成器属于迭代器,但迭代器不一定是生成器。
# 一、实现一个迭代器:遍历1-3的数字
classMyIterator:
def__init__(self):
self.num = 1# 初始化迭代起始值
# 迭代器协议:返回自身
def__iter__(self):
returnself
# 迭代器协议:返回下一个值,没有则抛StopIteration
def__next__(self):
ifself.num <= 3:
temp = self.num
self.num += 1
return temp
else:
raise StopIteration
# 测试迭代器
it = MyIterator()
for i in it:
print(i) # 输出:1 2 3
# 二、实现一个生成器:遍历1-3的数字(两种方式,推荐yield)
# 方式1:yield关键字(最常用)
defmy_generator():
for i inrange(1, 4):
yield i # 暂停函数,返回当前值,下次调用从暂停处继续
# 方式2:生成器表达式(类似列表推导式,用()代替[])
gen_expr = (i for i inrange(1, 4))
# 测试生成器
g = my_generator()
for i in g:
print(i) # 输出:1 2 3
for i in gen_expr:
print(i) # 输出:1 2 3
原理解析
把迭代器比作一本印好的书,所有内容(数据)都已经存在了,翻页(next())只是依次查看,书的页数(数据量)越大,占的空间(内存)越多;
生成器则是一个说书人,你让他讲下一个(next()),他才现场编(生成)一个,讲完就忘,不会把所有内容都记下来,哪怕要讲10000个故事,也只占一点点记忆空间。
题目3:Python的深拷贝和浅拷贝有什么区别?分别用代码演示
题目描述
解释浅拷贝(shallow copy)和深拷贝(deep copy)的含义,说明二者在处理嵌套对象时的差异,用代码实现。
常见错误答案
- 1. 浅拷贝和深拷贝都是复制对象,没区别,改新对象原对象都不变;
- 3. 浅拷贝只复制一层,深拷贝复制所有层,但处理非嵌套对象时也有区别。
正确答案+代码演示
拷贝的核心区别在于是否复制嵌套对象的引用,仅针对可变对象(列表、字典、自定义对象) 有效,不可变对象(字符串、数字、元组)因无法修改,拷贝后都会指向同一内存地址。
- • 浅拷贝:只复制对象的表层结构,嵌套对象仍然共享引用,修改新对象的嵌套部分,原对象会跟着变;
- • 深拷贝:复制对象的所有层级结构,嵌套对象也会被全新复制,新对象和原对象完全独立,互不影响;
- • =`不是拷贝:只是给原对象起了一个新名字,两个名字指向同一内存地址,改一个另一个必变。
Python中实现浅拷贝的方式:copy.copy()、对象自身的copy()方法(如list.copy())、切片[:];实现深拷贝需要导入copy模块的deepcopy()。
import copy
# 定义一个嵌套的可变对象(列表里套列表)
original = [1, 2, [3, 4]]
# 1. 赋值:不是拷贝
a = original
a[2][0] = 300
print("赋值后原对象:", original) # 输出:[1, 2, [300, 4]],原对象被修改
# 恢复原对象
original = [1, 2, [3, 4]]
# 2. 浅拷贝:copy.copy()
b = copy.copy(original)
b[0] = 100# 修改表层数据,原对象不变
b[2][1] = 400# 修改嵌套数据,原对象跟着变
print("浅拷贝后原对象:", original) # 输出:[1, 2, [3, 400]]
print("浅拷贝的新对象:", b) # 输出:[100, 2, [3, 400]]
# 恢复原对象
original = [1, 2, [3, 4]]
# 3. 深拷贝:copy.deepcopy()
c = copy.deepcopy(original)
c[0] = 1000# 修改表层数据
c[2][1] = 4000# 修改嵌套数据
print("深拷贝后原对象:", original) # 输出:[1, 2, [3, 4]],原对象完全不变
print("深拷贝的新对象:", c) # 输出:[1000, 2, [3, 4000]]
原理解析
把嵌套对象比作一个装着盒子的大盒子,原对象是原版大盒子。
- • 赋值
=:就是给原版大盒子贴了一个新标签,不管撕哪个标签,打开的都是同一个盒子; - • 浅拷贝:复制了一个新的大盒子,但大盒子里的小盒子还是原版的,你改大盒子里的纸巾(表层数据),原版没事,你改小盒子里的糖果(嵌套数据),原版的小盒子里的糖果也会变;
- • 深拷贝:复制了一个新的大盒子,并且把里面的小盒子、小盒子里的所有东西都全新复制了一遍,新盒子和原版盒子完全没关系,怎么改都互不影响。
题目4:什么是Python的GIL锁?它的影响是什么?
题目描述
解释GIL锁的全称和定义,说明它对Python多线程的影响,以及适用场景。
常见错误答案
- 1. GIL锁是Python的全局锁,有了它就不能用多线程了,多线程完全没用;
- 2. GIL锁是Python语言的特性,所有Python解释器都有;
- 3. GIL锁会让Python的多线程在任何场景下都比单线程慢。
正确答案+代码演示
GIL的全称是全局解释器锁(Global Interpreter Lock),是CPython解释器(Python官方默认解释器)的一个特性,不是Python语言的特性,Jython、IronPython等解释器没有GIL锁。
简单说,GIL锁的核心规则是:同一时刻,只有一个线程能获得Python解释器的执行权限,即使是多核CPU,Python多线程也只能利用一个核心。
影响:
- • 对CPU密集型任务(如数据计算、循环遍历):Python多线程因为GIL锁的限制,无法实现真正的并行,效率甚至不如单线程(线程切换还会带来额外开销);
- • 对IO密集型任务(如网络请求、文件读写、数据库操作):Python多线程依然有效,因为IO操作时线程会释放GIL锁,其他线程可以趁机执行。
如果要处理CPU密集型任务,Python推荐用多进程(multiprocessing),因为每个进程有独立的Python解释器和GIL锁,能利用多核CPU实现真正的并行。
import threading
import time
from multiprocessing import Process
# 定义CPU密集型任务:循环计算
defcpu_task(n):
res = 0
for i inrange(n):
res += i
# 定义IO密集型任务:模拟睡眠(对应网络/文件IO)
defio_task(n):
time.sleep(n)
# 测试CPU密集型:多线程
deftest_cpu_thread():
start = time.time()
t1 = threading.Thread(target=cpu_task, args=(10**8,))
t2 = threading.Thread(target=cpu_task, args=(10**8,))
t1.start()
t2.start()
t1.join()
t2.join()
end = time.time()
print(f"CPU密集型-多线程耗时:{end-start:.2f}秒")
# 测试CPU密集型:多进程
deftest_cpu_process():
start = time.time()
p1 = Process(target=cpu_task, args=(10**8,))
p2 = Process(target=cpu_task, args=(10**8,))
p1.start()
p2.start()
p1.join()
p2.join()
end = time.time()
print(f"CPU密集型-多进程耗时:{end-start:.2f}秒")
# 测试IO密集型:多线程
deftest_io_thread():
start = time.time()
t1 = threading.Thread(target=io_task, args=(2,))
t2 = threading.Thread(target=io_task, args=(2,))
t1.start()
t2.start()
t1.join()
t2.join()
end = time.time()
print(f"IO密集型-多线程耗时:{end-start:.2f}秒")
if __name__ == "__main__":
test_cpu_thread() # 耗时约10秒(因电脑配置而异)
test_cpu_process() # 耗时约5秒,利用多核,快一倍
test_io_thread() # 耗时约2秒,两个线程同时睡眠,效率高
原理解析
把Python解释器比作一个只有一把钥匙的卫生间,GIL锁就是这把唯一的钥匙,线程就是要上卫生间的人。
不管外面有多少人(多少线程),同一时刻只有一个人能拿到钥匙(获得GIL锁)进入卫生间(执行代码);这个人出来(线程执行完/遇到IO),钥匙才会交给下一个人。
对于CPU密集型的人(一直占着卫生间不出来),其他人只能干等,效率极低;对于IO密集型的人(进去一下就出来洗手,释放钥匙),其他人可以轮流使用,效率就很高。
而多进程就是建了多个独立的卫生间,每个卫生间都有自己的钥匙(独立GIL锁),多个人可以同时上厕所,完美解决CPU密集型任务的问题。
题目5:@staticmethod和@classmethod的区别是什么?
题目描述
解释Python中静态方法(@staticmethod)和类方法(@classmethod)的定义,说明二者的参数、调用方式、使用场景的差异,用代码演示。
常见错误答案
- 1. 两个装饰器没区别,都是给类定义的方法,不用实例化就能调用;
- 3. 实例方法、类方法、静态方法都能随便调用,没有使用场景的区别。
正确答案+代码演示
@staticmethod和@classmethod都是为类定义的方法,无需实例化类就能调用,也能通过实例调用,但二者的参数传递、绑定对象、使用场景有本质区别,同时和普通的实例方法(带self)也不同。
核心差异:
- • @staticmethod(静态方法):无默认参数,不绑定类也不绑定实例,只是挂在类里的普通函数,和类的属性、方法无关;
- • @classmethod(类方法):默认参数是
cls(代表类本身),绑定类,能通过cls访问/修改类的属性和方法,支持类的继承; - • 实例方法:默认参数是
self(代表类的实例),绑定实例,只能通过实例调用,能访问实例和类的属性。
classPerson:
# 类属性
species = "人类"
# 实例方法:带self,绑定实例
def__init__(self, name, age):
self.name = name # 实例属性
self.age = age
# 静态方法:无默认参数,不绑定任何对象
@staticmethod
defsay_hello():
print("你好呀~")
# 静态方法不能直接访问类属性/实例属性,如需访问需显式传入类/实例
print(f"我是{Person.species}")
# 类方法:带cls,绑定类,cls代表Person类本身
@classmethod
defchange_species(cls, new_species):
cls.species = new_species # 通过cls修改类属性
print(f"类属性被修改为:{cls.species}")
# 类方法:创建类的实例(常用场景:工厂方法)
@classmethod
defcreate_adult(cls, name):
return cls(name, 18) # 等价于Person(name, 18)
# 1. 静态方法:类直接调用/实例调用都可以
Person.say_hello() # 输出:你好呀~ 我是人类
p1 = Person("张三", 20)
p1.say_hello() # 输出同上
# 2. 类方法:类直接调用/实例调用,都能修改类属性
Person.change_species("智人") # 输出:类属性被修改为:智人
p1.change_species("现代人") # 输出:类属性被修改为:现代人
print(Person.species) # 输出:现代人
# 3. 类方法的经典场景:工厂方法,快速创建实例
p2 = Person.create_adult("李四")
print(p2.name, p2.age) # 输出:李四 18
# 4. 实例方法:只能通过实例调用
print(p1.name) # 输出:张三
原理解析
把类Person比作一家工厂,实例p1、p2比作工厂生产的产品。
- • 实例方法:是产品的专属功能,只有产品能使用(比如手机的拍照功能),工厂本身用不了;
- • 类方法:是工厂的生产功能,绑定工厂本身,能修改工厂的生产线(类属性),还能批量生产特定产品(工厂方法),工厂直接调用就行;
- • 静态方法:是工厂里的一个饮水机,挂在工厂里,但和生产无关,工厂能用来喝水,产品(如果能拿)也能用来喝水,本身不影响生产。
题目6:Python列表推导式的性能为什么比普通for循环好?
题目描述
解释列表推导式和普通for循环创建列表的性能差异原因,用代码验证性能差距。
常见错误答案
- 3. 列表推导式是并行执行,for循环是串行执行。
正确答案+代码演示
列表推导式的性能比普通for循环+append创建列表快20%~50%,核心原因不是代码长短,而是底层执行机制的差异,和并行无关,二者都是串行执行。
性能优势的本质:
- 1. 列表推导式是Python解释器的底层优化实现,用C语言级别的循环执行,而普通for循环是Python级别的循环,每次迭代都要执行Python的字节码,开销更大;
- 2. 列表推导式是一次性预分配内存,创建列表前会预估元素数量,直接分配足够的内存空间,而普通for循环的
append方法会动态扩容(当列表容量不足时,重新分配更大的内存,复制原数据),带来额外的内存开销。
注意:列表推导式的优势仅体现在创建列表,如果循环中需要复杂的逻辑(如多条件判断、函数调用),性能差距会缩小,且过于复杂的列表推导式会降低代码可读性,不建议使用。
import time
# 测试:创建100万个元素的列表,值为0-999999
n = 10**6
# 1. 普通for循环+append
start1 = time.time()
lst1 = []
for i inrange(n):
lst1.append(i)
end1 = time.time()
print(f"普通for循环耗时:{end1-start1:.4f}秒")
# 2. 列表推导式
start2 = time.time()
lst2 = [i for i inrange(n)]
end2 = time.time()
print(f"列表推导式耗时:{end2-start2:.4f}秒")
# 输出示例(因电脑配置而异):
# 普通for循环耗时:0.0821秒
# 列表推导式耗时:0.0312秒
# 列表推导式快了近3倍
原理解析
把创建列表比作搬砖盖房子,要搬100万块砖盖一面墙。
- • 普通for循环+append:相当于每次搬一块砖,都要跑回工棚拿一次手套(每次迭代执行Python字节码、判断是否扩容),搬完一块再搬下一块,中间的额外操作特别多;
- • 列表推导式:相当于提前准备好足够的手套,直接开个卡车去拉砖(底层C语言循环、一次性预分配内存),不用反复跑回工棚,也不用中途换卡车(动态扩容),全程一气呵成,效率自然更高。
题目7:说说Python的垃圾回收机制(GC)
题目描述
解释Python如何进行垃圾回收,核心的回收算法有哪些,分别处理什么场景。
常见错误答案
- 1. Python的垃圾回收就是靠引用计数,简单粗暴;
- 2. 引用计数为0就会立即回收内存,不会有内存泄漏;
- 3. Python的垃圾回收是自动的,程序员完全不用管内存。
正确答案+代码演示
Python的垃圾回收机制是以引用计数为基础,结合标记-清除、分代回收的混合机制,大部分情况下是自动的,但并非绝对,特殊场景下仍可能出现内存泄漏。
核心三大算法:
- 1. 引用计数(核心):Python中每个对象都有一个引用计数器,记录当前对象被多少个变量引用。当引用计数为0时,对象会被立即回收,释放内存。
- • 引用增加:对象被赋值给新变量、作为参数传入函数、加入容器;
- • 引用减少:变量被赋值为其他对象、变量超出作用域、容器被删除/移除对象。
- 2. 标记-清除(解决循环引用):引用计数的致命缺陷是无法处理循环引用(如两个对象互相引用,引用计数永远不为0),标记-清除算法会扫描所有对象,标记出可达对象(能被程序访问的),清除不可达对象(循环引用的对象),解决内存泄漏问题。
- 3. 分代回收(优化性能):基于“存活越久的对象,越不容易被回收”的统计规律,Python将对象分为3代(0代、1代、2代),新创建的对象属于0代,存活时间越长,代际越高。垃圾回收器会频繁扫描0代对象,较少扫描1代,极少扫描2代,减少扫描次数,提升回收效率。
import sys
# 测试引用计数
a = [1, 2, 3]
# sys.getrefcount()会把自身调用也算一次,所以结果比实际多1
print(sys.getrefcount(a)) # 输出:2(a引用 + 函数参数引用)
b = a
print(sys.getrefcount(a)) # 输出:3(a + b + 函数参数)
b = None# 解除b的引用
print(sys.getrefcount(a)) # 输出:2(a + 函数参数)
# 测试循环引用(引用计数无法处理)
x = [1]
y = [2]
x.append(y)
y.append(x)
# 解除x、y的引用,此时x和y互相引用,引用计数为1,不会被引用计数回收
x = None
y = None
# 此时需要标记-清除算法来回收这两个对象的内存
原理解析
把Python的内存空间比作一个宿舍,垃圾回收机制就是宿管阿姨,对象就是宿舍里的同学。
- • 引用计数:阿姨数每个同学的朋友数量(引用数),如果一个同学没有任何朋友(引用计数0),说明他已经离校了,阿姨立即把他的床位清空(回收内存);
- • 标记-清除:解决两个同学互相锁门,都不出门的问题(循环引用),阿姨会挨个宿舍敲门,能回应的(可达对象)就是还在的,没人回应的(不可达对象)就是已经离校的,直接撬门清空床位;
- • 分代回收:阿姨根据同学的住校时间分宿舍,0代是新生(新对象),经常检查是否离校;1代是老生(存活较久的对象),偶尔检查;2代是宿管老员工(核心对象,如内置函数),几乎不检查,这样阿姨不用天天挨个宿舍查,节省时间(提升性能)。
题目8:多个Python装饰器的执行顺序是什么?写代码演示
题目描述
如果一个函数被多个装饰器装饰(如@d1 @d2 def f(): pass),说明装饰器的装饰顺序和执行顺序,用代码验证。
常见错误答案
- 1. 装饰器的装饰顺序和执行顺序一致,从上到下执行;
- 2. 多个装饰器装饰后,调用函数时只执行最外层的装饰器;
正确答案+代码演示
多个装饰器装饰同一个函数,核心规则是:装饰(加载)顺序从下到上,执行顺序从上到下(简记:先装饰的后执行,后装饰的先执行)。
本质:@d1 @d2 def f(): pass 等价于 f = d1(d2(f)),先把原函数f传给d2装饰,得到新函数d2(f),再把d2(f)传给d1装饰,最终得到d1(d2(f)),这是装饰顺序从下到上;调用f()时,先执行d1的wrapper函数,再执行d2的wrapper函数,最后执行原函数f,这是执行顺序从上到下。
# 定义装饰器1
defdecorator1(func):
defwrapper():
print("装饰器1的前置逻辑")
func()
print("装饰器1的后置逻辑")
return wrapper
# 定义装饰器2
defdecorator2(func):
defwrapper():
print("装饰器2的前置逻辑")
func()
print("装饰器2的后置逻辑")
return wrapper
# 多个装饰器装饰:@d1 在上,@d2 在下
@decorator1
@decorator2
deftest():
print("原函数执行")
# 调用被装饰后的函数
if __name__ == "__main__":
test()
# 输出结果(执行顺序从上到下):
# 装饰器1的前置逻辑
# 装饰器2的前置逻辑
# 原函数执行
# 装饰器2的后置逻辑
# 装饰器1的后置逻辑
原理解析
把多个装饰器比作给手机贴膜+装手机壳,原函数是裸机,@decorator2是贴膜,@decorator1是装手机壳。
装饰顺序(从下到上):先给手机贴膜(先装饰@d2),再装手机壳(后装饰@d1),不可能先装壳再贴膜;
执行顺序(从上到下):用手机时,先打开手机壳(先执行@d1的前置逻辑),再揭开贴膜(再执行@d2的前置逻辑),才能用到手机本身(原函数);用完手机后,先贴回贴膜(执行@d2的后置逻辑),再装上手机壳(执行@d1的后置逻辑)。
题目9:Python中的*args和**kwargs是什么?有什么作用?
题目描述
解释*args和**kwargs的含义、区别,说明它们的使用场景,用代码演示。
常见错误答案
- 1.
*args和**kwargs是必须一起使用的,缺一不可; - 2.
*args接收字典参数,**kwargs接收列表参数; - 3.
args和kwargs是Python的关键字,不能改成其他名字。
正确答案+代码演示
*args和**kwargs是Python中用于处理可变参数的语法,不是关键字,只是约定俗成的命名,把args改成params、kwargs改成kwargs_dict也完全可以,核心是前面的*和**。
二者的核心区别:
- •
*args:全称是arguments,用于接收任意数量的位置参数,打包成一个元组传递给函数,参数数量可多可少,也可以没有; - •
**kwargs:全称是keyword arguments,用于接收任意数量的关键字参数(key=value),打包成一个字典传递给函数,参数数量可多可少,也可以没有; - • 使用顺序:如果函数同时有普通参数、
*args、**kwargs,必须按**普通参数 → *args → kwargs的顺序定义,否则会报错。
使用场景:当不确定函数需要接收多少个参数时,用*args和**kwargs,让函数的参数更灵活,比如装饰器、通用工具函数、类的继承重写。
# 定义一个灵活的函数:普通参数 + *args + **kwargs
deffunc(name, *args, **kwargs):
print(f"普通参数:{name}")
print(f"*args接收的位置参数(元组):{args}")
print(f"**kwargs接收的关键字参数(字典):{kwargs}")
# 调用函数:传入不同数量的参数
func("张三") # 无可变参数
# 输出:普通参数:张三 | args:() | kwargs:{}
func("李四", 20, 180) # 传入2个位置参数
# 输出:普通参数:李四 | args:(20, 180) | kwargs:{}
func("王五", 25, 175, city="北京", job="程序员") # 位置+关键字参数
# 输出:普通参数:王五 | args:(25, 175) | kwargs:{'city': '北京', 'job': '程序员'}
# 解包传递:将列表/元组解包给*args,字典解包给**kwargs
lst = [22, 165]
dic = {"city": "上海", "job": "测试"}
func("赵六", *lst, **dic) # 等价于 func("赵六",22,165,city="上海",job="测试")
# 重命名:*args改成*params,**kwargs改成**kw
deffunc2(*params, **kw):
print(params, kw)
func2(1, 2, a=3, b=4) # 输出:(1, 2) {'a': 3, 'b': 4}
原理解析
把函数的参数列表比作一个快递柜,普通参数是固定的专属格口(只能放指定的快递),*args是一排无编号的临时格口(能放任意数量的小件快递,堆成一堆(元组)),**kwargs是一排有编号的临时格口(能放任意数量的大件快递,按编号(key)摆放(字典))。
不管有多少个快递(参数),都能通过这三个区域放下,快递柜(函数)的兼容性拉满,这就是*args和**kwargs的核心作用。
题目10:Python的with语句的核心原理是什么?为什么能自动关闭资源?
题目描述
解释with语句的作用,说明其核心实现原理,用代码实现一个支持with语句的自定义对象。
常见错误答案
- 1. with语句只能用于文件操作,其他场景用不了;
- 2. with语句自动关闭资源是Python的语法糖,没有底层原理;
- 3. 只要对象有
close()方法,就能用with语句。
正确答案+代码演示
with语句的核心作用是简化资源的管理,比如文件、网络连接、数据库连接等,能自动获取资源,在代码块执行完毕后(无论是否发生异常),自动释放/关闭资源,避免因程序员忘记调用close()方法或程序异常导致的资源泄漏。
核心原理:with语句的对象必须实现上下文管理器协议,即对象必须包含__enter__()和__exit__()两个方法,这两个方法共同完成资源的获取和释放:
- 1.
__enter__()方法:进入with代码块时执行,用于获取资源,返回的对象会被赋值给as后的变量(可选); - 2.
__exit__(exc_type, exc_val, exc_tb)方法:退出with代码块时执行(无论是否异常),用于释放资源,参数用于接收异常信息(如果有),返回True可忽略异常,返回False则抛出异常。
文件对象、数据库连接对象等Python内置对象,都已经实现了这两个方法,所以能直接用with语句;自定义对象只要实现这两个方法,也能成为上下文管理器,支持with语句。
# 一、with语句操作文件(内置上下文管理器)
# 无需手动调用f.close(),执行完毕自动关闭
withopen("test.txt", "w", encoding="utf-8") as f:
f.write("Hello Python!")
# 二、自定义一个支持with语句的对象:模拟文件操作
classMyFile:
def__init__(self, filename, mode):
self.filename = filename
self.mode = mode
self.file = None
# 实现__enter__:获取资源(打开文件)
def__enter__(self):
self.file = open(self.filename, self.mode, encoding="utf-8")
print("文件已打开")
returnself.file # 返回文件对象,赋值给as后的变量
# 实现__exit__:释放资源(关闭文件),接收异常参数
def__exit__(self, exc_type, exc_val, exc_tb):
ifself.file:
self.file.close()
print("文件已关闭")
# 打印异常信息(如果有)
if exc_type:
print(f"发生异常:{exc_type}, {exc_val}")
returnFalse# 不忽略异常,抛出给上层
# 测试自定义上下文管理器
with MyFile("my_test.txt", "w") as f:
f.write("自定义上下文管理器!")
# 故意抛出异常,测试是否仍会关闭文件
# 1/0
# 输出:文件已打开 → 文件已关闭(即使打开1/0,也会执行关闭)
原理解析
把with语句比作去酒店开房间,上下文管理器对象是酒店前台,__enter__()方法是前台给房卡(获取资源),__exit__()方法是退房时前台收回房卡(释放资源)。
不管你在房间里住了多久、有没有打碎东西(程序是否异常),只要你离开房间(退出with代码块),前台都会主动收回房卡(释放资源),不会让房卡一直被你占用(资源泄漏),而不用你自己去前台退卡(手动调用close())。
最后:给宝子们的面试建议
刷完这10道题,是不是发现很多之前“会用但说不出”的点,其实底层原理都很简单?面试官考这些题,不是为了为难你,而是想考察你是否真正理解Python的基础,而不是只会搬代码、调API。
给大家3个面试准备的核心建议,亲测有效:
- 1. 打牢基础,拒绝死记硬背:Python的核心基础(装饰器、迭代器、内存管理、面向对象)是面试的重中之重,不要只记用法,要搞懂底层原理,用通俗的话讲出来,面试官更看重你的理解能力;
- 2. 多敲代码验证:遇到不确定的知识点,不要靠猜,直接打开Python解释器敲代码验证,比如多个装饰器的执行顺序、深浅拷贝的差异,代码是最好的答案;
- 3. 总结错题,形成自己的知识体系:把面试中遇到的错题、不懂的点整理下来,按“知识点-错误答案-正确答案-原理-代码示例”的格式记录,面试前反复复盘,比刷100道新题更有效。
Python的面试,从来不是考偏题、怪题,而是考你对基础的掌握程度和编程思维。把这些核心点吃透,不仅能轻松通过面试,在实际工作中也能少踩坑、写出更优雅的代码。
互动时间
宝子们,这10道题你答对了几道?有没有踩过其中的坑?评论区告诉我你的答案~