面试题精讲:Python 语言特性

1. Python 的参数传递机制

面试核心: Python 既不是“值传递”,也不是“引用传递”,而是“传对象引用”(Pass-by-object-reference)

核心结论

  • 不可变对象 (Immutable):如 int, str, tuple。在函数内修改它们,实际是创建了新对象,原变量指向的地址不变。
  • 可变对象 (Mutable):如 list, dict, set。在函数内修改它们,是直接在原内存地址上进行操作。

代码剖析

def modify(x, y):
    x = 2          # x 指向了新对象 2,原外部变量无感知
    y.append(1)    # y 在原地址操作,外部变量同步变化

a = 1
b = []
modify(a, b)
print(a, b)  # 输出: 1 [1]

道满笔记: 记住“类型属于对象,变量只是标签”。函数执行时会复制一份标签。如果标签贴到了不可变对象上,你撕掉它贴到新对象上,原标签还在原地;如果标签贴在可变对象(如箱子)上,你往箱子里放东西,原标签看这个箱子时内容自然变了。


2. 元类 (Metaclass)

面试核心: 类是创建实例的模板,而元类是创建类的模板

  • 本质: 在 Python 中,一切皆对象,类也是对象。元类(默认是 type)就是用来产生这些“类对象”的。
  • 应用场景: 通常用于框架开发(如 Django ORM、状态机检查)。它能在类创建的瞬间(即定义时)自动改变类的行为。
  • 语法: 通过 metaclass=MyMeta 指定。

3. 实例方法、类方法与静态方法

Python 的方法体系非常灵活,理解它们的绑定关系是关键。

方法类型装饰器第一个参数绑定对象调用方式
实例方法self实例对象obj.method()
类方法@classmethodcls类本身Cls.method()obj.method()
静态方法@staticmethod无绑定Cls.method()obj.method()

核心区别:

  1. 实例方法:最常用,用于访问或修改实例的状态。
  2. 类方法:常用于工厂模式。例如从文件、JSON 等不同来源创建类实例,因为它能通过 cls 调用构造函数。
  3. 静态方法:逻辑上属于这个类(比如工具函数),但不需要访问类或实例的任何属性。

4. 类变量 vs 实例变量

这是面试中最容易掉坑的地方,涉及 Python 的属性查找顺序(MRO)

场景一:不可变对象的“覆盖”

class Person:
    name = "default"  # 类变量

p1 = Person()
p1.name = "Jack"      # 此时 p1 并没有修改类变量,而是在自己身上创建了一个同名实例变量
print(Person.name)    # "default" (未变)
print(p1.name)        # "Jack" (覆盖查找)

场景二:可变对象的“污染”

class Container:
    items = []        # 类变量(列表是可变的)

c1, c2 = Container(), Container()
c1.items.append(1)    # 直接操作了类变量指向的内存
print(c2.items)       # [1] (受牵连)

避坑指南:

  • 原则: 除非你明确想让所有实例共享数据(如计数器),否则永远在 __init__ 中初始化实例变量
  • 理解: 访问属性时,Python 先找实例字典 __dict__,找不到再去类字典里找。

补充:Python 的垃圾回收 (GC)

  1. 引用计数 (Reference Counting):主要机制。计数归零即销毁。
  2. 标记-清除 (Mark and Sweep):解决循环引用问题。
  3. 分代回收 (Generational Collection):基于“存活越久的对象越不容易销毁”的假设,分为 0、1、2 三代,提高回收效率。

这部分内容是 Python 开发者的“进阶分水岭”。我为你进行了重排和深度润色,修正了原稿中部分过时的 Python 2 术语(如 type 显示方式),并引入了更现代的 Python 3.6+ 标准(如 f-string 的深度应用)。


5. Python 自省 (Introspection)

核心考点: 运行时“识破”对象身份的能力。

自省是指程序在运行时能够获知对象类型、属性和方法的一种机制。

  • 常用工具:
    • type(): 获取对象类型。
    • dir(): 列出对象的所有属性和方法。
    • getattr() / hasattr(): 动态获取或判断属性。
    • isinstance(obj, cls): (面试推荐) 判断对象是否属于某个类(考虑继承关系)。
