Skip to content

Python 基础是面试的第一道门槛,涵盖语法特性、数据结构、面向对象、核心 API。看似简单,但细节最能体现基本功。

Q1: Python 的数据类型有哪些?可变和不可变类型的区别? 「🟢 校招/初级」

考察点:考察对 Python 数据类型体系的整体认知,以及对可变/不可变概念的理解深度。筛掉那些只会用而不理解底层内存模型的候选人。

参考答案

Python 的数据类型可以从多个维度分类:

按类别划分:

类别类型示例
数值型int, float, complex, bool42, 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, boollist, 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:实际键值对数组(按插入顺序存储)

这种设计的好处:

  1. 保持插入顺序
  2. 内存更节省(稀疏的索引数组只存 int,密集的 entries 存完整数据)
  3. 迭代更高效(直接遍历 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) 的操作。

字典的查找过程:

  1. 计算 key 的哈希值 hash(key)
  2. 取模得到初始索引位置
  3. 比较该位置的 key 是否匹配(先比较哈希值,再比较值)
  4. 如果匹配,返回对应 value
  5. 如果不匹配,按探测序列找下一个位置,直到找到或遇到空位

追问延伸

  • 什么情况下 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 → 仅限关键字参数 → **kwargs
python
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 必须用关键字传递

常见应用场景:

  1. 装饰器中透传参数
  2. 包装其他函数时保持签名灵活
  3. 接收配置项(大量可选参数)
  4. 函数链式调用时传递参数

追问延伸

  • 仅限位置参数(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):        # 名称修饰的私有方法
        pass

Python 的访问控制:

命名方式含义真实可访问性
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 → object

3. 多态(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.h

3. 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 = name

2. 字符串表示

方法作用调用方式
__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: newinit 的区别? 「🟡 中级」

考察点:考察对 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__ 是在创建时调用的(不是实例),可以控制类的生成过程。

常见误区

  1. "__new__ 是构造函数,__init__ 是初始化函数"——这个说法大致正确但要注意:__new__ 是静态方法,不是传统意义的构造函数。

  2. "可以在 __init__ 中控制创建什么对象"——不行,__init__ 执行时对象已经创建好了,它只能修改已存在对象的属性。

  3. "__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: 什么是 strrepr?有什么区别? 「🟢 校招/初级」

考察点:考察对 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!rlogger.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

执行步骤:

  1. 计算 EXPRESSION,得到上下文管理器对象
  2. 调用对象的 __enter__() 方法
  3. 如果有 as VARIABLE,将 __enter__() 的返回值赋给 VARIABLE
  4. 执行 BLOCK 中的代码
  5. 无论 BLOCK 是否抛出异常,都会调用 __exit__() 方法
  6. __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_tbtraceback 对象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))  # StopIteration

2. 生成器表达式

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, striter(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 2Python 3
print语句:print "hello"函数:print("hello")
默认编码ASCIIUTF-8
字符串str = bytes,unicode = 文本str = 文本(Unicode),bytes = 字节
整数除法3 / 2 = 1(地板除)3 / 2 = 1.5(真除法)
整数类型int + long统一为 int(任意精度)
rangerange 返回列表,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 2Python 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 不向后兼容?这样设计的考量是什么?