
简介南开大学软件学院Python期末复习资料面向该校学生及Python基础学习者系统梳理期末考试涉及的核心知识点。压缩包内为1个docx文档约5.28MB文档结构清晰、内容详实便于按章节查阅与打印。内容覆盖字典、元组、列表、集合等数据结构的操作细节浅拷贝与深拷贝的差异函数默认参数陷阱、*args/**kwargs的用法以及面向对象中的封装、继承、多态和MRO C3算法同时包含魔法函数、装饰器、生成器、正则表达式等进阶要点并配有大量代码示例与易错点提示。已有1199人学习适合考前集中复习、对照查漏补缺也可作为期末突击和日常回顾的实用手册。1. 期末复习笔记怎么用先分清“背概念”和“动手跑”拿到这份软件学院的 Python 期末复习资料第一反应是“全”但真正把它吃下去的人不多。因为 Python 期末复习的最大误区就是把“结论”当“知识”——比如“字典无序、元组不可变、集合不能重复”这句话背下来能填空但遇到dict.update()、list.extend()、*args解包这类实操题还是会翻车。这份资料的价值恰恰在于它把高频考点按“数据结构 → 函数 → 面向对象 → 魔法函数”串成了一条线每一节都有可以直接抄进解释器里跑的示例代码。适合三类人准备期末考的软件学院学生、想快速梳理 Python 核心边界的转行者、以及面试前需要系统过一遍语法细节的在职开发。全文按“是什么 → 怎么用 → 坑在哪”展开每段代码都可以复制到本地跑一遍。2. 数据结构与引用模型字典为什么无序、列表切片为什么是左闭右开这份笔记的前半部分几乎都在讲 Python 四种内置容器列表、元组、字典、集合。表面上是在背“无序”“不可变”“可重复”这些形容词实际上这些词决定了一个更底层的问题——你写的代码在内存里到底是怎么被组织的。2.1 先弄懂“无序”和“不可变”四种容器的底层差异先看资料里反复强调的三个结论字典和集合是无序的因为底层是哈希表元素位置由哈希值决定没法用下标索引。元组、数字、字符串是不可变的它们能做字典的 key。集合中的元素不允许重复所以天然适合去重。这三条不是并列关系而是因果关系。哈希表要求 key 在生命周期内哈希值不变所以只有不可变对象才能当 key而哈希表的存储顺序依赖于哈希值和当前表的容量所以字典和集合天然无序。列表之所以有序且能索引是因为它底层是连续内存块上的指针数组按下标访问是 O(1) 的。# 验证可变对象不能做字典的 key d {} try: d[[1, 2]] list # 列表可变哈希值会变直接报错 except TypeError as e: print(TypeError:, e) d[(1, 2)] tuple # 元组不可变合法 print(d) s {1, 2, 2, 3} # 集合会自动去重 print(s) # 输出 {1, 2, 3}顺序不保证这里[1, 2]作为列表是可变对象Python 无法给它一个稳定的哈希值所以TypeError是预期的。换成元组(1, 2)后就能正常作为 key。集合{1, 2, 2, 3}里写了两个 2输出只剩一个这就是“集合元素不允许重复”的直观验证。日常写代码时如果你需要一个“不重复且之后不再修改”的元素集合用元组套进集合比用列表再手动去重更安全。还有一个细节空集合必须用set()创建用{}创建的是空字典。这个不算坑但期末选择题很喜欢拿它出题。2.2 列表与字典操作extend、sort、update 的边界行为资料里的代码实例都很短但每一句都对应一个考点。列出几个高频的list.extend(seq)是在列表尾部逐个追加另一个列表中的元素和append(seq)把整个列表当成一个元素塞进去不同。list.sort(keyNone, reverseFalse)默认从小到大排序reverseTrue时从大到小。list.reverse()无返回值直接原地反转。dict.update(dict1)是用参数里的字典去更新调用者相当于合并重复 key 会被覆盖。dict.setdefault(key, default)如果 key 不存在就插入并返回默认值如果存在就返回原值不修改。dict.pop(key)必须给 key否则报错dict.popitem()无参删除并返回最后一个键值对。del dict[key]是彻底删除键dict.clear()是清空内容字典对象还在。lst [3, 1, 2] lst.extend([4, 5]) # 逐个追加lst 变成 [3, 1, 2, 4, 5] lst.append([6, 7]) # 整个列表作为最后一个元素 print(lst) # [3, 1, 2, 4, 5, [6, 7]] lst [3, 1, 2] lst.sort(reverseTrue) # 原地降序排序 print(lst) # [3, 2, 1] d1 {a: 1, b: 2} d2 {b: 3, c: 4} d1.update(d2) print(d1) # {a: 1, b: 3, c: 4}b 被覆盖extend和append的区别是期末考试最喜欢挖的坑append是增加一个元素extend是合并一个序列。update的合并语义同样值得注意它并不是“如果 key 不存在才写入”而是无条件覆盖同名字段。如果你只想补缺失的 key应该用setdefault或dict.get手动判断。2.3 对象、引用与标识为什么 a b 之后改一个等于改两个资料里有一段很重要的话“列表不存具体的值在内存中只存 IDa、b 的 ID 一样。”这就是 Python 的引用模型。变量名本身不持有对象持有的是对象在内存中的地址。执行a [1, 2, 3]时Python 在堆上创建了一个列表对象a只是一个指向它的标签。再执行b a时b也指向同一个列表并没有复制数据。a [1, 2, 3] b a b.append(4) print(a) # [1, 2, 3, 4]a 也被改了 print(a is b) # True两个名字指向同一个对象is比较的是 id比较的是 value。这个区分在期末复习里考过不止一次a is b为 True 不代表两个变量“相等”而是代表它们是同一个对象。对不可变对象来说和is经常产生不同的结果比如两个内容相同的字符串可能因为驻留机制被缓存到同一内存地址但两个内容相同的列表一定是不同对象。实操建议在理解引用模型时把“变量名 → 箭头 → 内存块”的画法在纸上画一遍。画过的同学基本不会再把当成拷贝来用。需要真正复制一份独立的列表时用切片 lst[:]或list(lst)但要记住这只是浅拷贝嵌套的可变对象仍然是共享的这部分的细节放在第 5 章避坑里展开。3. 函数与参数传递默认参数陷阱、*args/**kwargs 与装饰器的连锁反应函数是这份笔记的第二个重头戏。这里面的坑都集中在“参数什么时候被求值”和“参数如何被组装”上。搞懂了这两点装饰器和闭包就能顺势理解因为它们本质上也是“函数对象被传参”。3.1 默认参数陷阱为什么不能用列表做默认值资料里给了一个非常经典的错误示例函数定义时用了可变对象L []作为默认参数。问题在于Python 函数默认参数在函数定义时只被求值一次之后每次调用都复用同一个列表对象。第一次调用往L里加了元素第二次调用看到的L已经是修改过的而不是当初定义时的空列表。# 错误写法默认参数指向可变对象 def add_item(item, L[]): L.append(item) return L print(add_item(1)) # [1] print(add_item(2)) # [1, 2]第二次调用拿到了上一次的残留数据 # 正确写法用 None 占位函数内部再创建新列表 def add_item(item, LNone): if L is None: L [] L.append(item) return L print(add_item(1)) # [1] print(add_item(2)) # [2]每次都是新列表第一个函数的两次输出分别是[1]和[1, 2]这个“记忆效应”对新手来说非常反直觉。第二个函数用None作为默认值因为None是不可变对象不会被修改每次调用进入函数体后都重新创建一个空列表行为完全可控。这条规则可以上升为一个通用的编码习惯默认参数只指向不可变对象如果业务上确实需要一个可变的默认容器用None占位再在函数内部初始化。3.2 *args 与 **kwargs定义时组装、调用时解包资料里把*args和**kwargs分成“函数定义时”和“函数调用时”两种场景这个区分特别关键。函数定义时*args收集所有未匹配的位置参数组装成一个元组传给args。**kwargs收集所有未匹配的关键字参数组装成一个字典传给kwargs。二者同时存在时*args必须放在**kwargs前面。def show(*args, **kwargs): print(args:, args) print(kwargs:, kwargs) show(1, 2, 3, namepy, score98) # args: (1, 2, 3) # kwargs: {name: py, score: 98}函数调用时*args是解包元组或列表把每个元素作为独立的位置参数传入。**kwargs是解包字典把每个键值对作为关键字参数传入。def calc(a, b, c): return a b c nums (1, 2, 3) print(calc(*nums)) # 6解包元组为三个位置参数 info {a: 10, b: 20, c: 30} print(calc(**info)) # 60解包字典为三个关键字参数注意看第二次调用字典的 key 恰好是函数参数名a、b、c如果 key 名字对不上会直接TypeError。这也是**kwargs解包最常见的报错来源。3.3 装饰器原理foo timeit(foo) 到语法糖资料里用计时器解释了装饰器的本质写一个timeit函数接收foo作为参数内部再定义一个包裹函数wrapper在调用foo前后计时然后返回wrapper。调用foo timeit(foo)后原来的foo名字指向了一个新函数这个新函数内部包装了旧函数。import time def timeit(func): def wrapper(*args, **kwargs): start time.time() result func(*args, **kwargs) end time.time() print(fcost: {end - start:.4f}s) return result return wrapper # 手动装饰 def foo(x): return x * 2 foo timeit(foo) print(foo(10)) # 等价语法糖 timeit def bar(x): return x * 2 print(bar(20))这里的核心是wrapper(*args, **kwargs)这一层透传wrapper接收任意参数原封不动地转交给原始函数func这样装饰后的函数才能保持原来的调用方式。timeit语法糖只是把foo timeit(foo)这一步换了个写法底层逻辑完全一致。复习时只要记住一句话装饰器就是“传一个函数进去返回一个新函数”。需要注意的是装饰器还涉及一个容易忽略的点被装饰后函数的__name__会变成wrapper如果要保留原函数的元信息需要用functools.wraps。期末复习不会考到这个深度但实习做项目时如果写 Flask 或 FastAPI 的路由装饰器这个细节迟早会遇到。4. 面向对象核心类属性、私有属性、C3 算法与 super 的钻石继承面向对象是这份笔记篇幅最大的一块也是最容易死记硬背的一块。重点其实只有四个类属性与实例属性的指向关系、私有属性的“假私有”实现、多继承的 MRO 顺序、以及super()在钻石继承里的作用。4.1 类属性与实例属性dict和class的指向关系资料里提到“最后一行的__dict__结果是实例调用__class__属性时会指向该实例对应的类然后可以再去调用其它类属性”。这句话的信息密度很高拆开看每个实例都有一个__dict__存放实例自己的属性。每个类也有一个__dict__存放类属性。实例可以通过self.__class__拿到自己的类对象然后访问类属性。class Song: title default # 类属性 def __init__(self, name): self.name name # 实例属性 s Song(hello) print(s.__dict__) # {name: hello}实例属性存在实例的字典里 print(Song.__dict__.get(title)) # default类属性存在类的字典里 print(s.__class__.__name__) # Song实例通过 __class__ 指向类更重要的是这一段“创建实例的时候如果实例中没有初始化实例的变量用类变量的值赋值给实例变量从此实例变量与类变量无瓜葛。”意思是当你通过实例读取一个不存在的属性时Python 会回溯到类属性但一旦你给实例的同名属性赋值相当于在实例的__dict__里新建了一个条目之后这个实例再也不会引用类属性了。class Dog: kind canine d Dog() print(d.kind) # canine来自类属性 d.kind pet # 实例属性新建覆盖了读取路径 print(Dog.kind) # canine类属性不受影响 print(d.__dict__) # {kind: pet}这个“赋值即隔离”的行为在读过源码前确实像玄学本质就是 Python 属性查找顺序实例__dict__→ 类__dict__→ 父类__dict__。记住了这条链路后面理解继承和多态就是水到渠成的事。4.2 私有属性两条下划线只是“改名”而不是“私有”Python 没有真正意义上的私有属性双下划线开头的__name会被解释器改名为_ClassName__name。这就是资料里说的“假定没有恶意行为Python 留出了类外访问私有属性和方法的方式_类名__属性名”。class PrivateDemo: def __init__(self): self.__secret hidden def __hidden_method(self): return method obj PrivateDemo() # 直接访问会 AttributeError # print(obj.__secret) # 通过改名规则访问 print(obj._PrivateDemo__secret) # hidden print(obj._PrivateDemo__hidden_method()) # method print(obj.__dict__) # {_PrivateDemo__secret: hidden}看到obj.__dict__的 key 是_PrivateDemo__secret就真相大白了。为什么说这是“防君子不防小人”因为改名规则是公开的任何人只要拼出_类名__属性名就能访问。期末复习只要记住一点在类内部用双下划线定义“约定私有”的属性类外访问一律走改名规则不要试图通过正常属性名直接访问。4.3 多继承与 MROC3 算法的两个原则Python 3 的多继承查找顺序由 C3 算法决定资料里给了口诀从子到父从左到右object永远是最后一个。具体执行时有两步剪枝从最左父类开始如果某个类是其他父类的子类就把它推迟到后面所有不是D直接父类的无关节点直接丢弃。以一个显式例子来看class A1: pass class A2: pass class A3: pass class B1(A1): pass class B2(A2): pass class B3(A3): pass class C1(B1): pass class C2(B2): pass class C3(B3): pass class D(C1, C2, C3): pass print(D.__mro__)输出大致为D - C1 - B1 - A1 - C2 - B2 - A2 - C3 - B3 - A3 - object。观察这条链从左到右的每一支都是先子类后父类同一层级的父类按继承顺序排列。这里最容易犯的错是把继承顺序写反比如class D(B1, B2, B3)和class D(B2, B1, B3)产生的 MRO 完全不同方法查找结果也就随之改变。class Animal: def speak(self): print(Animal speaks) class Dog(Animal): def speak(self): print(Dog barks) class Cat(Animal): def speak(self): print(Cat meows) class Pet(Dog, Cat): pass p Pet() p.speak() # Dog barks按 MRO 从左往右先找到 Dog.speak当Pet自己没有定义speak时Python 会按Pet - Dog - Cat - Animal - object的顺序查找。这个顺序和括号里的位置完全一致写在前面的父类有更高的优先级。这就是为什么资料里强调“继承顺序很重要”——同样的父子类关系只是换了一下括号里的顺序查到的可能就是另一个父类的方法。4.4 super() 与钻石继承避免多次初始化的关键钻石继承是多继承的经典陷阱如果D同时继承B和C而B、C都继承A用父类名去调用A.__init__()会导致 A 被初始化两次。这是因为直接调用类名方式不会走 MRO每个父类都会独立地再去初始化它的父类。class A: def __init__(self): print(A init) class B(A): def __init__(self): print(B init) A.__init__(self) class C(A): def __init__(self): print(C init) A.__init__(self) class D(B, C): def __init__(self): print(D init) B.__init__(self) C.__init__(self) d D() # 输出顺序 # D init # B init # A init # C init # A init - A 被初始化了两次改用super()后调用链变成严格按 MRO 走每个类在链上只初始化一次。class A: def __init__(self): print(A init) class B(A): def __init__(self): print(B init) super().__init__() class C(A): def __init__(self): print(C init) super().__init__() class D(B, C): def __init__(self): print(D init) super().__init__() d D()输出顺序会变成D init - B init - C init - A init。这里的super()并不是“调用父类”而是“按 MRO 顺序调用下一个类”。super()去掉了Medium1这类显式类名后代码在继承结构变化时依然能正确工作。期末复习遇到super()相关的题先画出 MRO再一条一条对照正确率会高很多。5. 避坑自查五条高频翻车记录与根因复盘期末复习阶段最常见的状态是“知识点都知道一写代码就报错”。以下五条是从资料内容里提炼出来的高频翻车点每条按现象、原因、解决三步拆解可以直接拿来当自查清单。5.1 现象默认参数被“记住”函数第二次调用结果不对定义函数def func(item, L[])连续两次调用后发现第二次结果包含了第一次的数据表现和闭包一样“有记忆”。原因默认参数在函数定义时求值L指向同一个列表对象调用时修改的是同一个对象的内存内容。解决默认参数一律指向不可变对象用None占位函数内部判断后创建新列表。5.2 现象list.copy()之后修改嵌套列表原列表也跟着变对二维列表进行copy()后修改新列表的某个子元素原列表的对应位置也被改动。原因copy()是浅拷贝只复制外层列表的指针数组内层列表仍然共享同一个对象引用。解决需要完全独立时用copy.deepcopy()。判断标准是“拷贝对象里只要有一个可变类型就建议用深拷贝”。对于数据量小的日常练习深拷贝的开销可以忽略不计。5.3 现象访问“私有属性”时拼了普通名字却报AttributeError原因双下划线开头的属性被 Python 改名为_类名__属性名普通名字在实例的__dict__里根本不存在。解决在类内部用双下划线定义私有属性类外部访问时用改名规则或者干脆不访问。这不是为了让你去偷数据而是让你知道“约定私有”是靠解释器改名实现的不是真正的权限控制。5.4 现象多继承时调用了错误的父类方法或者在钻石继承中父类被初始化两次原因括号中父类顺序决定了 MRO方法查找从左往右直接调用父类名.__init__绕过了 MRO导致钻石结构里同一个祖先被多次初始化。解决多继承时先打印ClassName.__mro__确认查找顺序初始化用super().__init__()而不是显式父类名让调用链按 MRO 依次执行。5.5 现象dict.pop(key)报KeyError而get(key)又永远不报错原因pop(key)需要显式删除指定键key 不存在时直接抛出异常get(key)的语义是“查询”不会修改字典键不存在时返回None。解决删除键时用pop(key)不确定键是否存在时先if key in d判断或使用pop(key, default)提供默认返回值。setdefault和update的区别同理一个是“缺失才写入”一个是“无条件覆盖”按业务场景选。6. 用魔法函数和深浅拷贝做期末自测一个可复现的验证流程复习资料的最后一部分集中在魔法函数、深浅拷贝和动态创建类上。这里的知识点不是背诵题而是可以通过“写小脚本验证”来巩固的。我复习时习惯把一个结论拆成三步读笔记里的结论 → 写一段不超过 15 行的代码 → 在解释器里跑通并对比输出。这套流程比反复看笔记高效得多。魔法函数里最值得手动跑的是__iter__和__getitem__因为两者存在“降级”关系。资料里说得很清楚如果一个类没有实现__iter__for循环会尝试用__getitem__按索引取值直到抛出IndexError才停止。这个机制可以用一段代码验证class NumList: def __init__(self, data): self.data data # 注释掉 __iter__ 再运行观察结果差异 def __iter__(self): return iter(self.data) def __getitem__(self, index): return self.data[index] obj NumList([10, 20, 30]) for item in obj: print(item)把__iter__注释掉后for循环依然能跑但走的是__getitem__的 0、1、2 下标路线。这个“先找迭代器找不到就退到下标”的规则比死记“实现__iter__才能迭代”更贴近解释器的真实行为。同理一个类没有__str__时会退到__repr__这也是可以用print(obj)直接验证的。深浅拷贝的验证也值得专门跑一遍。资料里的结论是最外层是可变类型时copy.copy()会创建新对象最外层是不可变类型时copy.copy()返回原对象但无论哪种浅拷贝内层可变对象仍然共享引用。想要一眼看清差异可以打印每层的 idimport copy nested [[1, 2], [3, 4]] shallow copy.copy(nested) deep copy.deepcopy(nested) print(id(nested[0]) id(shallow[0])) # True浅拷贝内层共享 print(id(nested[0]) id(deep[0])) # False深拷贝内层独立一句话总结深浅拷贝的判断标准单看外层浅拷贝对可变对象创建新引用、对不可变对象直接复用深拷贝则是不管有几层嵌套所有可变对象全部重建。期末做题时遇到“修改拷贝后的数据会不会影响原对象”先看修改发生在第几层再决定答案是“会”还是“不会”。把魔法函数和拷贝这两个话题放在最后是因为它们兼具“高频考点”和“代码验证友好”两个属性。我的期末复习习惯是每天固定抽两小时把笔记里的结论逐条转化成小脚本跑一遍跑不通的标记为薄弱点第二天优先补。这个方法陪我考过了 Python 和算法两门课从那以后每轮复习我都强制自己走一遍“读结论 → 写验证 → 看输出”的流程经验证明比单纯翻笔记管用得多。希望帮到你。本文还有配套的精品资源点击获取