class MyClass: pass
obj = MyClass()

print(type(obj))            # <class '__main__.MyClass'>
print(isinstance(obj, object)) # True (Python 3 中所有类都继承自 object)

6. 列表与字典推导式 (Comprehensions)

核心考点: 简洁高效的集合构造方式。

  • 列表推导式: [x for x in range(5)]
  • 字典推导式 (Py 2.7+):
    # 快速交换 key 和 value
    old_dict = {'a': 1, 'b': 2}
    new_dict = {v: k for k, v in old_dict.items()}

7. 下划线的命名奥秘

核心考点: 约定 vs 强制规则。

  • _foo (单前缀)一种约定。暗示该变量是“私有的”,不希望外部直接调用。from module import * 时不会被导入。
  • __foo (双前缀)名称修饰 (Name Mangling)。解释器会将名字改为 _ClassName__foo,以防止子类覆盖父类属性。这不是真正的私有,但增加了直接访问的难度。
  • __foo__ (双前后缀)魔术方法 (Magic Methods)。Python 内部定义的特殊用途方法,如 __init____call__。开发者应避免自定义此类命名。

8. 字符串格式化:从 % 到 f-string

核心考点: 语法演进与可读性。

  1. % 占位符:古老但稳健。缺点是处理元组参数时容易报错(需手动包裹成单元素元组)。
  2. .format():功能强大,支持位置和关键字参数,比 % 更安全、美观。
  3. f-string (Py 3.6+)当前首选
    • 速度最快:它是运行时解析而非函数调用。
    • 可读性最高:直接在 {} 内写表达式。
name, score = "道满", 98.5
print(f"{name} 的得分是 {score:.2f}") # 支持格式化控制

9. 迭代器 (Iterator) 与 生成器 (Generator)

核心考点: 内存优化与延迟计算。

  • 生成器 (Generator):一种特殊的迭代器,通过 yield 关键字定义。
  • 核心区别
    • 列表:一次性把所有数据塞进内存(容易撑爆内存)。
    • 生成器“边循环边计算”。它只记住当前位置,调用 next() 才计算下一个值。
  • 语法点
    • [] 得到列表。
    • () 得到生成器表达式 (Generator Expression)。

10. *args**kwargs (参数解包)

核心考点: 函数定义的灵活性。

  • *args (Arguments):接收任意数量的位置参数,存为元组(tuple)。
  • **kwargs (Keyword Arguments):接收任意数量的关键字参数,存为字典(dict)。

进阶用法:解包 (Unpacking)

def add(a, b, c): return a + b + c

data = [1, 2, 3]
print(add(*data))  # 将列表解包为 1, 2, 3 传给函数

注意顺序: 在函数定义中,参数顺序必须是:(普通参数, *args, 默认参数, **kwargs)


这部分内容直击 Python 的底层灵魂和高阶面试点。特别是 GIL协程MRO,是区分中高级开发者的关键。我为你进行了逻辑梳理和术语规范,并补充了 Python 3 的最新背景。


11. 面向切面编程 (AOP) 与 装饰器

面试核心: 装饰器是 AOP 在 Python 中的实现方式。

  • 什么是 AOP? AOP 旨在将与业务逻辑无关的功能(如日志、权限、事务)从业务代码中抽离出来。
  • 装饰器的本质: 它是一个闭包,接收一个函数并返回一个增强版的函数。它让代码满足“开闭原则”,即:不修改原函数代码,却能动态添加功能。

12. 鸭子类型 (Duck Typing)

面试核心: 关注“能做什么”,而不是“是什么”。

  • 定义: 如果一个对象走起来像鸭子,叫起来也像鸭子,那它就是鸭子。
  • Python 实践: 你不需要对象继承自某个特定接口(如 Readable),只要它实现了 read() 方法,就可以被当做文件处理。
  • 优势: 极大地提高了代码的灵活性和解耦度。

13. 为什么 Python 不需要函数重载?

面试核心: 动态语言的特性天然覆盖了重载的需求。

