Appearance
Python 基础是面试的第一道门槛,涵盖语法特性、数据结构、面向对象、核心 API。看似简单,但细节最能体现基本功。
Q1: Python 的数据类型有哪些?可变和不可变类型的区别? 「🟢 校招/初级」
考察点:考察对 Python 数据类型体系的整体认知,以及对可变/不可变概念的理解深度。筛掉那些只会用而不理解底层内存模型的候选人。
参考答案:
Python 的数据类型可以从多个维度分类:
按类别划分:
| 类别 | 类型 | 示例 |
|---|---|---|
| 数值型 | int, float, complex, bool | 42, 3.14, 1+2j, True |
| 序列型 | str, list, tuple, range, bytes | "hello", [1,2], (1,2), range(10) |
| 映射型 | dict | {"a": 1} |
| 集合型 | set, frozenset | {1,2,3}, frozenset([1,2]) |
| 其他 | NoneType, 函数, 类, 模块 | None |
可变 vs 不可变的核心区别:
| 特性 | 不可变类型 | 可变类型 |
|---|---|---|
| 代表类型 | int, float, str, tuple, frozenset, bool | list, dict, set, bytearray |
| 内存地址 | 修改值会创建新对象(id 变化) | 原地修改(id 不变) |
| 可哈希 | 是(可作 dict 的 key) | 否(不能作 dict 的 key) |
| 线程安全 | 天然线程安全 | 需要额外同步机制 |
| 性能 | 访问快,复用(小整数/字符串驻留) | 增删改灵活 |
python
# 不可变:修改会创建新对象
a = "hello"
print(id(a)) # 140123...
a += " world"
print(id(a)) # 140456... 新对象
# 可变:原地修改
lst = [1, 2, 3]
print(id(lst)) # 140789...
lst.append(4)
print(id(lst)) # 140789... 同一个对象重要注意点:tuple 的"不可变"是指引用不可变
python
t = (1, [2, 3])
t[1].append(4) # 合法!tuple 内部的 list 仍然可变
print(t) # (1, [2, 3, 4])追问延伸:
- Python 中
is和==的区别是什么?(is比较 id,==比较值) - 小整数驻留机制了解吗?(-5 到 256 的整数在 CPython 中被缓存复用)
- 字符串驻留(intern)机制?(相同字面量的字符串共享同一个对象)
Q2: 列表和元组的区别?各有什么应用场景? 「🟢 校招/初级」
考察点:考察对 Python 最常用的两种序列类型的理解,以及在实际开发中选型判断的能力。筛掉只会用 list 不知道 tuple 的初级开发者。
参考答案:
核心区别对比表:
| 特性 | list(列表) | tuple(元组) |
|---|---|---|
| 可变性 | 可变(mutable) | 不可变(immutable) |
| 语法 | [1, 2, 3] | (1, 2, 3) 或 1, 2, 3 |
| 方法 | append、pop、insert、sort 等丰富方法 | count、index 极少方法 |
| 内存占用 | 较大(预留扩容空间) | 较小(固定大小) |
| 可哈希 | 否 | 是(元素都不可变时) |
| 可作 dict key | 否 | 是 |
| 创建速度 | 较慢 | 较快 |
性能对比:
python
import sys
lst = [1, 2, 3, 4, 5]
tup = (1, 2, 3, 4, 5)
print(sys.getsizeof(lst)) # 120 字节(更多,因为有 over-allocation)
print(sys.getsizeof(tup)) # 80 字节(紧凑存储)
# 从字节码看 tuple 构造更简单
# 列表需要 BUILD_LIST,元组直接 BUILD_TUPLE元组的独特能力:元组拆包(Unpacking)
python
# 基本拆包
a, b, c = (1, 2, 3)
# 交换变量(Pythonic 写法)
a, b = b, a
# _ 忽略不关心的值
name, _, age = ("Alice", "female", 25)
# * 收集剩余元素
first, *rest, last = (1, 2, 3, 4, 5)
print(first, rest, last) # 1 [2, 3, 4] 5
# 函数返回多个值(本质是返回 tuple)
def min_max(lst):
return min(lst), max(lst) # 自动打包成 tuple应用场景:
| 使用 list 的场景 | 使用 tuple 的场景 |
|---|---|
| 需要动态增删改元素 | 固定数据结构(如坐标、RGB 颜色) |
| 同类型元素集合 | 不同类型元素的组合(结构化数据) |
| 需要排序、反转等操作 | 作为 dict 的 key 或 set 的元素 |
| 函数内部临时数据处理 | 函数返回多个值 |
| 可变的有序序列 | 保护数据不被意外修改 |
追问延伸:
- 为什么 tuple 比 list 快?(编译器优化、无需扩容、内存紧凑)
namedtuple了解吗?和普通 tuple、dataclass 有什么区别?- 列表的扩容机制是什么?(原容量的 1.125 倍左右,over-allocation)
- Python 3.10+ 的 match/case 模式匹配和元组有什么关系?
Q3: 字典的底层实现?哈希冲突怎么处理? 「🟡 中级」
考察点:考察对 Python 核心数据结构底层原理的理解深度,哈希表是数据结构基础,也是高频面试题。筛掉只会调 API 不懂原理的开发者。
参考答案:
底层数据结构:哈希表(Hash Table)
Python 字典本质是一张哈希表,通过哈希函数将 key 映射到数组索引,实现 O(1) 的平均查找复杂度。
Python 3.7+ 的重要特性:保持插入顺序
Python 3.6 开始内部实现改为"分离式哈希表"(split table),3.7 正式成为语言规范:
dk_indices:哈希索引数组(紧凑,存的是 entries 数组的下标)dk_entries:实际键值对数组(按插入顺序存储)
这种设计的好处:
- 保持插入顺序
- 内存更节省(稀疏的索引数组只存 int,密集的 entries 存完整数据)
- 迭代更高效(直接遍历 entries 数组)
哈希冲突的处理:开放寻址法(Open Addressing)
Python 字典使用开放寻址法而非拉链法来解决哈希冲突。具体采用的是伪随机探测 + 二次探测的混合策略。
python
# 简化的探测序列示意(实际更复杂)
def probe_sequence(hash_value, table_size):
perturb = hash_value
j = hash_value % table_size
while True:
yield j
j = (5 * j + 1 + perturb) % table_size
perturb >>= 5 # 右移,逐渐混入更多哈希位为什么不用拉链法?
- 链表节点需要额外内存开销
- 缓存局部性差(链表节点分散在堆内存)
- Python 的对象本身就很大,开放寻址更适合小对象场景
扩容机制:
| 负载因子 | 扩容倍数 | 触发条件 |
|---|---|---|
| 2/3 (约 0.667) | 小表(<50k)扩容4倍,大表(≥50k)扩容2倍 | 已用槽位 / 总槽位 > 2/3 |
扩容时会重新计算所有 key 的哈希位置(rehash),这是一个 O(n) 的操作。
字典的查找过程:
- 计算 key 的哈希值
hash(key) - 取模得到初始索引位置
- 比较该位置的 key 是否匹配(先比较哈希值,再比较值)
- 如果匹配,返回对应 value
- 如果不匹配,按探测序列找下一个位置,直到找到或遇到空位
追问延伸:
- 什么情况下 dict 的查找会退化为 O(n)?(所有 key 哈希冲突时极端情况)
- Python 3.7+ 字典有序,那 OrderedDict 还有什么用?(move_to_end、popitem(last=True/False) 等方法)
- set 的底层实现和 dict 有什么关系?(set 就是只有 key 没有 value 的 dict)
- 为什么 dict 的 key 必须是可哈希的?
__hash__和__eq__的关系是什么?(相等的对象必须有相同的哈希值)
Q4: 列表推导式、字典推导式、集合推导式?生成器表达式? 「🟢 校招/初级」
考察点:考察对 Python 简洁语法特性的掌握程度,体现 Pythonic 编码风格。筛掉写 Python 却像写 C/Java 的开发者。
参考答案:
列表推导式(List Comprehension)
python
# 基本语法:[表达式 for 变量 in 可迭代对象 if 条件]
squares = [x ** 2 for x in range(10)]
# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
# 带条件筛选
even_squares = [x ** 2 for x in range(10) if x % 2 == 0]
# [0, 4, 16, 36, 64]
# 多层循环
pairs = [(x, y) for x in [1, 2, 3] for y in [3, 1, 4] if x != y]
# [(1, 3), (1, 4), (2, 3), (2, 1), (2, 4), (3, 1), (3, 4)]字典推导式(Dict Comprehension)
python
# 基本语法:{key_expr: value_expr for 变量 in 可迭代对象 if 条件}
square_dict = {x: x ** 2 for x in range(5)}
# {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}
# 字典键值反转
reversed_dict = {v: k for k, v in square_dict.items()}
# 带条件
filtered = {k: v for k, v in square_dict.items() if v > 5}
# {3: 9, 4: 16}集合推导式(Set Comprehension)
python
# 基本语法:{表达式 for 变量 in 可迭代对象 if 条件}
unique_lengths = {len(word) for word in ["hello", "world", "python", "code"]}
# {4, 5, 6}
# 去重并做变换
chars = {c.lower() for c in "HelloWorld"}
# {'h', 'e', 'l', 'o', 'w', 'r', 'd'}生成器表达式(Generator Expression)
python
# 用圆括号,惰性计算
gen = (x ** 2 for x in range(10))
print(type(gen)) # <class 'generator'>
# 不占用完整内存,逐个生成
for val in gen:
print(val) # 每次迭代计算一个值
# 常用于函数参数(可以省略一层括号)
sum(x ** 2 for x in range(100)) # 直接传生成器
max(x for x in [3, 1, 4, 1, 5, 9])推导式 vs 普通 for 循环 vs map/filter:
| 特性 | 推导式 | for 循环 | map/filter |
|---|---|---|---|
| 可读性 | 高(简洁直观) | 低(啰嗦) | 中(函数式风格) |
| 性能 | 略好(底层优化) | 稍差 | 接近推导式 |
| 灵活性 | 中(适合简单变换) | 高(复杂逻辑) | 低(只适合单参数函数) |
| 副作用 | 不推荐(纯函数式) | 可以 | 不推荐 |
性能对比(100 万次迭代):
python
# 列表推导式最快(字节码层面优化)
# map 次之(C 层面循环)
# for 循环最慢(Python 层面逐行解释)最佳实践:
- 推导式最多嵌套 2 层,超过就用普通 for 循环
- 推导式内部不要有复杂逻辑,保持简洁
- 大数据量用生成器表达式,节省内存
- 不要为了"一行代码"牺牲可读性
追问延伸:
- 推导式中的变量作用域?(Python 3 中推导式有自己的作用域,不会污染外部)
- 生成器表达式和列表推导式在内存上有多大差异?(数据量越大差异越明显)
- 什么情况下用 map/filter 比推导式更好?(已有现成函数时,如
map(str, lst)) - 生成器只能遍历一次吗?为什么?(是的,因为有状态,遍历完就耗尽了)
Q5: Python 中的 *args 和 **kwargs 是什么? 「🟢 校招/初级」
考察点:考察对 Python 函数参数机制的理解,这是编写灵活函数和阅读框架源码的基础。筛掉不了解可变参数机制的开发者。
参考答案:
*args:可变位置参数
*args 将多余的位置参数收集为一个元组(tuple)。
python
def func(a, b, *args):
print(f"a={a}, b={b}")
print(f"args={args}") # tuple
func(1, 2, 3, 4, 5)
# a=1, b=2
# args=(3, 4, 5)**kwargs:可变关键字参数
**kwargs 将多余的关键字参数收集为一个字典(dict)。
python
def func(a, b, **kwargs):
print(f"a={a}, b={b}")
print(f"kwargs={kwargs}") # dict
func(1, b=2, c=3, d=4)
# a=1, b=2
# kwargs={'c': 3, 'd': 4}组合使用(标准写法):
python
def func(*args, **kwargs):
print(f"位置参数: {args}")
print(f"关键字参数: {kwargs}")
func(1, 2, 3, x=4, y=5)
# 位置参数: (1, 2, 3)
# 关键字参数: {'x': 4, 'y': 5}解包操作(Unpacking):
python
# * 解包可迭代对象(list, tuple, str 等)
def add(a, b, c):
return a + b + c
nums = [1, 2, 3]
print(add(*nums)) # 等价于 add(1, 2, 3) = 6
# ** 解包字典
params = {"a": 1, "b": 2, "c": 3}
print(add(**params)) # 等价于 add(a=1, b=2, c=3) = 6
# 列表中解包(Python 3.5+)
merged = [*[1, 2], *[3, 4]] # [1, 2, 3, 4]
# 字典中解包(Python 3.5+)
merged_dict = {**{"a": 1}, **{"b": 2}} # {'a': 1, 'b': 2}完整的参数顺序:
位置参数 → *args → 仅限关键字参数 → **kwargspython
def example(pos1, pos2, *args, kw_only1, kw_only2=None, **kwargs):
pass
# pos1, pos2: 普通位置参数
# *args: 收集多余位置参数
# kw_only1, kw_only2: 仅限关键字参数(keyword-only)
# **kwargs: 收集多余关键字参数仅限关键字参数(Keyword-Only Arguments):
python
def func(a, b, *, c, d=None):
# c 和 d 只能通过关键字传递
pass
func(1, 2, c=3) # 正确
func(1, 2, 3) # 报错!c 必须用关键字传递常见应用场景:
- 装饰器中透传参数
- 包装其他函数时保持签名灵活
- 接收配置项(大量可选参数)
- 函数链式调用时传递参数
追问延伸:
- 仅限位置参数(Positional-Only Parameters)了解吗?(Python 3.8+ 的
/语法) *和**解包在函数调用、列表、字典中分别有什么限制?- 为什么
print(*objects, sep=' ', end='\n')可以接收任意多个参数? - 装饰器中为什么常用
*args, **kwargs?
Q6: Python 的函数参数传递机制?是值传递还是引用传递? 「🟡 中级」
考察点:考察对 Python 内存模型和函数调用机制的深度理解。这是一道经典"陷阱题",能筛掉那些从 Java/C++ 转过来但没真正理解 Python 的人。
参考答案:
Python 是"传对象引用"(Call by Object Reference / Call by Sharing)
既不是纯值传递,也不是纯引用传递,而是传递对象的引用(内存地址)。
核心规则:
- 函数参数接收的是对象的引用(不是对象的副本)
- 但这个引用是按值传递的(传的是引用的副本)
- 所以:函数内对参数重新赋值不会影响外部,但修改对象内部状态会影响外部
不可变对象的表现(看起来像值传递):
python
def change_num(n):
n = 100 # 重新绑定局部变量 n 到新对象
print("函数内:", n)
x = 10
change_num(x)
print("函数外:", x) # 还是 10,不受影响原因:int 是不可变类型,n = 100 不是修改原对象,而是创建新对象并让局部变量 n 指向它,不影响外部的 x。
可变对象的表现(看起来像引用传递):
python
def change_list(lst):
lst.append(4) # 修改对象内部状态
print("函数内:", lst)
my_list = [1, 2, 3]
change_list(my_list)
print("函数外:", my_list) # [1, 2, 3, 4],被修改了!原因:list 是可变类型,lst.append(4) 直接修改原对象的内部状态,而 lst 和外部的 my_list 指向同一个对象。
关键区分:重新赋值 vs 修改内部状态
python
def func1(lst):
lst = [4, 5, 6] # 重新赋值 → 不影响外部
def func2(lst):
lst[:] = [4, 5, 6] # 切片赋值 → 修改原对象 → 影响外部
def func3(lst):
lst.append(4) # 原地修改 → 影响外部对比总结表:
| 操作 | 不可变对象(int/str/tuple) | 可变对象(list/dict/set) |
|---|---|---|
重新赋值(x = new_val) | 不影响外部 | 不影响外部 |
修改内部状态(.append()等) | 不可能(对象不可变) | 影响外部 |
| 整体表现 | 类似值传递 | 类似引用传递 |
为什么这样设计?
- Python 一切皆对象,变量是对象的"标签"(引用)
- 传递引用避免了大对象的拷贝开销
- 不可变对象保证了数据安全性(不会被意外修改)
- 可变对象提供了灵活性
追问延伸:
- 如何在函数内安全地修改传入的 list 而不影响外部?(传入副本:
func(lst.copy())) - Python 的变量和 C/Java 的变量有什么本质区别?(Python 变量是引用/标签,C 变量是内存空间)
id()函数返回的是什么?它和is运算符有什么关系?- 函数参数默认值为什么不能用可变对象?(经典陷阱:
def func(lst=[]))
Q7: Python 的面向对象三大特性? 「🟢 校招/初级」
考察点:考察面向对象基础概念在 Python 中的具体体现。筛掉对 OOP 概念模糊、只会写过程式代码的初级开发者。
参考答案:
面向对象三大特性:封装、继承、多态。
1. 封装(Encapsulation)
封装是将数据和操作数据的方法绑定在一起,隐藏内部实现细节,只暴露必要的接口。
python
class Person:
def __init__(self, name, age):
self.name = name # 公开属性
self._age = age # 约定的"受保护"属性
self.__password = "123" # 名称修饰(Name Mangling)
def get_age(self): # 公开方法
return self._age
def _validate(self): # 约定的"受保护"方法
pass
def __encrypt(self): # 名称修饰的私有方法
passPython 的访问控制:
| 命名方式 | 含义 | 真实可访问性 |
|---|---|---|
name | 公开(public) | 完全可访问 |
_name | 约定的受保护(protected) | 仍可直接访问,靠约定 |
__name | 名称修饰(private-like) | 被重命名为 _类名__name,仍可绕过 |
Python 没有真正的私有!"我们都是成年人"(We are all consenting adults here)——靠约定而非强制。
2. 继承(Inheritance)
继承让子类获得父类的属性和方法,实现代码复用和层次抽象。
python
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
raise NotImplementedError
class Dog(Animal): # 单继承
def speak(self):
return f"{self.name} says Woof!"
class Cat(Animal):
def speak(self):
return f"{self.name} says Meow!"
# super() 调用父类方法
class Puppy(Dog):
def __init__(self, name, age):
super().__init__(name) # 调用父类初始化
self.age = age多继承与 MRO:
python
class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass # 多继承
print(D.__mro__) # 方法解析顺序
# D → B → C → A → object3. 多态(Polymorphism)
多态指不同类的对象对同一消息做出不同响应。Python 通过鸭子类型实现多态。
python
def make_sound(animal):
# 不关心 animal 是什么类型,只关心它有没有 speak 方法
print(animal.speak())
make_sound(Dog("Buddy")) # Buddy says Woof!
make_sound(Cat("Mimi")) # Mimi says Meow!
# 甚至可以传入完全不相关的对象,只要有 speak 方法
class Robot:
def speak(self):
return "Beep boop!"
make_sound(Robot()) # Beep boop! —— 这就是鸭子类型三大特性总结:
| 特性 | 目的 | Python 中的实现 |
|---|---|---|
| 封装 | 隐藏细节、保护数据 | 属性+方法、_约定、__名称修饰、property |
| 继承 | 代码复用、层次化 | class 子类(父类)、多继承、MRO、super() |
| 多态 | 统一接口、灵活扩展 | 鸭子类型(duck typing)、Protocol |
追问延伸:
- 什么是鸭子类型?和 Java 的接口有什么区别?
__名称修饰(Name Mangling)的规则是什么?- 为什么说 Python 没有真正的私有属性?
super()的本质是什么?它一定调用父类吗?(不一定,调用的是 MRO 中的下一个类)- 组合(Composition)和继承的区别?什么时候优先用组合?
Q8: 什么是鸭子类型?Python 是怎么实现多态的? 「🟡 中级」
考察点:考察对 Python 动态类型系统核心思想的理解,以及对多态本质的认识。筛掉只会背诵"封装继承多态"但不理解动态语言多态实现方式的开发者。
参考答案:
鸭子类型(Duck Typing)
"如果它走路像鸭子,叫起来像鸭子,那么它就是鸭子。"
鸭子类型是动态类型语言的一种类型判断方式:不关心对象的具体类型,只关心对象是否具有所需的方法或属性。
核心思想:关注行为,而非类型
python
class Duck:
def quack(self):
print("嘎嘎嘎")
def walk(self):
print("鸭子走路")
class Person:
def quack(self):
print("人模仿鸭子叫")
def walk(self):
print("人走路")
def in_the_forest(duck_like):
# 不检查类型,直接调用方法——只要有 quack 和 walk 就行
duck_like.quack()
duck_like.walk()
in_the_forest(Duck()) # 没问题
in_the_forest(Person()) # 也没问题——人也能"像鸭子"与静态语言多态的对比:
| 特性 | Python(鸭子类型) | Java(接口/继承) |
|---|---|---|
| 类型检查时机 | 运行时 | 编译时 |
| 多态实现 | 隐式(有方法即可) | 显式(implements/extends) |
| 耦合度 | 低(松耦合) | 高(必须有继承/接口关系) |
| 灵活性 | 高 | 低 |
| 安全性 | 较低(运行时才发现错误) | 高(编译时检查) |
Python 多态的多种实现方式
1. 鸭子类型(最 Pythonic)
python
# 只要实现了 __len__,就能用 len()
class MyCollection:
def __len__(self):
return 42
len(MyCollection()) # 42 —— 不需要继承任何类2. 继承多态(传统方式)
python
from abc import ABC, abstractmethod
class Shape(ABC):
@abstractmethod
def area(self):
pass
class Circle(Shape):
def __init__(self, r):
self.r = r
def area(self):
return 3.14 * self.r ** 2
class Rectangle(Shape):
def __init__(self, w, h):
self.w = w
self.h = h
def area(self):
return self.w * self.h3. Protocol(结构化子类型,Python 3.8+)
python
from typing import Protocol
class Quackable(Protocol):
def quack(self) -> None: ...
# 不需要显式继承,只要有 quack 方法就符合协议
class Duck:
def quack(self) -> None:
print("嘎嘎")
def make_sound(q: Quackable) -> None:
q.quack()
make_sound(Duck()) # 静态类型检查通过4. 运算符重载(特殊形式的多态)
python
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def __add__(self, other): # + 运算符多态
return Vector(self.x + other.x, self.y + other.y)
v1 = Vector(1, 2)
v2 = Vector(3, 4)
v3 = v1 + v2 # 调用 __add__鸭子类型的优缺点
| 优点 | 缺点 |
|---|---|
| 代码更灵活、更简洁 | 运行时才发现 AttributeError |
| 松耦合,易于扩展 | IDE 补全和静态检查困难 |
| 符合"开闭原则"(对扩展开放) | 文档不齐全时难以理解接口 |
| 不需要定义接口/抽象类 | 重构风险较高 |
如何弥补缺点?
- 使用类型提示(Type Hints)+ Protocol
- 使用
hasattr()或getattr()做防御性检查 - 使用抽象基类(ABC)显式约束接口
- 完善文档和单元测试
追问延伸:
- ABC(抽象基类)和鸭子类型有什么关系?是互补还是替代?
isinstance()检查的是什么?它和鸭子类型冲突吗?- Python 的协议(Protocol)和 Go 的接口很像,了解吗?
- 什么是"猴子补丁"?它和鸭子类型有什么关系?
- 举一个 Python 标准库中鸭子类型的例子?(文件类对象、迭代器协议、上下文管理器协议)
Q9: Python 的魔术方法(dunder method)有哪些常见的? 「🟢 校招/初级」
考察点:考察对 Python 面向对象核心特性的掌握广度。魔术方法是 Python 语言的精髓,能体现开发者对 Python 的熟悉程度。筛掉只会写普通类的初级开发者。
参考答案:
魔术方法(Magic Method / Dunder Method)是 Python 中以双下划线开头和结尾的特殊方法,它们让自定义类能够像内置类型一样使用运算符和内置函数。
分类速览
1. 构造与生命周期
| 方法 | 作用 | 调用时机 |
|---|---|---|
__new__(cls, ...) | 创建并返回实例 | __init__ 之前,静态方法 |
__init__(self, ...) | 初始化实例属性 | 创建实例时自动调用 |
__del__(self) | 析构方法 | 对象被垃圾回收时 |
python
class MyClass:
def __new__(cls, name):
print("创建实例")
return super().__new__(cls)
def __init__(self, name):
print("初始化实例")
self.name = name2. 字符串表示
| 方法 | 作用 | 调用方式 |
|---|---|---|
__str__(self) | 用户友好的字符串表示 | str(obj)、print(obj) |
__repr__(self) | 开发者友好的明确表示 | repr(obj)、交互式环境 |
__format__(self, fmt) | 格式化字符串 | f"{obj:fmt}"、format() |
python
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __repr__(self):
return f"Point({self.x}, {self.y})" # 尽量能 eval 重建
def __str__(self):
return f"({self.x}, {self.y})"3. 比较运算
| 方法 | 运算符 | 说明 |
|---|---|---|
__eq__(self, other) | == | 相等 |
__ne__(self, other) | != | 不等(默认委托给 __eq__ 取反) |
__lt__(self, other) | < | 小于 |
__le__(self, other) | <= | 小于等于 |
__gt__(self, other) | > | 大于 |
__ge__(self, other) | >= | 大于等于 |
__hash__(self) | — | 哈希值(和 __eq__ 配套使用) |
__bool__(self) | bool() | 布尔值 |
使用
@functools.total_ordering装饰器可以只需定义__eq__和一个比较方法,自动补全其余比较。
4. 容器/序列协议
| 方法 | 作用 | 调用方式 |
|---|---|---|
__len__(self) | 长度 | len(obj) |
__getitem__(self, key) | 获取元素 | obj[key] |
__setitem__(self, key, value) | 设置元素 | obj[key] = value |
__delitem__(self, key) | 删除元素 | del obj[key] |
__contains__(self, item) | 包含判断 | item in obj |
__iter__(self) | 返回迭代器 | iter(obj)、for 循环 |
__next__(self) | 迭代下一个 | next(obj) |
5. 算术运算
| 方法 | 运算符 | 说明 |
|---|---|---|
__add__(self, other) | + | 加法 |
__sub__(self, other) | - | 减法 |
__mul__(self, other) | * | 乘法 |
__truediv__(self, other) | / | 真除法 |
__floordiv__(self, other) | // | 地板除 |
__mod__(self, other) | % | 取模 |
__pow__(self, other) | ** | 幂运算 |
__iadd__(self, other) | += | 原地加法 |
__radd__(self, other) | — | 反向加法(左操作数不支持时) |
6. 上下文管理器
| 方法 | 作用 |
|---|---|
__enter__(self) | 进入 with 块时调用,返回值赋给 as 变量 |
__exit__(self, exc_type, exc_val, exc_tb) | 退出 with 块时调用(无论是否异常) |
7. 可调用对象
| 方法 | 作用 | 调用方式 |
|---|---|---|
__call__(self, *args, **kwargs) | 让对象可调用 | obj(args) |
python
class Adder:
def __init__(self, n):
self.n = n
def __call__(self, x):
return x + self.n
add5 = Adder(5)
print(add5(10)) # 15 —— 对象像函数一样被调用8. 属性访问控制
| 方法 | 作用 |
|---|---|
__getattr__(self, name) | 属性不存在时调用 |
__getattribute__(self, name) | 所有属性访问都经过这里 |
__setattr__(self, name, value) | 设置属性时调用 |
__delattr__(self, name) | 删除属性时调用 |
__dir__(self) | dir() 列出属性时调用 |
9. 描述符协议
| 方法 | 作用 |
|---|---|
__get__(self, instance, owner) | 读取属性时调用 |
__set__(self, instance, value) | 设置属性时调用 |
__delete__(self, instance) | 删除属性时调用 |
追问延伸:
__getattr__和__getattribute__有什么区别?(前者只在找不到属性时调用,后者总是调用)- 为什么重写了
__eq__就要重写__hash__?(相等的对象必须有相同哈希,否则 dict/set 行为异常) - 如何让自定义类支持
with语句?(实现__enter__和__exit__) - 描述符是什么?property 和描述符有什么关系?
__call__和装饰器有什么关系?(类装饰器就是通过__call__实现的)
Q10: new 和 init 的区别? 「🟡 中级」
考察点:考察对 Python 对象创建过程的深入理解。这是理解元类、单例模式等高级特性的基础。筛掉对实例化过程一知半解的开发者。
参考答案:
核心区别
| 特性 | __new__ | __init__ |
|---|---|---|
| 类型 | 静态方法(特殊的,不需要 @staticmethod) | 实例方法 |
| 作用 | 创建并返回实例对象(分配内存) | 初始化实例属性 |
| 参数 | 第一个参数是 cls(类本身) | 第一个参数是 self(已创建的实例) |
| 执行顺序 | 先执行 | __new__ 返回实例后才执行 |
| 返回值 | 必须返回一个实例(通常是 cls 的实例) | 不需要返回值(返回 None) |
| 常见用途 | 单例、不可变类修改、元类控制 | 设置属性、初始化状态 |
对象创建的完整流程
python
class Person:
def __new__(cls, name):
print(f"1. __new__ 被调用,cls = {cls}")
instance = super().__new__(cls) # 调用父类 object.__new__ 创建实例
print(f"2. 实例已创建,id = {id(instance)}")
return instance # 返回实例后,才会调用 __init__
def __init__(self, name):
print(f"3. __init__ 被调用,self = {self}")
self.name = name # 给实例设置属性
p = Person("Alice")
# 输出:
# 1. __new__ 被调用,cls = <class '__main__.Person'>
# 2. 实例已创建,id = 140...
# 3. __init__ 被调用,self = <__main__.Person object at 0x...>new 的典型应用场景
1. 单例模式
python
class Singleton:
_instance = None
def __new__(cls, *args, **kwargs):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
a = Singleton()
b = Singleton()
print(a is b) # True —— 同一个实例2. 修改不可变类(继承 int/str/tuple 等)
python
class PositiveInt(int):
def __new__(cls, value):
if value < 0:
raise ValueError("必须是正数")
return super().__new__(cls, value) # int 是不可变的,必须在 __new__ 中处理
n = PositiveInt(5)
print(n) # 5
PositiveInt(-3) # ValueError: 必须是正数为什么必须在
__new__中处理?因为int是不可变类型,一旦创建就不能修改,所以必须在创建之前(__new__阶段)做验证。
3. 元类中控制类的创建
元类的 __new__ 是在类创建时调用的(不是实例),可以控制类的生成过程。
常见误区
"
__new__是构造函数,__init__是初始化函数"——这个说法大致正确但要注意:__new__是静态方法,不是传统意义的构造函数。"可以在
__init__中控制创建什么对象"——不行,__init__执行时对象已经创建好了,它只能修改已存在对象的属性。"
__new__必须返回cls的实例"——一般是的,但也可以返回其他类的实例(这种情况下__init__不会被调用)。
python
class A:
def __new__(cls):
return B() # 返回 B 的实例
class B:
def __init__(self):
print("B.__init__ 被调用")
a = A() # 会调用 B.__init__,A.__init__ 不会被调用
print(type(a)) # <class '__main__.B'>追问延伸:
- 元类的
__new__和普通类的__new__有什么区别?(元类创建的是类对象) - 单例模式除了
__new__还有哪些实现方式?(模块导入、装饰器、元类) __del__析构方法什么时候调用?有什么坑?(循环引用导致不调用、异常被忽略)__init_subclass__了解吗?(Python 3.6+,类被继承时调用,是元类的轻量替代品)
Q11: 什么是 str 和 repr?有什么区别? 「🟢 校招/初级」
考察点:考察对 Python 对象字符串表示的理解,体现代码规范和调试能力。筛掉不知道如何让类"打印出来好看"的初级开发者。
参考答案:
核心区别
| 特性 | __str__ | __repr__ |
|---|---|---|
| 目标受众 | 用户(User) | 开发者(Developer) |
| 目的 | 友好、可读的描述 | 明确、无歧义的表示 |
| 调用方式 | str(obj)、print(obj)、f"{obj}" | repr(obj)、交互式环境、!r 格式化 |
| 设计原则 | 简洁好看 | 尽量能 eval() 重建对象 |
| 优先级 | 用户友好优先 | 精确明确优先 |
python
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
def __str__(self):
return f"{self.name}, {self.age}岁"
def __repr__(self):
return f"Person(name='{self.name}', age={self.age})"
p = Person("Alice", 25)
print(str(p)) # Alice, 25岁 —— 给用户看的
print(repr(p)) # Person(name='Alice', age=25) —— 给开发者看的
# 在交互式环境中直接输入 p,显示的是 __repr__ 的结果
# >>> p
# Person(name='Alice', age=25)两者的关系
- 如果只定义了
__repr__,__str__默认会调用__repr__( fallback 机制) - 如果只定义了
__str__,__repr__还是默认的<类名 object at 地址> - 最佳实践:至少定义
__repr__,因为它是更基础的那个
python
# 只有 __repr__ 时,__str__ 也能用
class A:
def __repr__(self):
return "A()"
a = A()
print(str(a)) # A() —— 自动 fallback 到 __repr__
print(repr(a)) # A()内置类型的例子
python
# 字符串的 __str__ 和 __repr__ 区别最明显
s = "hello\nworld"
print(str(s)) # hello
# world
print(repr(s)) # 'hello\nworld' —— 保留转义字符,明确表示这是字符串
# 列表的 str 和 repr 一样(因为列表元素用 repr 表示)
lst = [1, 2, "a"]
print(str(lst)) # [1, 2, 'a']
print(repr(lst)) # [1, 2, 'a']设计原则
__repr__ 的黄金法则:
eval(repr(obj)) == obj(尽量做到)
也就是说,__repr__ 返回的字符串应该尽可能描述"如何重建这个对象"。
实际应用:
- 调试日志用
%r或!r:logger.debug("处理用户: %r", user) - 错误信息用
repr更精确 - 用户界面展示用
str
python
# f-string 中的格式化
p = Person("Alice", 25)
print(f"{p}") # Alice, 25岁 —— 调 __str__
print(f"{p!r}") # Person(...) —— 调 __repr__
print(f"{p!s}") # Alice, 25岁 —— 调 __str__(显式)追问延伸:
__format__方法了解吗?它和__str__有什么关系?- 为什么
print(列表)显示的是repr格式而不是str格式? dataclass自动生成的__repr__长什么样?- 调试时用
str还是repr更好?为什么?
Q12: 什么是上下文管理器?with语句的原理? 「🟡 中级」
考察点:考察对 Python 资源管理机制的理解,以及对协议(Protocol)思想的掌握。筛掉不会用 with 或只知其然不知其所以然的开发者。
参考答案:
什么是上下文管理器
上下文管理器是实现了上下文管理协议的对象,即实现了 __enter__ 和 __exit__ 两个方法。它用于资源的自动获取和释放,确保资源被正确清理。
with 语句的执行流程
python
with EXPRESSION as VARIABLE:
BLOCK执行步骤:
- 计算
EXPRESSION,得到上下文管理器对象 - 调用对象的
__enter__()方法 - 如果有
as VARIABLE,将__enter__()的返回值赋给VARIABLE - 执行
BLOCK中的代码 - 无论 BLOCK 是否抛出异常,都会调用
__exit__()方法 __exit__()返回True表示抑制异常,返回False表示传播异常
python
class FileManager:
def __init__(self, filename, mode):
self.filename = filename
self.mode = mode
self.file = None
def __enter__(self):
print("打开文件")
self.file = open(self.filename, self.mode)
return self.file # 返回值赋给 as 后面的变量
def __exit__(self, exc_type, exc_val, exc_tb):
print("关闭文件")
if self.file:
self.file.close()
# 返回 True 会抑制异常,默认返回 None(等价于 False)
return False # 不抑制异常,正常抛出
with FileManager("test.txt", "w") as f:
f.write("hello")
# 输出:
# 打开文件
# hello
# 关闭文件(即使中间报错也会执行)exit 的三个参数
| 参数 | 含义 | 正常退出时 |
|---|---|---|
exc_type | 异常类型 | None |
exc_val | 异常实例(异常值) | None |
exc_tb | traceback 对象 | None |
python
class SuppressError:
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is ValueError:
print(f"捕获到 ValueError: {exc_val}")
return True # 抑制异常,不再向外抛出
return False # 其他异常正常抛出
with SuppressError():
raise ValueError("测试")
# 捕获到 ValueError: 测试 —— 异常被抑制,程序继续contextlib:更简单的实现方式
使用 @contextlib.contextmanager 装饰器 + 生成器,可以更简洁地实现上下文管理器:
python
from contextlib import contextmanager
@contextmanager
def file_manager(filename, mode):
# __enter__ 部分
print("打开文件")
f = open(filename, mode)
try:
yield f # yield 之前是 __enter__,yield 的值赋给 as 变量
# yield 之后是 __exit__(正常退出时执行)
finally:
# finally 确保无论是否异常都执行
print("关闭文件")
f.close()
with file_manager("test.txt", "w") as f:
f.write("hello")原理:contextmanager 装饰器将生成器包装成上下文管理器:
__enter__:启动生成器,执行到yield,返回yield的值__exit__:如果有异常,用throw()注入异常到生成器;否则继续执行生成器剩余代码
常见应用场景
| 场景 | 示例 |
|---|---|
| 文件操作 | with open(...) as f: |
| 数据库连接 | with conn.cursor() as cursor: |
| 线程锁 | with lock: |
| 临时文件 | with tempfile.NamedTemporaryFile() as f: |
| 重定向输出 | with redirect_stdout(f): |
| 计时器 | with Timer(): |
| 事务管理 | with transaction: |
contextlib 其他实用工具
python
from contextlib import closing, suppress, redirect_stdout
import io
# closing:将有 close() 方法的对象变成上下文管理器
with closing(open("file.txt")) as f:
pass
# suppress:忽略指定异常
with suppress(FileNotFoundError):
os.remove("nonexistent.txt")
# redirect_stdout:重定向标准输出
f = io.StringIO()
with redirect_stdout(f):
print("hello")
print(f.getvalue()) # hello\n追问延伸:
- 上下文管理器和
try/finally有什么关系?(本质是try/finally的语法糖) - 什么是异步上下文管理器?(
__aenter__、__aexit__,配合async with) contextlib.ExitStack了解吗?有什么用?(动态管理多个上下文管理器)- 为什么说上下文管理器体现了 Python 的"协议"思想?
- 你在项目中自定义过上下文管理器吗?用来解决什么问题?
Q13: Python中的可迭代对象、迭代器、生成器有什么区别和联系? 「🟡 中级」
考察点:考察对 Python 迭代协议的深入理解,这是理解 for 循环、生成器、协程等核心概念的基础。筛掉只会写 for 循环但不懂迭代器原理的开发者。
参考答案:
概念定义
可迭代对象(Iterable):实现了 __iter__() 方法的对象,可以被 for 循环遍历。
- 例子:list、tuple、dict、str、set、range、文件对象
- 判断:
isinstance(obj, Iterable)或 能用iter()转换
迭代器(Iterator):实现了 __iter__() 和 __next__() 方法的对象,是一个有状态的"指针",记住遍历位置。
- 例子:
iter([1,2,3])、文件对象本身 - 特点:一次性(只能遍历一次)、惰性(按需计算)
生成器(Generator):特殊的迭代器,用 yield 关键字或生成器表达式创建。
- 例子:
(x for x in range(10))、包含yield的函数 - 特点:自动实现迭代器协议、惰性计算、节省内存
关系图
生成器 (Generator)
⊂ 迭代器 (Iterator)
⊂ 可迭代对象 (Iterable)迭代协议详解
python
# 可迭代对象:有 __iter__
class MyIterable:
def __iter__(self):
return MyIterator()
# 迭代器:有 __iter__ 和 __next__
class MyIterator:
def __init__(self):
self.n = 0
def __iter__(self): # 迭代器的 __iter__ 返回自身
return self
def __next__(self):
if self.n >= 3:
raise StopIteration
self.n += 1
return self.n
# for 循环的本质
for x in MyIterable():
print(x)
# 等价于:
it = iter(MyIterable()) # 调用 __iter__ 得到迭代器
while True:
try:
x = next(it) # 调用 __next__ 取下一个值
print(x)
except StopIteration:
break生成器的两种创建方式
1. 生成器函数(yield)
python
def countdown(n):
while n > 0:
yield n
n -= 1
gen = countdown(3)
print(type(gen)) # <class 'generator'>
print(next(gen)) # 3
print(next(gen)) # 2
print(next(gen)) # 1
print(next(gen)) # StopIteration2. 生成器表达式
python
gen = (x ** 2 for x in range(1000000)) # 不占用大量内存
lst = [x ** 2 for x in range(1000000)] # 占用大量内存
print(type(gen)) # <class 'generator'>
print(type(lst)) # <class 'list'>三者对比表
| 特性 | 可迭代对象 | 迭代器 | 生成器 |
|---|---|---|---|
| 实现方法 | __iter__ | __iter__ + __next__ | 自动实现(yield/生成器表达式) |
| 状态 | 无状态(每次迭代独立) | 有状态(记住位置) | 有状态(记住位置和局部变量) |
| 遍历次数 | 多次 | 一次 | 一次 |
| 内存占用 | 完整数据在内存 | 较少(只存状态) | 最少(惰性计算) |
支持 next() | 否(需先 iter()) | 是 | 是 |
| 例子 | list, dict, str | iter(lst), 文件对象 | 生成器函数/表达式 |
生成器的高级方法
| 方法 | 作用 |
|---|---|
next(gen) | 获取下一个值 |
gen.send(value) | 向生成器发送值(恢复执行时 yield 表达式的值) |
gen.throw(exc) | 向生成器抛出异常 |
gen.close() | 关闭生成器 |
python
def echo():
while True:
received = yield # yield 可以接收 send 的值
print(f"收到: {received}")
gen = echo()
next(gen) # 启动生成器,执行到 yield
gen.send("hi") # 收到: hi
gen.send(123) # 收到: 123
gen.close()应用场景
什么时候用生成器?
- 大数据量处理(如读大文件、流式数据)
- 无限序列(斐波那契数列、计数器)
- 管道(Pipeline)模式,数据层层处理
- 协程的基础(async/await 底层就是生成器)
追问延伸:
yield from了解吗?它解决了什么问题?(委托给子生成器,简化生成器嵌套)- 迭代器和可迭代对象为什么都要有
__iter__?(迭代器的__iter__返回自身,这样迭代器也能被 for 循环使用) - 为什么生成器只能遍历一次?(有状态,遍历完状态就到末尾了)
- 什么是惰性求值(Lazy Evaluation)?有什么好处?
itertools模块了解多少?常用的有哪些?
Q14: Python中的深拷贝和浅拷贝? 「🟢 校招/初级」
考察点:考察对 Python 对象引用模型的理解,这是很多 bug 的根源。筛掉对"引用"概念模糊的初级开发者。
参考答案:
三种赋值/拷贝方式
python
import copy
a = [1, 2, [3, 4]]
# 1. 赋值(=):引用同一个对象
b = a
print(a is b) # True —— 完全是同一个对象
# 2. 浅拷贝:复制顶层对象,内部元素还是引用
c = a.copy() # 或 a[:] 或 list(a) 或 copy.copy(a)
print(a is c) # False —— 顶层是新对象
print(a[2] is c[2]) # True —— 内部元素还是同一个引用
# 3. 深拷贝:递归复制所有层级
d = copy.deepcopy(a)
print(a is d) # False
print(a[2] is d[2]) # False —— 内部元素也是新对象效果对比图
赋值 (=):
a → [1, 2, [3, 4]]
b ↗
浅拷贝 (copy):
a → [1, 2, ref]
↓
c → [1, 2, ref] → [3, 4] (共享内部对象)
深拷贝 (deepcopy):
a → [1, 2, ref] → [3, 4]
d → [1, 2, ref] → [3, 4] (完全独立)修改时的表现差异
python
a = [1, 2, [3, 4]]
b = a
c = a.copy()
d = copy.deepcopy(a)
# 修改顶层元素
a[0] = 100
print(b[0]) # 100 —— 赋值:同步变
print(c[0]) # 1 —— 浅拷贝:顶层独立,不变
print(d[0]) # 1 —— 深拷贝:完全独立,不变
# 修改内部嵌套元素
a[2].append(5)
print(b[2]) # [3, 4, 5] —— 赋值:同步变
print(c[2]) # [3, 4, 5] —— 浅拷贝:内部共享,也变了!
print(d[2]) # [3, 4] —— 深拷贝:完全独立,不变对比总结表
| 方式 | 顶层对象 | 嵌套对象 | 内存占用 | 速度 | 典型用法 |
|---|---|---|---|---|---|
赋值 = | 同一个 | 同一个 | 0 额外 | 最快 | 别名引用 |
浅拷贝 copy | 新的 | 共享引用 | 较少 | 较快 | 元素都是不可变时 |
深拷贝 deepcopy | 新的 | 新的 | 较多 | 较慢 | 需要完全独立副本时 |
浅拷贝的多种实现方式
python
lst = [1, 2, 3]
# 列表浅拷贝
lst.copy()
lst[:]
list(lst)
copy.copy(lst)
# 字典浅拷贝
d = {"a": 1}
d.copy()
dict(d)
copy.copy(d)
# 集合浅拷贝
s = {1, 2, 3}
s.copy()
set(s)
copy.copy(s)特殊情况:不可变对象
对于不可变对象(int、str、tuple 等),浅拷贝和深拷贝没有实质区别,因为对象本身不能修改,共享引用也不会有问题。
python
import copy
a = (1, 2, 3)
b = copy.copy(a)
c = copy.deepcopy(a)
print(a is b) # True —— 不可变对象,copy 直接返回原对象
print(a is c) # True —— deepcopy 也直接返回原对象但注意:如果 tuple 内部有可变对象,deepcopy 还是会递归复制内部的可变对象。
deepcopy 的特殊能力
1. 处理循环引用
python
a = [1, 2]
a.append(a) # 循环引用:a[2] 是 a 自己
print(a) # [1, 2, [...]]
b = copy.deepcopy(a) # 能正确处理,不会无限递归2. 处理自定义对象
python
class Person:
def __init__(self, name, friends=None):
self.name = name
self.friends = friends or []
p1 = Person("Alice")
p2 = copy.deepcopy(p1)
print(p1 is p2) # False
print(p1.friends is p2.friends) # False —— 递归复制追问延伸:
- 什么是"引用计数"?它和拷贝有什么关系?
- 自定义类如何控制拷贝行为?(实现
__copy__、__deepcopy__方法) - 函数参数传递是浅拷贝还是深拷贝?(都不是,是传引用)
- 切片操作
lst[:]是浅拷贝还是深拷贝?(浅拷贝) - 什么情况下浅拷贝就够用了?(列表元素都是不可变类型时)
Q15: Python 2 和 Python 3 的主要区别? 「🟢 校招/初级」
考察点:考察对 Python 语言演进的了解,也侧面反映开发者的从业时间和学习能力。筛掉还停留在 Python 2 思维模式的开发者。
参考答案:
Python 3 是 Python 的重大升级版本,不向后兼容。Python 2 已于 2020 年 1 月 1 日停止维护。
核心区别对比表
| 特性 | Python 2 | Python 3 |
|---|---|---|
语句:print "hello" | 函数:print("hello") | |
| 默认编码 | ASCII | UTF-8 |
| 字符串 | str = bytes,unicode = 文本 | str = 文本(Unicode),bytes = 字节 |
| 整数除法 | 3 / 2 = 1(地板除) | 3 / 2 = 1.5(真除法) |
| 整数类型 | int + long | 统一为 int(任意精度) |
| range | range 返回列表,xrange 惰性 | 只有 range(惰性,类似 xrange) |
| 异常捕获 | except ValueError, e: | except ValueError as e: |
| 异常链 | 不支持 | 支持 raise ... from ... |
| 类型提示 | 不支持 | 支持(Python 3.5+) |
| f-string | 不支持 | 支持(Python 3.6+) |
| 海象运算符 | 不支持 | 支持 :=(Python 3.8+) |
| 装饰器 | 只能在函数/类定义前 | 同左,语法更灵活 |
详细解读
1. print:语句 → 函数
python
# Python 2
print "hello"
print "hello", "world" # 空格分隔
print >> sys.stderr, "err"
# Python 3
print("hello")
print("hello", "world", sep=" ") # 可选参数
print("err", file=sys.stderr)
print("no newline", end="")2. 字符串和编码
这是 Python 3 最大的改进之一,彻底解决了编码混乱问题。
python
# Python 2: str 是字节串,unicode 才是文本
s = "你好" # <type 'str'> —— 字节,取决于源文件编码
u = u"你好" # <type 'unicode'> —— 文本
# Python 3: str 是 Unicode 文本,bytes 是字节
s = "你好" # <class 'str'> —— Unicode 文本
b = b"hello" # <class 'bytes'> —— 字节3. 除法
python
# Python 2
3 / 2 # 1 —— 整数除法(地板除)
3.0 / 2 # 1.5
3 // 2 # 1 —— 地板除(Python 2.2+ 引入)
# Python 3
3 / 2 # 1.5 —— 真除法
3 // 2 # 1 —— 地板除4. range vs xrange
python
# Python 2
range(5) # [0, 1, 2, 3, 4] —— 立即生成列表,占内存
xrange(5) # xrange(5) —— 惰性生成,省内存
# Python 3
range(5) # range(0, 5) —— 惰性,类似 Python 2 的 xrange
# xrange 被移除了5. 迭代器而非列表
Python 3 中很多内置函数返回迭代器而非列表,更节省内存:
| 函数 | Python 2 | Python 3 |
|---|---|---|
map() | 返回 list | 返回 iterator |
filter() | 返回 list | 返回 iterator |
zip() | 返回 list | 返回 iterator |
dict.keys() | 返回 list | 返回 dict_keys 视图 |
dict.values() | 返回 list | 返回 dict_values 视图 |
dict.items() | 返回 list | 返回 dict_items 视图 |
6. 其他重要变化
python
# 类型提示 (Python 3.5+)
def greet(name: str) -> str:
return f"Hello, {name}"
# f-string (Python 3.6+)
name = "Alice"
print(f"Hello, {name}") # Hello, Alice
# 海象运算符 (Python 3.8+)
if (n := len("hello")) > 3:
print(f"长度为 {n},大于3")
# 仅限位置参数 (Python 3.8+)
def f(a, b, /, c, d, *, e, f):
# a, b 只能位置传参
# c, d 可以位置或关键字
# e, f 只能关键字传参
pass
# match-case 模式匹配 (Python 3.10+)
match status:
case 200:
print("OK")
case 404:
print("Not Found")
case _:
print("Other")追问延伸:
- 你实际项目中用的是 Python 2 还是 3?做过 2to3 迁移吗?
- Python 3 中为什么要区分 str 和 bytes?有什么好处?
__future__模块了解吗?有什么用?(在 Python 2 中导入 Python 3 的特性)- Python 3.10 / 3.11 / 3.12 有哪些新特性你比较喜欢?
- 为什么 Python 3 不向后兼容?这样设计的考量是什么?