python面试
一、 Python基础语法
1. 可变与不可变对象
- 题目:Python中什么是可变对象和不可变对象?请分别举例。
- 考察点:对内存模型的理解。
- 答案:
- 不可变对象:内存内容不可改变,修改变量会指向新的内存地址。如:
int,float,str,tuple。 - 可变对象:内存内容可以改变,修改变量不会改变内存地址。如:
list,dict,set。
- 不可变对象:内存内容不可改变,修改变量会指向新的内存地址。如:
2. 列表与元组的区别
- 题目:
list和tuple的区别是什么?分别用在什么场景? - 考察点:基本数据结构的特性。
- 答案:列表可变(可增删改),元组不可变。元组通常用于存储异构数据(如数据库一条记录),且由于不可变性,可作为字典的键;列表用于存储同质数据,需要频繁增删改的场景。
**3. 参数传递
-
题目:下面代码的输出是什么?为什么?
def func(a, b=[]): b.append(a) return b print(func(1)) print(func(2)) -
考察点:默认参数的陷阱。
-
答案:输出
[1]和[1, 2]。默认参数b=[]只在函数定义时被初始化一次。后续调用如果不传b,使用的都是同一个列表对象。
4. *args 和 **kwargs 的作用
- 题目:在函数定义中,
*args和**kwargs是什么意思? - 考察点:不定长参数的使用。
- 答案:
*args:用于接收多余的位置参数,打包成元组(tuple)。**kwargs:用于接收多余的关键字参数,打包成字典(dict)。
二、 数据结构与算法
**5. 列表去重
- 题目:给定一个列表
lst = [1, 2, 2, 3, 4, 4, 5],如何快速去重并保持原来的顺序? - 考察点:集合的特性和排序。
- 答案:
# 方法一 (Python 3.7+ 字典保持插入顺序) list(dict.fromkeys(lst)) # 方法二 (传统方法) seen = set() result = [x for x in lst if not (x in seen or seen.add(x))]
**6. 字典排序
- 题目:有一个字典
{'A': 3, 'B': 1, 'C': 2},如何根据值(value)进行升序排序? - 考察点:高阶函数
sorted和 lambda 表达式的使用。 - 答案:
sorted(dict.items(), key=lambda item: item[1]) # 结果: [('B', 1), ('C', 2), ('A', 3)]
三、 高级特性
**7. 装饰器:装饰器本质上是一个函数,它接受一个函数作为参数,返回一个新的函数。
- 题目:写一个简单的装饰器,用于计算函数的执行时间。
- 考察点:闭包和装饰器语法。
- 答案:
import time def timer(func): def wrapper(*args, **kwargs): start = time.time() result = func(*args, **kwargs) print(f"耗时: {time.time() - start}秒") return result return wrapper @timer def test(): time.sleep(1)
**8. 生成器 vs 迭代器
迭代器:
惰性计算:迭代器一次只生成一个元素,不一次性生成所有元素
只能前进:迭代器只能向前移动,不能后退
节省内存
包括_iter_ 和_next_ 方法
- 题目:解释什么是生成器?它和迭代器有什么关系?
yield关键字的作用是什么? - 考察点:惰性求值、内存优化。
- 答案:生成器是迭代器的一种。函数中使用了
yield即成为生成器。它允许函数在每次yield时暂停并记住状态,下次从该位置继续执行,用于节省内存(处理大数据流)和表示无限序列。
**9. 深拷贝与浅拷贝
- 题目:在Python中,
import copy; copy.copy(x)和copy.deepcopy(x)有什么区别? - 考察点:对象引用的理解。
- 答案:
- 浅拷贝:拷贝父对象,但不会拷贝父对象内部的子对象,新旧对象共享子对象。
- 深拷贝:完全拷贝父对象及其子对象,新旧对象完全独立。
你想聚焦在Python中理解深拷贝和浅拷贝的区别,我会用Python的代码和新手能看懂的逻辑,把这个知识点讲透——核心还是「是否复制嵌套的引用类型」。
一、Python中浅拷贝(Shallow Copy)
浅拷贝只会复制对象的「表层结构」,对于对象内部的嵌套元素(比如列表里的列表、字典里的列表),依然共享同一个内存地址,修改嵌套元素会影响原对象。
1. 浅拷贝的常见实现方式
- 列表:
list.copy()、切片arr[:] - 字典:
dict.copy() - 通用:
copy模块的copy.copy()
2. 代码示例(最典型的嵌套列表)
import copy
# 原对象:外层列表 + 嵌套列表(引用类型)
original = [1, 2, [3, 4]]
# 方式1:列表自带的浅拷贝
shallow1 = original.copy()
# 方式2:切片浅拷贝
shallow2 = original[:]
# 方式3:copy模块的浅拷贝
shallow3 = copy.copy(original)
# 测试1:修改「表层基础类型」→ 原对象不受影响
shallow1[0] = 100
print("原对象:", original) # [1, 2, [3, 4]]
print("浅拷贝1:", shallow1) # [100, 2, [3, 4]]
# 测试2:修改「嵌套的引用类型」→ 原对象跟着变(核心坑点)
shallow2[2].append(5)
print("原对象:", original) # [1, 2, [3, 4, 5]]
print("浅拷贝2:", shallow2) # [1, 2, [3, 4, 5]]
二、Python中深拷贝(Deep Copy)
深拷贝会递归复制对象的「所有层级」,包括嵌套的引用类型,拷贝后的对象和原对象完全独立,修改任何层级的元素都不会影响原对象。
1. 深拷贝的唯一核心方式
必须用 copy 模块的 copy.deepcopy()(Python没有内置的深拷贝语法糖)。
2. 代码示例
import copy
original = [1, 2, [3, 4]]
# 深拷贝:递归复制所有层级
deep = copy.deepcopy(original)
# 测试:修改嵌套元素 → 原对象完全不受影响
deep[2].append(5)
print("原对象:", original) # [1, 2, [3, 4]]
print("深拷贝:", deep) # [1, 2, [3, 4, 5]]
三、Python特有的避坑点(新手必看)
- 只有「可变对象」才需要区分深浅拷贝
- 不可变对象(int、str、tuple):因为无法修改内部值,拷贝本质都是引用传递,深浅拷贝无差异。
a = (1, 2) # 元组是不可变对象 b = copy.copy(a) c = copy.deepcopy(a) print(a is b) # True → 同一个对象 print(a is c) # True → 同一个对象 - 深拷贝的性能成本
深拷贝需要递归遍历所有层级,对于嵌套极深、数据量大的对象(比如多层嵌套的大字典),速度会比浅拷贝慢很多,非必要不滥用。 - 特殊对象的深拷贝限制
对于文件句柄、网络连接、类实例的方法等「不可序列化对象」,deepcopy可能无法完全拷贝,或拷贝后失效,需额外处理。
总结
- 浅拷贝(
copy.copy()/列表copy/切片):只复制表层,嵌套引用类型共享,修改嵌套元素会影响原对象,速度快、占用内存少。 - 深拷贝(
copy.deepcopy()):递归复制所有层级,完全独立,修改任何元素都不影响原对象,速度慢、占用内存多。 - Python关键:只有列表、字典等可变对象的嵌套结构,才需要区分深浅拷贝;不可变对象(int/str/tuple)无差异。
四、 并发编程
**10. GIL是什么?
- 题目:解释Python中的GIL(全局解释器锁)。它对多线程程序有什么影响?
- 考察点:对Python核心机制的理解。
- 答案:GIL是CPython解释器中的一个互斥锁,它确保任何时候只有一个线程执行Python字节码。这使得CPU密集型的多线程程序无法真正利用多核优势;但对于I/O密集型程序,由于线程经常等待I/O(此时会释放GIL),多线程依然能显著提升效率。
五、 编程实践
**11. 合并两个字典
- 题目:在Python 3.5+ 中,如何简洁地合并两个字典
dict1和dict2? - 考察点:Python版本特性。
- 答案:
merged = {**dict1, **dict2} # Python 3.9+ 还可以使用: dict1 | dict2
**12. 反转字符串
- 题目:给定一个字符串 “hello world”,请将其反转。
- 考察点:切片技巧。
- 答案:
s = "hello world" reversed_s = s[::-1] # 结果: "dlrow olleh"
**13. 异常处理
- 题目:
try-except-else-finally中,else和finally分别在什么情况下执行? - 考察点:异常处理流程。
- 答案:
else:当try块中没有抛出异常时执行。finally:无论是否发生异常,都会执行(通常用于清理资源,如关闭文件)。
**14. 上下文管理器
- 题目:如何使用
with语句打开文件?相比于直接open()和close()有什么好处? - 考察点:资源管理。
- 答案:
好处:即使文件操作过程中发生异常,with open('file.txt', 'r') as f: content = f.read()with语句也能确保文件被正确关闭,避免资源泄露。
六、 面向对象
**15. __init__ 和 __new__ 的区别
- 题目:解释
__new__和__init__在类实例化过程中的作用。 - 考察点:对象创建与初始化的底层机制。
- 答案:
__new__:是一个静态方法,负责创建实例(分配内存),返回实例对象。它先执行。__init__:是一个实例方法,负责初始化实例(赋值属性),修改__new__创建的对象。它后执行。
你想搞清楚Python中__init__和__new__的核心区别,这是理解Python类实例化过程的关键——简单说,__new__负责创建对象(造空壳),__init__负责初始化对象(填内容),这是新手最容易混淆的两个魔术方法。
一、核心区别(先记结论,再看代码)
| 特性 | __new__(cls, *args, **kwargs) |
__init__(self, *args, **kwargs) |
|---|---|---|
| 作用 | 创建类的实例(返回一个空对象),是「构造方法」 | 初始化实例的属性(给空对象赋值),是「初始化方法」 |
| 第一个参数 | cls(类本身) |
self(__new__创建好的实例) |
| 返回值 | 必须返回一个实例(通常是super().__new__(cls)) |
无返回值(默认None,写return会报错) |
| 调用时机 | 实例创建时最先调用(比__init__早) |
__new__返回实例后才调用(给实例赋值) |
| 适用场景 | 单例模式、修改不可变对象(str/tuple/int)的创建逻辑 | 给实例初始化属性(99%的场景只用这个) |
二、代码示例(新手能直接跑)
1. 基础示例:看调用顺序和参数
class Person:
# __new__:创建实例,第一个参数是类本身
def __new__(cls, name, age):
print(f"__new__被调用:创建{cls}类的实例")
# 调用父类的__new__创建空实例(必须返回,否则__init__不会执行)
instance = super().__new__(cls)
return instance # 返回创建好的空实例
# __init__:初始化实例,第一个参数是__new__返回的实例
def __init__(self, name, age):
print(f"__init__被调用:初始化{self}实例的属性")
self.name = name # 给实例赋值
self.age = age
# 创建实例:先执行__new__,再执行__init__
p = Person("张三", 18)
print(p.name, p.age) # 张三 18
# 输出结果:
# __new__被调用:创建<class '__main__.Person'>类的实例
# __init__被调用:初始化<__main__.Person object at 0x10xxx>实例的属性
# 张三 18
2. 关键场景1:__new__不返回实例,__init__不会执行
class Test:
def __new__(cls):
print("__new__执行,但不返回实例")
# 故意不返回实例
return None
def __init__(self):
print("__init__执行") # 永远不会打印
t = Test() # 只输出:__new__执行,但不返回实例
print(t) # None(因为__new__返回None)
3. 关键场景2:单例模式(__new__的经典用法)
单例模式要求一个类只能创建一个实例,核心就是重写__new__控制实例创建:
class Singleton:
# 用类属性保存唯一实例
_instance = None
def __new__(cls):
# 如果实例不存在,才创建;否则返回已有的实例
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
# 测试:两次创建的是同一个实例
s1 = Singleton()
s2 = Singleton()
print(s1 is s2) # True(地址相同,是同一个对象)
4. 关键场景3:不可变对象的修改(__init__做不到)
str、tuple是不可变对象,初始化后无法修改,要定制创建逻辑只能用__new__:
class CustomStr(str):
# 定制字符串:创建时自动转大写
def __new__(cls, value):
# 先处理值,再创建实例
new_value = value.upper()
return super().__new__(cls, new_value)
# 测试:创建时自动转大写
s = CustomStr("hello")
print(s) # HELLO
三、新手必记的核心要点
- 99%的场景只用
__init__:日常写类,只需要重写__init__初始化属性,__new__由Python默认处理,不用管。 __new__是类方法,__init__是实例方法:__new__接收cls(类),__init__接收self(实例)。__new__决定是否创建实例,__init__只负责初始化:如果__new__返回None,__init__永远不会执行。- 单例模式是
__new__最常用的场景:这是面试高频考点,一定要掌握上面的单例代码。
总结
__new__是「构造方法」,负责创建实例(返回空对象),参数是cls,适用于单例、定制不可变对象;__init__是「初始化方法」,负责填充实例属性(无返回值),参数是self,日常开发99%的场景只用它;- 调用顺序:先执行
__new__创建实例,返回实例后才执行__init__初始化。
七、 陷阱与细节
**16. == 与 is 的区别
- 题目:
a == b和a is b有什么区别? - 考察点:值比较与引用比较。
- 答案:
==:比较两个对象的值是否相等。is:比较两个对象的id(内存地址)是否相同,即是否是同一个对象。
**17. 负索引与切片
- 题目:对于一个列表
a = [0, 1, 2, 3, 4, 5],a[-1]、a[-2:]和a[::-1]分别输出什么? - 考察点:Python灵活的索引机制。
- 答案:
a[-1]->5a[-2:]->[4, 5]a[::-1]->[5, 4, 3, 2, 1, 0]
**18. 类变量与实例变量
-
题目:写出下面代码的输出:
class Person: tags = ["person"] # 类变量 p1 = Person() p2 = Person() p1.tags.append("student") print(p2.tags) -
考察点:类属性与实例属性的交互。
-
答案:输出
['person', 'student']。因为tags是类变量,所有实例共享同一个列表。通过p1.tags修改会影响p2.tags。如果赋值p1.tags = ["new"],则p1会新建一个实例属性,不再影响类变量。
更多推荐


所有评论(0)