在 Java 等强类型语言中,重载是为了处理“不同参数类型”和“不同参数个数”。

  1. 参数类型: Python 是动态类型,函数本就能接收任何类型,无需重载。
  2. 参数个数: Python 通过 缺省参数(默认参数)可变参数 (*args, **kwargs) 完美解决。

14. 新式类与旧式类 (MRO 问题)

面试核心: 菱形继承下的查找顺序。

  • 现状: Python 3 只有新式类(默认继承自 object)。
  • 旧式类(深度优先): 遇到菱形继承时,会一路向上找到顶层,可能导致中间层被重写的属性被忽略。
  • 新式类(C3 算法 / 广度优先): 采用 MRO(Method Resolution Order)确保继承链中的每个类只被访问一次,且顺序科学。
    • 注:可以使用 ClassName.mro() 查看查找顺序。

15. __new__ vs __init__

面试核心: “诞生”与“装修”的区别。

  1. __new__:负责创建实例,是一个类方法(第一个参数是 cls),必须返回一个实例。
  2. __init__:负责初始化实例,是一个实例方法(第一个参数是 self),不返回任何值。
  • 关系: 先有 __new__ 生产出的毛坯房,才有 __init__ 进去搞装修。

16. 单例模式 (Singleton) 的实现

面试核心: 考察编码功底,务必熟练掌握 __new__装饰器 实现。

  1. __new__ 实现:通过类变量保存唯一实例。
  2. 装饰器实现:通过闭包字典存储类与实例的映射。
  3. 模块导入(最推荐):Python 模块在第一次导入时会生成 .pyc,之后直接从内存读取,天然单例。

17. 作用域 (LEGB 规则)

面试核心: 变量查找的层级关系。

  1. L (Local):函数内部。
  2. E (Enclosing):外部嵌套函数(闭包常见)。
  3. G (Global):模块全局。
  4. B (Built-in):内建作用域(如 len, str)。

18. GIL 全局解释器锁

面试核心: 为什么 Python 多线程不能利用多核?

  • 本质: 为了线程安全,CPython 确保同一时刻只有一个线程在执行字节码。
  • 影响:
    • IO 密集型:线程在等待 IO 时会释放锁,所以多线程有效
    • CPU 密集型:因为锁的存在,多线程无法并行,反而因为切换损耗变
  • 对策: 使用 multiprocessing(多进程)或异步编程。

19. 协程 (Coroutine)

面试核心: 用户态的轻量级线程。

  • 演进: yield (Py2) -> @asyncio.coroutine (Py3.4) -> async/await (Py3.5+)。
  • 特点: * 由开发者通过代码手动控制切换,不经过内核。
    • 单线程内实现并发,极大地减少了 CPU 上下文切换的开销。
  • 结论: 是解决高并发 IO 问题的终极方案。

20. 闭包 (Closure)

面试核心: 延伸了变量的作用域,让函数拥有“记忆”。

  • 定义: 当一个内嵌函数引用了外部函数的变量,且外部函数返回这个内嵌函数时,就形成了闭包。
  • 关键点: 即使外部函数已经执行完毕,被引用的外部变量依然存活在内存中(存储在内嵌函数的 __closure__ 属性中)。
  • 应用: 装饰器、延迟计算、封装私有变量。

21. Lambda 匿名函数

面试核心: 快速定义单行函数。

  • 语法: lambda 参数: 表达式
  • 特点: 没有名字,只能包含一个表达式(不能有 returnif-else 块等复杂语句)。
  • 场景: 通常作为参数传递给 mapfiltersorted 等高阶函数。

22. 函数式编程 (map, filter, reduce)

面试核心: 声明式地处理集合。

  • filter(func, seq):保留使 func 返回 True 的元素。
  • map(func, seq):对每个元素应用 func
  • reduce(func, seq)注意: 在 Python 3 中已移至 functools 模块。它对序列进行累积计算(如求和、求阶乘)。

道满建议: 现代 Python 推荐优先使用 列表推导式,因为它比 mapfilter 更具读性且通常更快。


