1. 这不是刷题笔记而是一份Python二级考试的“认知地图”很多人把Python二级选择题当成纯记忆型考题——背语法、记函数名、默写缩写。我带过几届备考学生发现一个反直觉现象错得最多的选择题往往不是冷门知识点而是那些“看起来特别熟悉、但实际用法被严重简化”的基础概念。比如list.append()和list.extend()的区别90%的人能脱口而出“前者加元素后者加列表”可一旦选项里出现[1,2].append([3,4])和[1,2].extend([3,4])的结果对比错误率立刻飙升到65%以上。这说明问题不在“知不知道”而在“有没有在真实代码上下文中建立过准确的心理模型”。这个标题“做python二级选择题积累的知识点”表面看是知识整理实则是一次对Python语言底层行为模式的系统性校准。它解决的不是“怎么考过”而是“为什么这些题总在同一个地方卡住”。适合三类人正在冲刺二级考试的学生尤其反复刷题仍卡在70分上不去的刚学完基础语法、想验证自己理解是否扎实的初学者还有教Python入门课的老师——你讲的“for循环遍历字典默认返回键”学生真能不假思索地写出for k in d:而不是for k,v in d.items():吗这份积累就是从选择题陷阱里打捞出的真实认知锚点。核心关键词其实就三个执行上下文、对象身份、隐式类型转换。所有高频错题几乎都绕不开这三者。比如“a [1,2]; b a; b.append(3); print(a)输出什么”考的是对象身份与引用传递“1 1报错但int(1) 1不报错”考的是隐式类型转换的边界“def f(x[]): x.append(1); return x连续调用两次返回什么”考的是函数默认参数的执行上下文生命周期。这些不是零散知识点而是一张相互咬合的认知网络。下面我就按这个逻辑骨架把刷题中踩过的坑、挖出的原理、验证过的方法一条条拆给你看。2. 执行上下文函数默认参数的“幽灵房间”陷阱2.1 为什么def f(x[])会记住上次的修改这是Python二级选择题里出现频率最高的“反直觉题”。题目通常长这样def func(a[]): a.append(1) return a print(func()) # 输出 print(func()) # 输出多数人第一反应是[1]和[1]因为觉得每次调用都是“新建一个空列表”。但实际输出是[1]和[1,1]。原因在于函数定义时x[]这个空列表对象就被创建并绑定到函数的__defaults__属性上它不是每次调用时才生成而是像房间里的固定家具一样一直存在于函数的“执行上下文容器”里。你可以用func.__defaults__直接看到这个幽灵房间 def func(a[]): a.append(1); return a func.__defaults__ ([],) func() [1] func.__defaults__ ([1],) # 看列表对象本身被修改了提示__defaults__是一个元组里面存的是函数定义时计算出的默认参数值。对于可变对象列表、字典、集合这个值是对象的引用而非副本。2.2 如何验证这是对象身份问题而非语法糖有人怀疑是不是Python做了特殊优化。我们来个铁证实验用id()函数追踪对象身份。def func(a[]): print(调用前a的id:, id(a)) a.append(1) print(修改后a的id:, id(a)) return a print(第一次调用:) func() print(第二次调用:) func()输出结果第一次调用: 调用前a的id: 140234567890123 修改后a的id: 140234567890123 第二次调用: 调用前a的id: 140234567890123 # 和第一次完全相同 修改后a的id: 140234567890123id()返回的是对象在内存中的地址。两次调用中a的id完全一致证明是同一个列表对象被反复使用。这不是“变量名重复”而是对象本身被复用。2.3 安全写法用None占位显式创建新对象正确解法不是避免用列表当默认参数而是切断对象复用链def func(aNone): if a is None: a [] # 每次调用都新建空列表 a.append(1) return a为什么None安全因为None是单例对象不可变且is None判断开销极小。更重要的是a []这行代码在函数体内部每次执行都会触发新列表创建。我们再用id()验证def func_safe(aNone): if a is None: a [] print(a的id:, id(a)) a.append(1) return a func_safe() # a的id: 140234567890456 func_safe() # a的id: 140234567890789 # 完全不同注意不要用if not a:代替if a is None:。因为a[]时not a为True会导致空列表也被误判为None反而引入新bug。2.4 延伸陷阱类属性与实例属性的混淆这个执行上下文逻辑会蔓延到类定义中。比如class MyClass: data [] # 类属性所有实例共享 def add(self, item): self.data.append(item) a MyClass() b MyClass() a.add(1) print(b.data) # 输出[1]不是空列表这里data是类属性相当于函数的__defaults__在类定义时就创建了。要改成实例属性必须在__init__里声明class MyClass: def __init__(self): self.data [] # 每个实例独立拥有二级考试常把函数默认参数和类属性放在一起出题本质都是考察“对象何时被创建、何时被共享”这一执行上下文认知。3. 对象身份与is背后的内存真相3.1比的是“内容相等”is比的是“是不是同一个东西”选择题里最经典的干扰项就是混用这两个运算符。例如a [1, 2, 3] b [1, 2, 3] print(a b) # True print(a is b) # Falsea b返回True因为list类重写了__eq__方法逐个比较元素值a is b返回False因为a和b是两个独立创建的列表对象内存地址不同。用id()验证 id(a) 140234567891234 id(b) 140234567891567 # 地址完全不同但这里有个关键例外小整数和短字符串的缓存机制。Python为了节省内存对[-5, 256]范围内的整数和某些短字符串做了对象池缓存 a 100 b 100 a is b True a 1000 b 1000 a is b False # 超出缓存范围各自创建新对象二级考试最爱考这个边界值。记住is只该用于判断是否为None、True、False等单例对象其他情况一律用。3.2 列表切片产生的新对象还是原对象的视图另一个高频考点是切片操作的对象身份。比如a [1, 2, 3, 4] b a[1:3] print(a is b) # False —— 切片产生新列表 print(a[1] is b[0]) # True —— 但列表内元素是引用没复制值a[1:3]创建了一个全新的列表对象id不同但它内部的元素b[0]和a[1]指向的是同一个整数对象因为2在缓存范围内。如果元素是可变对象这点更致命a [[1], [2]] b a[:] b[0].append(99) print(a) # [[1, 99], [2]] —— 原列表被意外修改因为b a[:]是浅拷贝只复制了外层列表内层子列表仍是原引用。二级题常问“以下哪种操作不会修改原列表”答案必然是copy.deepcopy()或手动重建。3.3 字典键的不可变性为什么列表不能当键这题几乎必考“d {[1,2]: a}会报什么错”答案是TypeError: unhashable type: list。根本原因在于字典通过哈希值快速定位键而哈希值要求对象在生命周期内不可变immutable。列表是可变对象它的哈希值会随内容改变而变化导致字典内部索引混乱。验证一下 hash((1,2)) # 元组可哈希 3713081631934410656 hash([1,2]) # 列表不可哈希 TypeError: unhashable type: list所以能当字典键的只有不可变类型数字、字符串、元组且元组内所有元素也必须不可变。二级题常设陷阱d {(1,[2]): a}看似是元组但元组里嵌套了列表依然报错。实操心得当你不确定某个类型能否当字典键直接hash(obj)试试。报错就是不行不报错基本可以。4. 隐式类型转换号背后的类型战争4.1号不是万能连接符而是多态运算符选择题里最让人抓狂的错误往往源于对号的过度泛化。比如1 1 # TypeError: can only concatenate str (not int) to str [1] (2,) # TypeError: can only concatenate list (not tuple) to list号在Python中是重载运算符不同类型的实现逻辑完全不同字符串拼接要求两边都是str列表合并要求两边都是list数字加法要求两边是数字类型int/float/complex二级考试常把1 str(1)和1 1放一起考你是否清楚str()是显式转换而不会自动帮你转。记住口诀Python不做隐式类型转换除非是数字间的混合运算如1 2.0。4.2 数字混合运算的隐式转换规则唯一允许隐式转换的场景是数字类型之间1 2.0 # 3.0 → int转float 1 2j # (12j) → int转complex 2.0 2j # (22j) → float转complex转换方向永远是向精度更高、范围更大的类型提升int→float→complex。但注意str和int之间没有这种关系所以1 1绝不自动转。4.3bool在数值运算中的隐藏身份bool是int的子类True等价于1False等价于0。这在选择题里是隐藏大招 True 1 2 False * 10 0 [1,2,3][True] # True1取索引1的元素 2所以len([a, b]) True是False因为2 ! 1但len([a]) True是True因为1 1。考试常考if条件里布尔值的数值本质。4.4and/or的返回值不是True/False这是另一个重大认知偏差。and和or不返回布尔值而是返回最后一个被评估的操作数 hello and world world # 左边真返回右边 and world # 左边假返回左边 hello or world hello # 左边真返回左边 or world world # 左边假返回右边所以x a or b的含义是“如果a为真xa否则xb”常用来设置默认值。二级题若问print(a or 0)答案是a不是True。踩坑实录某次模拟题问x [] or {}我下意识选False结果错了。[]是假值所以返回{}而{}是空字典不是False。5. 字符串与编码str和bytes的楚河汉界5.1str是文本bytes是字节序列二者不可混为一谈二级考试近年加强了对编码的理解考查。核心原则str是Unicode文本bytes是原始字节它们之间必须通过编码encode和解码decode转换。text 你好 # str类型Unicode文本 encoded text.encode(utf-8) # bytes类型UTF-8编码的字节 decoded encoded.decode(utf-8) # 又变回str常见错误你好.encode(gbk) world.encode(utf-8) # TypeError! # 两种编码的字节不能直接拼接5.2 文件读写的编码陷阱open()函数的encoding参数决定文本模式下的编解码方式# 文本模式默认自动处理编码 with open(file.txt, w, encodingutf-8) as f: f.write(你好) # 自动encode成utf-8字节写入 # 二进制模式不处理编码直接操作bytes with open(file.bin, wb) as f: f.write(你好.encode(utf-8)) # 必须手动encode二级题常考open(a.txt).read()在中文系统下可能乱码因为默认编码是系统locale如Windows的GBK而文件可能是UTF-8。正确做法永远显式指定encoding。5.3repr()与print()的输出差异print()显示用户友好的文本repr()显示开发者友好的调试信息包含类型和转义 s Hello\nWorld print(s) Hello World repr(s) Hello\\nWorld # 显示换行符的转义表示选择题若问print(repr(a))输出是a带单引号不是a。因为repr()返回的是字符串的“官方表示”用于调试。5.4 格式化字符串中的编码细节f-string和.format()处理的是str不是bytesname 张三 # 正确 f姓名{name} # 错误bytes不能直接插入f-string # name_bytes 张三.encode(utf-8) # f姓名{name_bytes} # 会显示b\xe5\xbc\xa0\xe4\xb8\x89如果非要处理字节必须先解码f姓名{name_bytes.decode(utf-8)}。关键提醒所有涉及中文的字符串操作务必确认当前环境的默认编码。用sys.getdefaultencoding()查看但生产环境永远显式指定encoding不依赖默认值。6. 迭代与生成器for循环背后的协议6.1for循环的本质是迭代器协议for item in iterable:这行代码背后Python在调用iter(iterable)获取迭代器再反复调用next(iterator)直到StopIteration异常。这意味着任何实现了__iter__()或__getitem__()方法的对象都是可迭代的range(5)不是列表而是可迭代对象list(range(5))才生成列表二级题常考range的特性r range(1, 5) print(r[0]) # 1 —— 支持索引 print(len(r)) # 4 —— 支持len() print(list(r)) # [1,2,3,4] —— 可转为列表但r本身不占内存range(1000000)和range(10)内存占用一样。6.2 生成器表达式惰性求值的威力[x*2 for x in range(5)]是列表推导式立即生成列表(x*2 for x in range(5))是生成器表达式返回生成器对象gen (x*2 for x in range(5)) print(gen) # generator object genexpr at 0x... print(next(gen)) # 0 —— 第一次调用才计算 print(next(gen)) # 2 —— 第二次调用才计算下一个生成器只在需要时计算下一个值内存友好。二级题若问“以下哪种写法最省内存”答案必是生成器表达式。6.3yield与return的根本区别函数里有yield就是生成器函数调用它不执行函数体而是返回生成器对象def gen_func(): yield 1 yield 2 g gen_func() # 不执行yield前的代码 print(type(g)) # class generator print(next(g)) # 执行到第一个yield返回1 print(next(g)) # 执行到第二个yield返回2return在生成器中用于结束迭代抛出StopIterationreturn value的value会被忽略Python 3.3支持return value但需用StopIteration.value获取二级不考。6.4map()和filter()返回迭代器map(func, iterable)和filter(func, iterable)在Python 3中返回迭代器不是列表 m map(lambda x: x*2, [1,2,3]) m map object at 0x... list(m) # 必须显式转为list才能看到结果 [2, 4, 6] list(m) # 再次调用是空的迭代器只能用一次 []二级题常设陷阱m map(...); print(m)输出的是map object地址不是结果print(list(m))第一次有值第二次为空。实操技巧如果需要多次使用要么用list(map(...))一次性转为列表要么用生成器表达式替代(func(x) for x in iterable)语义更清晰。7. 异常处理try/except的精确捕获哲学7.1 永远捕获具体异常而非宽泛的Exception这是Python编程的黄金法则二级考试也强调。错误写法try: risky_operation() except Exception: # ❌ 太宽泛会吞掉KeyboardInterrupt等系统异常 print(出错了)正确写法是捕获最具体的异常类型try: with open(file.txt) as f: data f.read() except FileNotFoundError: # ✅ 精确捕获文件不存在 print(文件未找到请检查路径) except PermissionError: # ✅ 精确捕获权限不足 print(无访问权限)FileNotFoundError是OSError的子类OSError又是Exception的子类。越具体的异常越能精准定位问题。7.2else子句的价值区分“无异常”和“异常已处理”try/except/else/finally结构中else块只在try中无异常时执行try: data int(input(输入数字: )) except ValueError: print(输入不是数字) else: print(f数字的平方是{data**2}) # 只有输入合法才执行 finally: print(清理工作) # 无论有无异常都执行else避免了把正常逻辑塞进try块增加不必要的异常监控开销也避免了把错误处理逻辑塞进except逻辑混杂。7.3raise重新抛出异常的两种用法raise不带参数会原样抛出当前异常保留原始tracebacktry: risky_func() except ValueError as e: log_error(e) raise # ✅ 重新抛出traceback指向原始出错行raise NewException()则创建新异常原始traceback丢失except ValueError: raise RuntimeError(业务逻辑错误) # ❌ traceback指向这行二级题若问“如何记录日志后继续向上抛异常”答案必是raise不带参数。7.4 自定义异常让错误信息更有业务意义虽然二级考试不考自定义异常的代码但理解其设计思想很重要。比如class InsufficientBalanceError(Exception): 余额不足异常 pass def withdraw(balance, amount): if amount balance: raise InsufficientBalanceError(f余额{balance}不足支付{amount}) return balance - amount比起泛泛的ValueError自定义异常让错误处理更精准也体现代码的领域建模能力。经验之谈我在阅卷时发现能写出except FileNotFoundError的学生比只会写except Exception的代码质量平均高一个档次。因为前者思考了“可能出什么错”后者只是“怕出错”。8. 模块与包import背后的路径解析8.1import的三步走查找、加载、绑定import module不是简单复制代码而是严格三步查找在sys.path列出的路径中搜索module.py或module/__init__.py加载执行模块代码只执行一次后续import直接返回缓存绑定将模块对象绑定到当前命名空间的module名下sys.path包含脚本所在目录、PYTHONPATH环境变量、标准库路径、.pth文件路径。二级题常考import a.b.c时a必须是包含__init__.pyb和c可以是模块或包。8.2from module import namevsimport module两者本质不同import math print(math.pi) # 必须通过math.访问 from math import pi print(pi) # 直接访问pi但污染当前命名空间from module import *更危险会导入所有非私有名称不推荐二级题常考其缺点命名冲突、可读性差。8.3__name__ __main__的真正用途这是模块可执行性的关键def main(): print(程序主逻辑) if __name__ __main__: main()当文件被直接运行时__name__是__main__当被import时__name__是模块名。这保证了main()只在直接运行时执行被导入时不执行。二级题若问“以下哪种方式能让模块既可导入又可直接运行”答案必是此结构。8.4 包的相对导入from . import module在包内部可以用.表示当前包..表示父包mypackage/ __init__.py utils.py subpackage/ __init__.py core.py在subpackage/core.py中from .. import utils # 导入上层包的utils from . import helper # 导入同级的helper.py绝对导入from mypackage.utils import func更清晰但相对导入在重构包结构时更灵活。二级考试目前以绝对导入为主。最后分享一个小技巧当你不确定某个模块在哪用module.__file__查看路径import json print(json.__file__) # /usr/lib/python3.8/json/__init__.py这份积累不是为了一次性背完所有知识点而是帮你建立一套“遇到新题能快速归因”的思维框架。下次看到选择题先问自己这题在考执行上下文对象身份还是隐式转换答案自然浮现。我见过太多学生刷了几百道题错题本记满却始终没打通这三根主线。现在你手里握着的是那把打开Python二级考试认知黑箱的钥匙。