Python 面试题库
收录真实面试常见问题,答案长度适合口头表达;拿不准时可返回相关主题复习。
基础
21个问题 · 0 已看01 Python 的赋值会做什么,为什么通过一个名称修改对象后,另一个名称也能看到变化? 查看答案 ▾ 收起 ▴
赋值会把名称绑定到对象;它既不会复制对象,也不是把对象装进“变量盒子”。因此,两个名称可以引用同一个可变对象。执行 backup = records 后,records.append(item) 修改的是共享列表,backup 也会看到新增项。records = [] 则不同,它只把 records 重新绑定到新列表。需要独立状态时应显式选择浅拷贝或深拷贝,并根据 API 是否要保留嵌套对象共享关系来决定拷贝深度。
02 Python 循环的 else 子句何时执行,它适合解决什么问题? 查看答案 ▾ 收起 ▴
循环正常结束时才会执行 else:for 耗尽迭代器,或 while 条件变为假。属于该循环的 break 会跳过 else,continue 不会。它很适合表达搜索:找到匹配记录时 break,把“未找到”路径放进 else。return 或未处理异常会直接离开外层函数或语句组,因此也不会到达 else。嵌套循环中,只有与这个 else 属于同一层循环的 break 才会抑制它。
03 为什么可变默认实参会在多次调用之间共享,应该怎样修复? 查看答案 ▾ 收起 ▴
默认表达式在 def 语句执行时求值,所得对象保存在该函数对象上;省略对应实参的调用会重复使用它。因此,def add(item, items=[]) 会在多次调用之间累积数据,因为每次修改的都是同一个列表。通常应使用不可变哨兵值,常见的是 None,再在函数体内创建新列表。若确实需要共享状态,应给它明确的名称和所有者。嵌套 def 只有在再次执行并创建新函数对象时,才会得到一组新的默认值。
04 Python 中的相等性与对象身份有什么区别? 查看答案 ▾ 收起 ▴
== 通过相等性协议判断两个对象表示的值是否相等;is 判断两个引用是否指向完全同一个对象。对于 None 这类单例哨兵值,应使用身份判断,因此通常写 value is None。不要因为小例子中的字符串或整数偶尔复用了实现对象,就用 is 比较它们;驻留和缓存不是值语义契约。自定义类还能实现 __eq__,使不同实例比较为相等。处理容器或领域值时,应根据真正要问的是“同值”还是“同一对象”选择操作。
05 为什么集合元素必须可哈希,自定义对象需要满足什么契约? 查看答案 ▾ 收起 ▴
集合先用元素的哈希值确定查找位置,再用相等性确认匹配,因此元素存放期间必须保持稳定的哈希。规则并不只是“必须不可变”:任何可哈希对象都能作为成员,而元组只有在全部元素都可哈希时才可哈希。自定义对象若比较相等,就必须产生相同哈希值。插入后再修改参与 __hash__ 或 __eq__ 的字段,可能让元素看起来“丢失”。集合成员宜使用不可变值对象;若记录必须可变,可改为存放稳定标识符。
06 元组的不可变性保证了什么,元组在什么情况下可哈希? 查看答案 ▾ 收起 ▴
元组不可变,指的是创建后不能替换、插入或删除其中的对象引用;它不会递归冻结所引用的对象。若元组中包含列表,该列表仍可修改,持有元组的代码也会看到内容变化。只有全部元素都可哈希时,元组本身才可哈希,因此包含列表的元组不能作为字典键或集合成员。元组适合固定位置的记录和多返回值;如果字段含义只能靠记住数字位置来理解,应改用具名记录类型。
45 为什么 match case 中的裸名称与常量行为不同? 查看答案 ▾ 收起 ▴
在 Python 3.14 的结构化模式匹配中,case READY: 这样的裸名称通常是捕获模式:它会绑定 subject,而不是与已有的局部或全局值比较。匹配所有输入的捕获还可能让后续 case 不可达。需要按值匹配时,应使用字面量或 case Status.READY: 这样的点号名称,并把具体模式放在一般模式之前,同时处理未知输入。guard 只在模式成功后执行,因此 guard 的副作用和 case 顺序都是可观察的取舍,测试必须覆盖。
46 在 Python 中遍历时,怎样安全地删除元素? 查看答案 ▾ 收起 ▴
Python 3.14 并没有一条适用于所有容器的“遍历时修改”规则。改变 dictionary 或 set 的大小通常会抛出 RuntimeError;从 list 删除元素却可能继续运行,只因索引移动而漏掉相邻项。过滤时应优先使用推导式或新结果。确实需要原地删除 list 时,可遍历 items.copy(),或采用经过仔细审查的逆序索引循环;set 则先算出待删项或遍历副本。测试要覆盖相邻项都应删除、无需删除和全部删除,不能只断言没有异常。
52 Python 函数何时能自行处理参数绑定错误? 查看答案 ▾ 收起 ▴
对 Python 3.14 的用户定义函数,解释器会先求值 callable 与参数表达式,再按签名绑定值,最后才进入函数体。缺少必需参数、同一参数得到多个值、意外关键字,以及违反 positional-only 或 keyword-only 规则,都会在函数体日志和内部 try 执行前抛出 TypeError。恢复逻辑必须放在调用方或转发 wrapper 中;适配器需要提前校验时,可用 inspect.Signature.bind()。一个具体风险是,后续绑定虽然失败,前面参数表达式的副作用却可能已经发生,因此应避免带副作用的调用构造。
53 函数契约测试应断言什么,而不是绑定实现细节? 查看答案 ▾ 收起 ▴
在 Python 3.14 中,调用方能观察到的是允许的参数形状、返回值或抛出的异常,以及文档声明的副作用。测试应覆盖正常输入、空值或阈值等精确边界,以及边界两侧的失败,并检查调用方拥有的对象是否被修改。除非执行顺序本身就是公开行为,否则不要依赖局部变量名或辅助函数调用顺序。类型注解仍只是契约描述,不会自动做运行时校验,因此反序列化后的错误输入需要显式边界测试。时钟、随机数、文件和网络若会隐藏关键输入,应通过参数注入。
58 list comprehension 使用什么作用域,其中创建的回调会怎样? 查看答案 ▾ 收起 ▴
在 Python 3.14 中,comprehension 的迭代目标位于隐式作用域内,因此 [item * 2 for item in values] 不会覆盖外层 item。但在该作用域中创建的函数仍然捕获绑定,而不是每次迭代的快照。若推导式生成 callback,稍后执行时它们可能都看到最终迭代值。可以用默认参数绑定当次值,或调用辅助工厂。直接计算普通值的推导式没有这种延迟执行间隔。测试应在推导式结束后调用 callback,并至少使用两个不同输入。
59 怎样理解嵌套 list comprehension 的求值顺序? 查看答案 ▾ 收起 ▴
Python 3.14 按从左到右的嵌套顺序执行 comprehension 中的 for 和 if,与等价的多层循环一致。最左侧 iterable 先取得;后面的子句可以使用前面绑定的名称,所有过滤条件通过后才求值结果表达式。遇到复杂推导式,应先改写成显式嵌套循环,检查名称可见性、输出顺序和调用次数。在 filter 与结果中重复调用昂贵 parser 会执行两次。若避免重复工作比保留单行更重要,应使用普通循环、辅助函数或清晰的赋值表达式。
60 什么时候不应该使用 list comprehension? 查看答案 ▾ 收起 ▴
在 Python 3.14 中,list comprehension 会立即构建结果,适合把遍历、过滤和转换组合成一个清晰的值表达式。不要主要用它执行文件写入、网络调用、日志或修改操作,再把生成的 list 丢弃。若中途表达式失败,结果名称不会被赋值,但此前元素造成的外部副作用不会回滚。普通循环能更清楚地暴露部分完成、错误处理和恢复。若结果很大且只消费一次,可用 generator expression 减少物化,但这会把单次消费责任交给调用方。
67 Python 类型注解会在运行时校验值吗? 查看答案 ▾ 收起 ▴
在 Python 3.14 中,annotation 提供机器可读的契约元数据,但普通赋值和函数调用不会自动强制执行。标注为 int 的 JSON 字段仍可能收到字符串、Boolean 或缺失值,除非边界代码或框架明确进行校验。静态检查用于发现开发期类型不一致,不可信的运行时数据则需要显式解析。还要注意 bool 是 int 的子类,因此 isinstance(value, int) 会接受标志值,除非额外排除。测试应覆盖错误容器形状、字段缺失、假值和数值边界。
68 Python 函数接收对象时,修改对象与重新绑定有什么区别? 查看答案 ▾ 收起 ▴
在 Python 3.14 中,函数调用会把参数名称绑定到参数表达式产生的对象,不会深拷贝对象。items.append(x) 修改的是 list 本身,因此所有 alias 都能观察到变化;items = new_list 只会重新绑定函数的局部名称,不会让调用方名称改指向新对象。无论有没有类型注解,赋值语义都一样。API 应明确选择“返回转换结果”还是“原地更新”,并同时测试 identity 与内容。浅拷贝只隔离外层容器,嵌套可变值仍可能共享。
71 Python set 应怎样跨越有序 API 或序列化边界? 查看答案 ▾ 收起 ▴
Python 3.14 的 set 表示无序且唯一的成员关系,不承诺迭代顺序。JSON 没有原生 set 类型;若没有排序规则就直接转成 list,会产生不稳定的快照和签名。可以对可比较元素排序、按文档规定的业务 key 排序,或遍历原序列并用独立 seen set 保留首次出现顺序。必须有意选择,因为直接转 set 还会丢掉计数和原位置。若重复本身代表非法输入,应先校验再去重。测试要改变输入顺序,并断言精确外部表示,而不是某次进程中观察到的顺序。
72 API 应直接返回内部 set、副本,还是 frozenset? 查看答案 ▾ 收起 ▴
在 Python 3.14 中,直接返回内部可变 set,等于允许调用方对所有者使用的同一个对象执行 clear、添加或更新。需要可变的顶层快照时返回 set(internal);只应暴露只读且可哈希的成员关系时返回 frozenset(internal)。两者都不会递归复制或冻结成员对象,而成员本身必须可哈希且稳定。复制的时间与内存成本也与成员数量成正比,高吞吐 API 可改为提供查询方法或明确的不可变 view 契约。应通过两个调用方的修改测试验证承诺的隔离。
76 什么语法会创建单元素 tuple,为什么 tuple(value) 不同? 查看答案 ▾ 收起 ▴
在 Python 3.14 中,真正创建 tuple 的是逗号,圆括号通常只是表达式分组。(42) 是 integer,(42,) 才是单元素 tuple。tuple(value) 则把 value 当作 iterable 消费,因此 tuple("ab") 会得到两个字符串,单次 iterator 也会被耗尽。若要把一个现有对象包成单元素 tuple,应写 (value,),而不是 tuple(value)。这个区别可能静默改变字符串和 generator,而非立即报错。测试应包含空字符串、多字符字符串、已有 tuple 和单次 iterator,同时暴露形状与消费行为。
77 对于持续演进的公共 API,什么时候 tuple 不是合适的返回类型? 查看答案 ▾ 收起 ▴
在 Python 3.14 中,用 tuple 返回数据会让元素数量、顺序和含义都成为调用方契约。某个分支或后续版本增删位置时,固定长度解包会抛出 ValueError;字段增多后,裸索引也很难审查。局部使用的短 pair 仍可能清楚,持续演进的 record 通常更适合 NamedTuple、data class 或其他具名字段对象。NamedTuple 保留 tuple 行为,但其 annotation 不会校验运行时输入。应测试每个返回分支都有同一稳定形状,并把新增字段视为兼容性变更。
78 当零是有效数据时,为什么 value or default 可能出错? 查看答案 ▾ 收起 ▴
在 Python 3.14 中,or 会在第一个操作数为 truthy 时返回它,否则返回第二个;它并不是专门处理 None 的合并运算符。因此,0、False、"" 和空容器等有效值都会被替换为 default。只有缺失才应触发回退时,要显式检查 value is None;若 None 也是有效值,则使用唯一 sentinel。自定义真值测试可能调用 __bool__() 或 __len__(),甚至可能抛出异常,所以不能把它当作无成本校验。测试必须覆盖每一种允许的假值。
79 API 在把文本或 float 转为 int 前必须决定什么? 查看答案 ▾ 收起 ▴
Python 3.14 的 int() 会随输入类型体现不同契约:int("101", 2) 按指定进制解析文本,int(3.9) 则把数值朝零截断;int("3.9") 会抛出 ValueError。这些操作都不能证明业务范围、单位或精度策略。边界代码必须规定允许的语法、符号、进制、空白、最小值和最大值,并报告非法数据,而不是静默替换默认值。金额通常不应使用二进制 float 表示精确值,应采用整数最小货币单位,或在明确舍入策略下使用 Decimal。
函数进阶
28个问题 · 0 已看07 仅限位置形参与仅限关键字形参如何让 Python API 更安全? 查看答案 ▾ 收起 ▴
/ 之前的形参仅限位置传入;裸 * 或 *args 之后的形参仅限关键字传入。仅限位置形参不会把内部名称变成公开调用契约,因此实现可以改名而不破坏关键字调用方。仅限关键字形参会让布尔选项、单位和超时等易混值在调用处显式出现,并防止顺序写反。*args 把额外位置实参收集为元组,**kwargs 把额外关键字实参收集为新字典。只有 API 确实接受开放参数集合时,才应使用兜底形参。
08 为什么在循环中创建的回调常常会读到循环变量的最终值? 查看答案 ▾ 收起 ▴
闭包通常保留的是对外层绑定的访问,而不是函数创建时的值快照。同一循环中创建的多个回调可能共享循环变量的单元格(cell),因此稍后执行时都会读取它的最终绑定。例如,三个 lambda: queue_name 可能都返回最后一个队列名。可以写成 lambda queue_name=queue_name: queue_name 保存当前对象,或每轮调用一次辅助工厂。默认值保存的仍是对象引用;若不能看到对象后续的修改,还需要显式复制。
09 装饰器会替换什么,为什么包装函数应使用 functools.wraps? 查看答案 ▾ 收起 ▴
装饰器在被装饰的定义执行时运行,并把函数名称重新绑定到装饰器的返回值。包装器可以改变调用行为,也可能遮蔽元数据和原始签名。functools.wraps(original) 会复制 __name__、__doc__、注解等常用属性,并设置 __wrapped__,让 inspect.signature 等工具可以沿包装链检查原函数。它并不能证明包装器正确转发参数或保持语义。透明包装器还应正确传递 *args 和 **kwargs、返回结果、保留异常,并在包装异步函数时正确使用 await。
10 生成器与可重复使用的集合有什么区别? 查看答案 ▾ 收起 ▴
生成器是一种惰性计算值的迭代器,会在多次 yield 之间保留暂停的执行帧。创建生成器不会把函数体执行到底;每次 next() 都从上一个 yield 处恢复,直到函数返回并触发 StopIteration。它只能单次遍历,耗尽后再次循环不会产生任何值。惰性可以节省内存并支持流式处理,但异常和副作用也会在消费时而非创建时发生。多个使用方需要独立遍历时,应分别创建生成器;数据规模和生命周期允许时,也可显式物化。
11 什么情况下 functools.partial 比 lambda 更清晰,它会保存什么? 查看答案 ▾ 收起 ▴
functools.partial 会创建一个可调用对象,调用原可调用对象时自动补上预先绑定的位置或关键字实参。当“预先固定参数”就是全部目的时,它比 lambda 更清晰,例如把 power(base, exponent) 专门化为固定 exponent=2 的平方函数。partial 对象保存的是原可调用对象和已绑定实参对象的引用,不会复制可变值。后续调用负责补齐其余参数,也可能覆盖先前绑定的关键字。若逻辑还包含分支、验证、清理或需要独立的领域接口,应使用具名函数。
12 即使存在同名全局名称,函数为什么仍可能抛出 UnboundLocalError? 查看答案 ▾ 收起 ▴
Python 会在函数运行前对整个代码块中的名称进行分类。函数内任意位置出现赋值、导入、循环目标等绑定操作,通常都会让目标名称在整个函数中被视为局部名称。于是,前面的读取会查找这个局部绑定,而不会回退到同名全局绑定;若执行到此处时局部值尚未赋予,就会抛出 UnboundLocalError。修复时应先确定状态所有权,不要盲目添加 global。优先通过参数传入并返回新值;只有最近的外层函数确实拥有持久状态时才使用 nonlocal。
40 *args 和 **kwargs 会复制调用方传入的对象吗? 查看答案 ▾ 收起 ▴
在 Python 3.14 中,可变位置参数会得到一个新 tuple,可变关键字参数会得到一个新 dictionary,但其中的元素和值仍是调用方提供的同一批对象。在局部 kwargs 中替换顶层条目不会改变被解包的源 mapping,修改嵌套 dictionary 却可能影响调用方状态。应把参数收集理解为浅层容器边界,而不是深拷贝。需要明确所有权,只复制必须隔离的嵌套层级,并分别通过修改测试检查外层 mapping 与嵌套值。
41 怎样诊断 Python 转发调用中的重复关键字错误? 查看答案 ▾ 收起 ▴
Python 3.14 会按源码顺序求值参数表达式,再在进入函数体前把它们绑定到目标签名。显式关键字与 **options、两个解包 mapping,或位置参数与关键字若填充同一参数,绑定会抛出 TypeError;被调函数内部无法捕获这种进入前错误。可用 inspect.signature() 查看目标,并在转发层通过 Signature.bind() 重现契约,然后明确选择拒绝、默认或覆盖策略。函数调用会拒绝重复关键字,不能照搬 dictionary 展开时后值覆盖前值的规则。
42 __exit__ 如何决定 with 代码块中的异常是否继续传播? 查看答案 ▾ 收起 ▴
在 Python 3.14 中,受保护代码块失败时,__exit__ 会收到异常类型、异常值和 traceback;正常结束时三者都是 None。返回真值会吞掉该异常,返回 False 或 None 则在清理后继续传播。因此,抑制逻辑只能匹配契约明确允许的异常类型,不能无条件返回 True。还要规定清理本身失败时的优先级,因为新错误可能取代代码块错误,除非通过异常链保留两者。测试应覆盖成功、可抑制错误、无关错误和清理错误。
43 上下文管理器在 __enter__ 中途失败时,谁负责清理? 查看答案 ▾ 收起 ▴
Python 3.14 只有在 __enter__ 成功后才会调用 __exit__。如果进入阶段先取得一个资源,获取下一个资源时失败,管理器的进入逻辑必须自行释放已有的部分状态。稳健的多步骤管理器可以把每次成功获取立即登记到内部 ExitStack,全部完成后再用 pop_all() 转移所有权。不能先批量获取,最后才登记清理。故障注入测试应让第二个或更后的获取步骤失败,并断言此前每个资源都恰好释放一次。
44 用 contextmanager 装饰的函数必须遵守什么契约? 查看答案 ▾ 收起 ▴
在 Python 3.14 中,contextlib.contextmanager 会把恰好 yield 一次的 generator 适配为上下文管理器。yield 前的代码负责进入,产出的值成为 as 目标,恢复执行后的代码处理退出。无条件释放应放进 finally;回滚、提交或选择性抑制则通过明确的 except、else 和 raise 路径表达。调用被装饰函数只会创建一次性管理器,不会立刻执行函数体,因此每次使用都应创建新实例。测试不能只走成功路径,还要覆盖代码块异常和 yield 后失败。
50 多层 Python 装饰器按什么顺序求值、应用和调用? 查看答案 ▾ 收起 ▴
在 Python 3.14 中,def 上方的 decorator 表达式先从上到下求值,得到的 callable 再从下到上应用到函数。因此,@outer 位于 @inner 上方时,最终绑定等价于 outer(inner(func))。实际调用时先进入外层 wrapper,最后离开外层。这个区别会影响鉴权、重试、缓存、事务和异常边界。不要只凭缩进猜顺序,应展开嵌套调用,并用事件记录测试进入、结果、异常和退出,还要覆盖 cache hit 与调用被拒绝的路径。
51 为什么同步 wrapper 会破坏 async 函数装饰器的执行边界? 查看答案 ▾ 收起 ▴
在 Python 3.14 中,调用 async def 只会返回 coroutine 对象,函数体要到被 await 时才执行。同步 wrapper 若只是返回 func(*args, **kwargs),包围的只是 coroutine 创建,不是之后的实际执行,因此计时日志和 try 会在异常发生前结束。透明的异步 decorator 需要 async def wrapper,并在预期的日志、清理、重试或异常边界内执行 await func(...)。wraps 能保留元数据,却不能修复行为。测试应覆盖 await 后的成功、异常和取消,重试代码也不能使用阻塞的 time.sleep()。
54 Python 3.14 的 functools.Placeholder 解决了 partial 的什么问题? 查看答案 ▾ 收起 ▴
Python 3.14 之前,partial() 只能预填一段连续的前导位置参数;要固定后面的槽位,通常需要 wrapper 或关键字。Python 3.14 新增 functools.Placeholder,可以保留某个位置槽,由之后的调用填入。每个 placeholder 都必须获得一个位置参数,而且不能作为关键字 placeholder 使用。它能简洁适配位置参数 API,但也会提高最低运行时版本,并可能让调用形状更难读。需要校验、强制策略或更清楚的参数语义时,应优先写具名 wrapper。
55 为什么 reduce 的 initializer 是契约的一部分,而不只是便利参数? 查看答案 ▾ 收起 ▴
在 Python 3.14 中,functools.reduce(function, iterable, initial) 会把 initial 视为位于输入之前。空输入直接返回它,单元素输入也会调用一次二元函数。省略 initializer 时,首元素成为 accumulator,空输入则抛出 TypeError。因此 seed 决定结果类型和空输入语义。存在真正 identity 时才使用;没有合理 identity 时,应先拒绝空输入,而不是发明误导性默认值。dict.update() 这类原地方法返回 None,把它藏进 reducer 会让下一步收到错误的 accumulator。
56 为什么 generator 应使用 return,而不是抛出 StopIteration? 查看答案 ▾ 收起 ▴
在 Python 3.14 中,若 StopIteration 从 generator 函数体逃出,会被转换为 RuntimeError: generator raised StopIteration;这样能避免嵌套操作意外耗尽时静默截断输出。正常结束 generator 应使用 return。return value 会进入 StopIteration.value,可由 yield from 接收,但普通 for 不会把它作为元素产出。可选读取另一个 iterator 时,应使用 next(source, default),或在局部捕获 StopIteration。测试要覆盖空输入与已耗尽输入,避免把正常协议边界误判为应用失败。
57 跳出 for 循环会关闭 generator 拥有的资源吗? 查看答案 ▾ 收起 ▴
在 Python 3.14 中,break 只会退出循环,通常不会对 iterator 调用 close()。仍被引用的 generator 可能暂停在 yield,继续占用文件、cursor 或锁,直到恢复、显式关闭或被回收。释放逻辑要放进 generator 的 finally,拥有消费责任的一方则在自己的 finally 中调用 close(),或使用 contextlib.closing()。不能依赖立即垃圾回收,因为那是与实现相关的生命周期假设。测试应只消费一个元素就停止,并断言清理立即发生。
61 哪些调用会共享 lru_cache 条目,为什么等价调用也可能 miss? 查看答案 ▾ 收起 ▴
在 Python 3.14 中,lru_cache 根据调用形式构造 key,不会先把每次调用绑定并规范化为统一签名。位置与关键字写法、省略与显式提供默认值,甚至不同关键字顺序,都可能占用不同条目。设置 typed=True 还会区分直接参数的类型,但不会递归标记嵌套内容的类型。命中率重要时,应统一内部调用约定,并在代表性流量下检查 cache_info()。不能让正确性依赖“看似相等的调用一定合并”,因为文档契约之外的 key 细节属于实现问题。
62 并发时,lru_cache 是否保证每个 key 只计算一次? 查看答案 ▾ 收起 ▴
Python 3.14 会保持 lru_cache 内部 mapping 在线程并发下的一致性,但这种线程安全不等于 single-flight。两个线程可能在任一结果写入前同时看到同一个 cold key,于是被包装函数会执行多次。因此,函数必须能容忍重复执行;若重复工作或副作用不可接受,应在业务层加入按 key 协调。预热后的 cache 测试发现不了这个竞争,必须故意重叠两个冷调用。也不要直接缓存 async def,因为那会保存 coroutine 对象,而同一个 coroutine 不能安全地重复 await。
63 什么时候不应该给函数加 lru_cache? 查看答案 ▾ 收起 ▴
在能明确说明“完整 key 的结果有效到哪个事件”为止之前,不应缓存。Python 3.14 的 lru_cache 只能看到参数;数据库行、权限、配置、时间等隐藏依赖发生变化时,条目不会自动失效。可以把稳定版本加入 key,或在拥有数据变更的事务提交后调用 cache_clear()。key 空间无界时使用有限 maxsize,并优先缓存不可变结果,因为命中会返回同一个已存对象。缓存实例方法还会把 self 保留在 key 中,可能延长实例生命周期。应测量命中分布与对象大小,不能假定复用一定划算。
64 Python 3.14 的 map(strict=True) 如何改变多输入映射? 查看答案 ▾ 收起 ▴
多输入 map() 默认在最短 iterable 耗尽时停止,并静默忽略较长输入的尾部。Python 3.14 新增 strict=True;长度不一致时会在迭代过程中抛出 ValueError。构造仍是惰性的,因此只创建 map 既不能证明长度相同,也不能证明转换成功。测试必须真正消费,而且发现不一致前可能已经产出若干结果。strict 模式能发现对齐错误,却不会回滚消费者此前的写入。业务要求全有或全无时,要使用事务或预校验;只有明确允许截断时才保留默认行为。
65 为什么 filter(None, values) 可能删除有效数据? 查看答案 ▾ 收起 ▴
在 Python 3.14 中,filter(None, values) 会执行普通真值测试,只保留 truthy 元素。它删除的不只是 None,还包括 0、False、空字符串和空容器;这些可能是有效业务值,例如零余额或明确允许的空标签。若只想去掉缺失值,应使用 lambda value: value is not None 这样的精确 predicate。返回结果是惰性、单次消费的 iterator,因此测试必须实际消费,并包含所有允许的假值。只有真正需要可重复使用 collection 的 API 边界才应物化。
66 什么时候 comprehension 或循环比 map、filter、reduce 更清楚? 查看答案 ▾ 收起 ▴
在 Python 3.14 中,map(named_function, source) 能清楚强调具名转换的复用;comprehension 则把短表达式、过滤条件和迭代结构放在一起。需要惰性时,generator expression 通常比嵌套 lambda 更直接。只有自定义 accumulator 与二元转换仍容易解释时才使用 reduce();sum、any、all、字符串连接或显式循环往往更准确地命名结果。涉及副作用、部分失败策略或多项状态更新时,普通循环更合适。不要只为缩短代码选择函数式形式,并要说明惰性结果由谁消费。
69 哪些 Python 语句会创建局部作用域,哪些只创建 suite? 查看答案 ▾ 收起 ▴
在 Python 3.14 中,函数调用会创建局部 namespace;module、class definition、comprehension 和 annotation scope 则各有专门规则。普通 if、for、while、with、try 和 match suite 不会创建类似函数的局部作用域。其中绑定的名称通常能在当前函数后面访问,但前提是运行时确实执行了该绑定;未进入的分支因此可能导致 UnboundLocalError。不能把其他语言的 block scope 习惯直接套用。应显式初始化必要值,或组织分支,使每条继续执行的路径都完成绑定。
70 为什么通过 locals() 赋值不能可靠地修改函数局部变量? 查看答案 ▾ 收起 ▴
在 Python 3.14 中,locals() 会暴露表示当前局部 namespace 的 mapping,但优化后的函数 frame 并不把 mapping 写入作为修改快速局部变量的受支持接口。读取它适合诊断,或传给明确接收 mapping 的 API;先执行 locals()[name] = value,再读取裸名称 name,不能作为可靠程序逻辑。动态字段应放入显式 dictionary、data class 或普通对象。也不要把整个 mapping 直接写入日志,其中可能含有凭据或大型对象。测试应直接针对显式容器契约。
73 singledispatch 选择实现时检查哪个值? 查看答案 ▾ 收起 ▴
在 Python 3.14 中,functools.singledispatch 只根据第一个参数的运行时类型选择实现。后续参数、容器元素类型和返回注解都不会增加分派维度。因此,即使注册逻辑想处理 value,save(context, value) 实际仍会按 context 分派。应把决定行为的对象放在首位,或改用更清楚的条件判断或多分派设计。singledispatchmethod 会跳过已绑定的 self 或 cls,使用第一个普通参数。测试应分别改变每个参数,并断言 generic.dispatch(type) 的结果。
74 singledispatch 如何解析子类和重叠的抽象基类? 查看答案 ▾ 收起 ▴
在 Python 3.14 中,精确注册最先获胜;否则 singledispatch 会考察具体类的 MRO 与适用的抽象基类。最近的普通已注册基类优先,但两个互不相关且都匹配的 ABC 可能产生歧义并抛出 RuntimeError,不会按注册顺序决定。内建继承关系也很重要:bool 是 int 的子类,因此若没有更具体注册,整数 handler 会接收标志值。应建立覆盖精确类型、普通子类、虚拟子类、bool 和未注册类型的分派矩阵。测试解析要用 dispatch(ConcreteType),因为 registry[type] 只能看到显式条目。
75 在插件系统中使用 singledispatch 注册有什么风险? 查看答案 ▾ 收起 ▴
Python 3.14 的注册通常在模块 import 时执行,因此未进入真实启动路径的插件不会提供任何 handler。对同一精确类型再次注册会改变共享 generic function,后注册者获胜,使 import 顺序成为行为的一部分。应定义确定性的发现流程、重复冲突策略,并在启动时通过 dispatch() 检查关键具体类型。公共 API 没有 unregister 操作,因此测试应创建局部 generic function,或使用全新进程隔离。新增宽泛 ABC 注册同样是 API 变更,因为已有类可能开始选择新的实现。
对象与类
9个问题 · 0 已看13 Python 的类属性与实例属性如何相互作用? 查看答案 ▾ 收起 ▴
普通实例读取属性时,如果实例没有自己的值,就会通过包括类继承链在内的机制继续查找。因此,类属性可作为共享的后备值;而 account.currency = "EUR" 这类赋值通常会创建或替换实例属性。新值只在该实例上遮蔽 Account.currency,不会改变类或其他实例。可变类属性尤其容易出错:继承到的列表可能被所有实例意外共享。每个对象独有的可变状态应在 __init__ 中创建;只有确实需要共享所有权时,才通过类修改共享配置。
14 为什么 super() 遵循 MRO,而不只是“调用父类”? 查看答案 ▾ 收起 ▴
super() 会把调用委托给当前类的方法解析顺序(MRO)中的下一个实现,并不一定是某个写死的父类。这在菱形多重继承中很重要:只要各方法协作,调用就能沿 C3 线性化顺序前进,使每个实现执行一次。所有参与方法都必须使用兼容签名,消费自己负责的参数,转发其余参数,并一致调用 super()。直接调用某个基类方法可能跳过其他类,或让共同祖先执行两次。行为不清楚时检查 Class.mro();若各类无法维持同一协作契约,应优先使用组合。
15 抽象基类会强制什么,又不会强制什么? 查看答案 ▾ 收起 ▴
使用 ABC 和 @abstractmethod 定义的抽象基类,会在子类仍有未实现抽象方法时阻止其实例化。这样,不完整的名义实现会在构造阶段失败,同时也为 isinstance 判断和共享行为提供统一目标。抽象方法本身仍可包含可复用实现,子类可以通过 super() 调用。该机制不会验证任意方法签名,也不能保证语义正确;子类完全可能把同名方法实现错。注册虚拟子类只影响成员关系判断,不会注入方法,因此测试仍须覆盖行为契约和失败路径。
16 为什么 dataclass 的可变默认字段应使用 default_factory? 查看答案 ▾ 收起 ▴
field(default_factory=list) 保存的是可调用对象,并在每次创建 dataclass 实例时调用它,因此每个对象都会得到自己的列表。若直接把可变列表写成字段默认值,就会暗示所有实例共享同一对象;dataclasses 会对常见的不可哈希可变默认值抛出 ValueError。工厂也可为字典、集合或领域对象创建独立默认值。default_factory 接收的是无参数可调用对象,而不是调用结果。跨多个字段的派生值或验证逻辑应放进 __post_init__,因为它需要完整初始化后的实例。
17 属性查找时,数据描述符与非数据描述符有什么区别? 查看答案 ▾ 收起 ▴
描述符是通过协议方法定制属性访问的类属性。数据描述符除 __get__ 外还定义 __set__ 或 __delete__,它的优先级高于实例的 __dict__。非数据描述符只定义 __get__,因此实例属性可以遮蔽它。函数就是非数据描述符;实例访问函数时会得到绑定方法,同时仍允许实例级遮蔽。可复用描述符通常应把每个实例的值存进实例本身或弱键存储,并在 __get__ 收到 instance=None 时返回描述符自身,以支持类级访问。
18 元类在类创建过程的哪个阶段参与,什么情况下值得使用? 查看答案 ▾ 收起 ▴
元类是“类的类”,通常默认使用 type。执行 class 语句时,__prepare__ 可以提供命名空间,类体向其中写入内容,随后元类的 __new__ 和 __init__ 创建并初始化类对象。以后调用这个类创建实例时,会经过元类的 __call__。只有框架需要在类定义时一致地校验或注册行为,才通常值得自定义元类。局部转换应优先考虑类装饰器或 __init_subclass__,因为元类会沿继承关系传播,多个无关元类还可能发生冲突。
47 deepcopy 如何处理对象图中的环和共享子对象? 查看答案 ▾ 收起 ▴
在 Python 3.14 中,copy.deepcopy() 会在一次遍历中维护从源对象 identity 到副本的 memo mapping。若源图中两条边指向同一个可变子对象,副本中的两条边通常也指向同一个新子对象;若一条边回到祖先,memo 会闭合这个环,而不是无限递归。自定义 __deepcopy__(memo) 必须先登记新对象,再递归复制子节点,并继续传递同一个 memo。朴素递归可能重复共享状态或在环上溢出。测试应检查 identity 关系和反向引用,不能只比较打印值。
48 Python 3.14 中的 copy、deepcopy 和 copy.replace 有什么区别? 查看答案 ▾ 收起 ▴
copy.copy() 会创建新的外层对象,但通常保留对子对象的引用。copy.deepcopy() 递归遍历对象图,并通过 memo 保持共享关系与环。Python 3.13 新增、3.14 继续提供的 copy.replace() 会创建同一受支持类型,并按名称替换指定字段;未改字段遵循该类型的替换语义,所以 replacement 不等于深拷贝。它支持 named tuple、data class 和定义了 __replace__() 的类。选择方式应从修改路径与所有权契约出发。盲目深拷贝可能成本很高,也可能不适用于锁、句柄和 identity 具有业务含义的对象。
49 怎样测试复制操作确实提供了契约承诺的独立性? 查看答案 ▾ 收起 ▴
在 Python 3.14 中,只比较 equality 无法证明副本独立,因为两个不同对象图在修改前可能值相等。测试夹具应包含可变嵌套子对象、两个字段共享同一子对象、允许时加入一个环,以及策略明确要求继续共享的对象。复制后沿结果中的各条关键路径修改,并断言源对象哪些路径应变化、哪些不应变化,同时检查 identity 和源图到副本图的拓扑。这样能区分赋值、浅拷贝、保留拓扑的深拷贝和朴素递归复制。夹具要保持有界,因为完整深拷贝可能代价很高。
并发
7个问题 · 0 已看19 协程对象与 asyncio Task 有什么区别? 查看答案 ▾ 收起 ▴
调用 async def 函数会创建协程对象,但不会独立调度函数体。await 该对象时,它作为当前任务的一部分被驱动。asyncio.Task 会包装协程,把它安排到正在运行的事件循环,并保存结果、异常或取消状态。因此,创建任务是一项所有权决定,并不只是 await 的另一种写法。调用方必须保留并最终等待该任务,或在 TaskGroup 中创建它,由任务组限定子任务生命周期并定义失败传播路径。
20 asyncio.TaskGroup 如何处理失败的子任务? 查看答案 ▾ 收起 ▴
TaskGroup 把所有子任务视为一项结构化操作。某个子任务抛出非取消异常后,任务组会取消尚未完成的兄弟任务,等待它们执行清理,并在退出上下文时把仍需报告的失败作为异常组抛出。协程应在 finally 或 async with 中释放资源,并通常让 CancelledError 继续传播。默认的 asyncio.gather 不同:它会向等待方传播首个异常,但不会仅因该异常自动取消其他 awaitable。应根据一个任务失败后其余工作是否仍有意义来选择 API。
21 并发执行 Python 工作时,应如何在线程与进程之间选择? 查看答案 ▾ 收起 ▴
应根据工作负载和共享边界选择。线程共享同一进程和内存,适合阻塞 I/O,也适合会释放解释器锁的库;但共享可变状态必须同步。进程拥有独立内存,可以并行执行 CPU 密集型 Python 工作,代价是启动、进程间通信、序列化和更高资源消耗。常见的启用 GIL 的 CPython 构建不会让纯 Python 的 CPU 密集线程并行,但这个结论不能代替测量。应对真实任务做基准测试,把数据传输成本算进去,并覆盖关闭和失败路径。
22 即使存在 GIL,Python 多线程代码为什么仍会发生竞态条件? 查看答案 ▾ 收起 ▴
GIL 并不会把应用不变量自动包成事务。检查账户余额再更新余额这类逻辑操作,包含多次读取、判断和写入;另一个线程可能在这些步骤之间穿插执行,特别是在阻塞调用或显式释放锁的边界。不要根据一次观察到的字节码序列,或某个内置操作在特定解释器构建上看似原子,就推断代码线程安全。应使用 Lock 保护完整不变量,或把状态交给单一 worker 并通过队列通信,也可使用不可变消息。测试要主动制造交错,并验证结果与关闭行为。
23 什么情况下应使用 Executor.map,而不是 submit 配合 as_completed? 查看答案 ▾ 收起 ▴
对一组输入应用同一个可调用对象,并要求按输入顺序消费结果时,适合使用 Executor.map。因此,前面的慢任务即使不影响后续任务完成,也可能阻塞后续结果交付。每个作业需要独立元数据、取消或错误处理时,应使用返回 Future 的 submit;再迭代 as_completed(futures),就能按完成顺序立即处理快速结果。两种方式中,读取结果都会重新抛出 worker 异常。还应限制提交量,定义超时,并通过上下文管理器或显式 shutdown() 关闭执行器。
24 为什么会重试的 Celery 任务应具备幂等性? 查看答案 ▾ 收起 ▴
Celery 任务通过 broker 交给 worker;外部副作用已经发生后、消息确认或结果记录之前,仍可能出现故障。重试或重新投递就可能再次执行该任务。任务应使用稳定的操作键,让写入具备条件性或安全的 upsert,并尽可能在与副作用相同的持久化边界记录完成状态。只对瞬时故障进行有界退避和抖动重试,永久性验证错误应立即失败。acks_late 会改变确认时机,但它本身不能让非幂等的付款或邮件发送变得安全。
37 asyncio 协程拥有资源时,应如何处理取消? 查看答案 ▾ 收起 ▴
取消是一种协作式控制信号,会在暂停点以 CancelledError 的形式送达。资源释放应放在 finally 中,或交给异步上下文管理器,使操作成功、失败或取消时都能执行清理。协程可以短暂捕获取消以恢复不变量,但清理后通常应重新抛出;吞掉取消可能破坏 TaskGroup 与超时边界。清理本身需要等待时,所有权边界应持续到清理完成。只有内部操作明确不可取消时才使用 shield,同时要记住调用者仍可能被取消。
标准库
6个问题 · 0 已看25 什么情况下应选择 collections 中的 Counter、defaultdict 或 deque? 查看答案 ▾ 收起 ▴
应选择最能表达操作意图的容器。Counter 把可哈希值映射到计数,并提供 most_common 等频次操作。defaultdict(factory) 在通过 __getitem__ 访问缺失键时创建并插入默认值,适合分组,但一次看似读取的操作也可能修改映射。deque 能高效地在两端追加和弹出,因此实现 FIFO 队列时优于反复删除列表索引 0。这些类型不能替代领域验证;API 边界仍需定义负计数、缺失分组、队列最大长度和序列化的含义。
26 为什么 itertools.groupby 可能为同一个键产生多个分组? 查看答案 ▾ 收起 ▴
itertools.groupby 只把键相同的连续区段分组,并不会像数据库的 GROUP BY 那样收集整个可迭代对象中的相同键。因此,按 A、B、A 排列的输入会产生三个分组。需要全局分组时,应先按同一个键排序,同时考虑排序的时间和内存成本。每个返回分组都是与外层 groupby 迭代器共享底层输入的迭代器。外层继续前进后,早先分组可能不再可用,所以应立即消费;若分组需要延长生命周期,则应显式物化。
27 pathlib 改善了什么,哪些边界仍需显式处理? 查看答案 ▾ 收起 ▴
pathlib.Path 用对象表示文件系统路径,并提供 / 拼接、修改后缀、遍历和打开文件等可组合操作。相比手工拼接分隔符,它提高了可读性和跨平台性,但不会让 I/O 自动可靠或安全。相对路径仍依赖当前工作目录,resolve() 只是改变路径视图而不是授予访问权限,而且文件可能在检查后、使用前发生变化。文本操作应指定编码,处理预期的 OSError 子类,并在真正执行操作时验证用户选择的路径仍位于允许的根目录内。
28 Python 数据经过 JSON 边界时会丢失哪些信息? 查看答案 ▾ 收起 ▴
JSON 承载的是小型、语言无关的数据模型,而不是任意 Python 对象的身份和行为。对象会表示为字符串键映射,数组会变成列表;自定义类、集合、字节、作为独立类型的元组以及带时区日期时间,都需要显式表示。因此,往返转换可以保留业务数据,却未必保留原始 Python 类型。应在边界定义 schema 并显式转换,不要依赖会悄悄抹掉结构的 default=str。输入必须视为不可信数据:按场景限制大小和嵌套深度,验证必需字段,并明确是否接受非标准数值。
29 为什么一次 Python 日志调用可能产生重复输出? 查看答案 ▾ 收起 ▴
Logger 按点分名称形成层级。service.api 处理的记录通常还会向祖先 logger 传播,因此在子 logger 和 root 上挂载等价 handler,可能让同一记录输出两次。库代码应通过 logging.getLogger(__name__) 创建具名 logger,把 handler 策略交给应用。应用在入口处统一配置 handler、级别、格式和目标。只有子 logger 确实需要独立处理时,才应有意设置 propagate=False。还要记住 logger 与 handler 的级别都会过滤记录;需要堆栈时,应在活动异常处理块中使用带异常信息的日志调用。
30 怎样使用 subprocess 安全地运行外部命令? 查看答案 ▾ 收起 ▴
对大多数有界命令,应调用 subprocess.run,并把可执行文件及每个实参分别放进列表元素。这样可以避开 shell 解析,让不可信文本始终只是一个参数。若非零退出码应视为失败,设置 check=True;同时设置超时,并明确输出是字节,还是按指定编码解码的文本。不要对用户可控数据使用 shell=True,否则转义错误会演变为命令注入。流式或长期交互可使用 Popen,但必须谨慎管理管道,避免双方死锁。需要可复现时,还应明确子进程环境和工作目录。
类型与工具链
6个问题 · 0 已看31 Python 类型提示会在运行时强制类型吗? 查看答案 ▾ 收起 ▴
不会。类型提示为静态检查器、编辑器、审查者以及主动读取注解的库描述契约;普通函数调用不会依据注解验证实参或返回值。HTTP 请求、JSON、环境变量和数据库行等运行时边界仍需要解析与验证。Any 是绕过静态检查的出口,并不证明值安全;如果代码接受任意值,但使用前必须缩窄类型,通常 object 更合适。应把静态检查与运行时验证分开设计,并测试转换层能否清楚报告无效输入。
32 typing.Protocol 与抽象基类有什么区别? 查看答案 ▾ 收起 ▴
Protocol 描述一个值为满足静态结构子类型所必须提供的成员。类无需继承或注册就能满足协议,这与鸭子类型 API 一致,也避免实现与某个继承层级耦合。抽象基类默认采用名义关系,可以在抽象方法未实现时阻止实例化,还能提供共享运行时行为。@runtime_checkable 允许对协议进行有限的 isinstance 检查,但这种检查只关注属性是否存在,不会验证完整签名或语义。协议应保持小而由使用方定义;跨越不可信运行时边界时仍需单独验证行为。
33 应如何选择 pytest fixture 的作用域? 查看答案 ▾ 收起 ▴
应选择与资源真实所有权一致、且能保持测试隔离的最窄作用域。function 作用域为每个测试创建新 fixture,是可变状态最安全的默认选择。class、module、package 或 session 作用域可减少昂贵初始化,但任何修改都可能泄漏到其他测试,使结果依赖执行顺序。yield fixture 应在 yield 前获取资源,并在 finally 中释放,确保测试失败时也会清理。不要只为提速就扩大作用域;先测量初始化成本,显式重置共享状态,并验证断言失败后文件、事务、进程或临时服务仍会关闭。
34 wheel 与源码分发包有什么区别? 查看答案 ▾ 收起 ▴
源码分发包(sdist)包含源材料和构建元数据,安装时通常需要在目标环境执行构建步骤。wheel 是已构建分发包,安装器可以把它直接展开到环境中,而无需运行项目常规构建;不过 wheel 可能受 Python 版本、ABI 或平台限制。若下游可能使用未覆盖平台,或需要检查并重新构建源码,通常应同时发布两者。测试对象应是实际产物,而不只是仓库工作树。还要在 pyproject.toml 中声明构建后端及其依赖,并进行隔离构建,防止本地未声明工具掩盖依赖缺失。
35 Python 虚拟环境会隔离什么,又不会隔离什么? 查看答案 ▾ 收起 ▴
虚拟环境为项目提供独立的解释器配置和包安装目录,因此安装一个项目的依赖通常不会修改其他项目或基础环境中的包。激活主要调整 PATH 等 shell 变量,只是使用便利,并非必需;直接调用环境内的解释器同样能选中它。venv 不会隔离操作系统库、进程、网络访问、凭据或容器,也不是可在机器之间复制的可移植产物。应根据已声明并锁定的依赖重新创建环境,把它排除在版本控制之外,并确认 python 与 pip 实际指向哪个解释器。
36 应如何在 timeit、cProfile 与采样分析器之间选择? 查看答案 ▾ 收起 ▴
应先明确需要回答的问题。timeit 会在受控条件下重复测量一段小操作,适合在分离初始化成本后比较局部方案。cProfile 会在代表性运行中记录函数调用,并报告调用次数、累计时间和单次时间,便于定位应用把时间花在哪里,但会引入分析开销。采样分析器周期性观察调用栈,通常对线上进程干扰更小,代价是结果近似,而且对极短调用细节较少。应使用真实数据,重复测量并检查波动,只优化确实影响端到端负载的瓶颈。
函数
2个问题 · 0 已看38 为什么循环中创建的 Python 闭包常看到最终值,应怎样修复? 查看答案 ▾ 收起 ▴
闭包保留对循环变量绑定的访问,而不是保存每次迭代的快照。因此,循环结束后调用这些闭包时,它们会读取共享 cell 中最后存入的对象。如果每个回调都需要当次值,可以用默认参数在函数创建时绑定,例如 lambda item=item: item;也可以调用辅助工厂,让每次迭代创建新的局部绑定。每个回调还拥有可变私有状态或更复杂行为时,应优先使用辅助工厂。测试要在循环结束后调用回调,并改变外层状态,避免立即调用掩盖延迟绑定。
39 闭包何时需要 nonlocal,何时可以不使用它而修改状态? 查看答案 ▾ 收起 ▴
内层函数要重新绑定外层函数作用域中的名称时,必须使用 nonlocal。如果省略它,赋值会把该名称归类为局部名称;先读取再赋值时可能抛出 UnboundLocalError。通过现有绑定修改对象则不同:items.append(value) 改变列表,却没有重新绑定 items,所以不需要 nonlocal。应有意区分两种操作。重新绑定一个简单的不可变计数器可能很清楚;多个相关状态字段和不变量则更适合放进 dataclass 或字典。当显式对象能让所有权边界更清楚时,应避免使用 nonlocal。
没有符合筛选条件的问题。