23. 深拷贝与浅拷贝

面试核心: 嵌套对象的内存指向问题。

  • 赋值 (b = a):只是增加了一个指向原对象的“标签”,两者完全等价。
  • 浅拷贝 (copy.copy):创建一个新对象,但对象内部的子对象仍然是原对象的引用。
  • 深拷贝 (copy.deepcopy):递归地拷贝对象及其所有嵌套子对象。修改新对象对原对象没有任何影响

24. 垃圾回收机制 (GC)

面试核心: 引用计数为主,标记清除与分代回收为辅。

  1. 引用计数:每个对象都有 ob_refcnt。为 0 时立即释放。
    • 缺点:无法处理循环引用。
  2. 标记-清除:寻找“不可达”的对象。从根对象出发,无法被遍历到的对象即为垃圾。
  3. 分代回收:核心思想是 “对象存在时间越长,越不可能是垃圾”
    • 分为 0、1、2 三代。新创建的对象在 0 代,经历过 GC 仍存活则晋升。高代次的检查频率远低于低代次,从而优化性能。

25. Python 里的 is==

面试核心: 身份(Identity)与 值(Value)的区别。

  • ==:检查两个对象的值是否相等(调用 __eq__)。
  • is:检查两个对象是否指向同一块内存地址(对比 id())。
  • 特例: Python 会缓存小整数(-5 到 256)和短字符串,所以 a = 10; b = 10; a is b 会返回 True,但大整数则不然。

26. 文件读取 (read, readline, readlines)

  • read():一次性读取全部,适合小文件。
  • readline():逐行读取,适合处理超大文件。
  • readlines():读取所有行并存入列表。
  • 最佳实践: 直接遍历文件对象 for line in f:,这使用了生成器,内存效率最高。

27. Python 3 时代的版本差异 (2 vs 3)

面试核心: 虽然 2 已经淘汰,但面试官喜欢考历史演进。

  1. print:从语句变成函数 print()
  2. 编码:Python 3 默认使用 UTF-8,字符串全是 Unicode。
  3. 整除3 / 2 在 Python 3 中等于 1.5(浮点数),而在 Python 2 中是 1
  4. rangexrange
    • Python 2 中 range 返回列表,xrange 返回迭代器。
    • Python 3 中取消了 xrange,现在的 range 本身就是迭代器(延迟计算)。

28. super().__init__() 的意义

面试核心: 维护 MRO 链条。

  • 作用: 自动找到父类(或多继承链中的下一个类)并调用其初始化方法。
  • Python 3 简化: 不再需要写 super(Class, self),直接 super(). 即可。它能确保在复杂的继承网络中,每个父类只被初始化一次。

29. super()__init__()

面试核心: 解决多继承下的初始化顺序问题。

在 Python 3 中,super() 会根据 MRO (Method Resolution Order) 列表自动查找下一个类。

  • 为什么要用 super()

    1. 避免硬编码: 不需要显式写出父类名字,方便代码维护。
    2. 多继承必备: 在菱形继承中,super() 确保每个父类的 __init__ 只被调用一次,而直接用 Base.__init__(self) 可能会导致重复调用。
  • Python 2 vs 3 语法差异:

    • Python 2: super(Child, self).__init__()(比较冗余)。
    • Python 3: 直接使用 super().__init__(),简洁且推荐。

30. rangexrange 的区别

面试核心: 内存优化与延迟计算(Lazy Evaluation)。

这是考察你是否了解 Python 迭代器机制的经典题。

特性Python 2.x 中的 rangePython 2.x 中的 xrangePython 3.x 中的 range
返回类型列表 (List)生成器对象 (Object)迭代对象 (similar to xrange)
内存占用随数量线性增加极低(固定内存)极低(固定内存)
执行速度初始创建慢,遍历快初始创建快,按需生成初始创建快,按需生成
  • 结论:
    • Python 3 中,旧版的 range 已经被废弃,现有的 range 行为与 xrange 一致。
    • 原理: range 不会在内存中真实创建一个百万元素的列表,而是在你循环到某个数时,才计算并返回那个数。