简介:南开大学软件学院Python期末复习资料是一份精准覆盖核心考点的完整笔记,面向软件学院本科生及Python进阶学习者,系统梳理了数据结构、对象与引用、内存管理、面向对象编程等期末考试重点。压缩包内含1个docx文档(5.28MB),内容按知识点分块排列,方便快速定位与背诵。目前已有1199人学习,是经过验证的高热度复习资料。文档详细列出了字典、集合、元组、列表的异同与操作技巧,包括切片写法、字典合并方法;阐明了浅拷贝与深拷贝的区别及不可变对象场景;对函数默认参数陷阱、*args与**kwargs、命名关键字参数做了示例说明;还覆盖了魔法函数、装饰器、生成器、抽象类、正则表达式、三器一魔等进阶主题。特别是C3算法、多继承MRO顺序、钻石继承、类方法和静态方法、私有属性访问等易混淆点,均有图解式讲解,适合考前集中突击与反复查阅。
1. Python 期末复习到底在考什么:一份能当黑匣子用的全量笔记
某高校软件学院的这份 Python 期末复习笔记,最让我意外的是它没有按章节平铺,而是把 C3 算法、魔法函数、可变默认参数这些最容易翻车的点全用代码例子顶了出来。整份笔记几乎没有废话,每一行都是可以直接丢进解释器验证的片段。它适合两类人:一类是刚学完一遍但还没做题的新手,靠它把知识点连成网;另一类是已经刷过题的熟手,拿它做考前最后的边界核对。我把它拆开讲一遍,重点放在“笔记里写了什么、为什么要这么写、考场上会怎么变着法子考”。
2. 数据结构操作边界:切片方向、setdefault、pop 与内存共享
2.1 列表与字符串切片:方向决定结果
笔记里第一段高频考点就是切片。它给了一个字符串str = "lawedf",然后让你看两个切片结果:
str = "lawedf" print(str[-3:-1]) # 输出 ed print(str[-1:-4:-1]) # 输出 fde第一个str[-3:-1]是从倒数第 3 个字符取到倒数第 1 个字符之前。"lawedf"的倒数第 3 位是e,倒数第 2 位是d,倒数第 1 位是f但不包含,所以结果是ed。第二个str[-1:-4:-1]是反向切片,从末尾f开始往左走,走到倒数第 4 位之前,步长是 -1,于是取了f、d、e三个字符,输出fde。
这里要记住两个规则:切片区间永远是左闭右开,不包含结束位;步长为负数时,起始位置必须在结束位置的右边,否则取出来是空字符串。考试里最常见的变体是把步长换成正数然后让你猜会不会越界,或者反过来写str[-1:-4]不带步长,这时候由于起始位在结束位左边,结果一定是空的。遇到这类题先画下标,再下结论。
2.2 字典的 setdefault、update、pop 与 popitem:方法选型别混
笔记里字典部分信息密度很高,几乎每个方法都是一道判断题素材。先看生成字典:
dict1 = dict.fromkeys(list, 10)fromkeys的语义是用一个序列里的元素当键,全部给同一个默认值。注意这里如果键是可变对象直接会报类型错误,因为字典的键必须是可哈希的。
再往下看几个容易混的方法:
d = {5: 2} d.setdefault(3, None) print(d) # {5: 2, 3: None} d1 = {'a': 1} d1.update(d) # 用 d 的内容合并进 d1 print(d1) # {'a': 1, 5: 2, 3: None}setdefault(key, default)的行为是:如果 key 不存在,就把 key 和 default 加进去;如果 key 已经存在,啥也不改。这里有个细节很多新手会判断错——setdefault是“会写入”的,而get(key, default)只是“读的时候给个默认值”,不会往字典里写入新键。笔记里特意强调“get 若是没这个 key 是不会往 dict 里加没有的那个元素的”,这句话就是考点。
删除相关的也要分清楚:
del d[key]是彻底把键值对从字典里移除。d.clear()是清空字典,字典对象还在,只是里面没内容了。d.pop(key)必须传 key,删掉指定键并返回它的值。d.popitem()不传参,删掉字典的“最后一项”。
列表的pop()可以无参删除最后一个元素,但字典的pop()不行,这就是笔记里强调“key 不能不给出”的原因。
2.3 元组、集合与空集合:不可变与无序的边界
笔记里有一个很经典的表述:字典和集合是无序的,因为它们没法用下标索引;元组、数字、字符串是不可变的;集合中不允许出现重复元素。这些话看起来简单,但组合起来能出很多判断对错的题。
t = (1,) # 这是元组,必须带逗号 empty_set = set() # 正确创建空集合 empty_dict = {} # 这是空字典,不是空集合创建只有一个元素的元组时要加逗号,不加逗号(1)就只是数字 1 外面套了一层括号,类型是 int。创建空集合只能用set(),用{}默认建出来的是空字典。这是试卷上出现率极高的一个坑。
集合用add()加元素,元素不能重复,而且集合本身是无序的,所以 1 和 True 在集合里会被当成同一个元素——因为1 == True且它们的哈希值相同。这个点如果笔记里没有展开,考场上遇到判断题就得自己补上。
还有列表与字典的对比,笔记里给了三条:字典的查找和搜索速度不会随 key 数量增加而变化,查找速度很快,但字典占用内存更多。原因是字典底层是哈希表,理想情况下查找是 O(1),而列表是顺序扫描,元素多了以后速度明显变慢。考试问你“为什么不用列表当字典用”或者“字典为什么快”,就答这三条。
2.4 内存共享:列表存的是引用不是值
笔记里有个图和一句话:列表不直接存具体的值在内存里,只存对象的 ID;两个变量指向同一个列表时,它们的 ID 一样,改其中一个另一个也跟着变。
a = [1, 2, 3] b = a # b 和 a 指向同一个列表对象 b.append(4) print(a) # [1, 2, 3, 4]这解释了为什么“直接等于”不是拷贝。同样道理,元组里的元素如果是列表,元组本身不可变,但列表里的内容照样可以被修改。面试和期末都喜欢拿这个出“光看代码猜输出”的题。
3. 函数进阶:默认参数陷阱与 *args/**kwargs 的解包细节
3.1 可变默认参数是经典翻车点
笔记里花了大篇幅讲默认参数,原文说得很直白:L 是可变参数,第一次调用后默认值的内容被改变了,下次再调用拿到的就不是函数定义时的[]了。看这段最经典的错误用法:
def add_item(item, L=[]): L.append(item) return L print(add_item(1)) # [1] print(add_item(2)) # [1, 2],你以为是 [2]原因是L=[]这个默认值在函数定义时只创建一次,之后所有不传 L 的调用都共享同一个列表。正确的做法是用不可变对象占位:
def add_item(item, L=None): if L is None: L = [] L.append(item) return L记住笔记里那句话:默认参数必须指向不变对象。None、数字、字符串、元组都可以当默认参数,列表、字典、集合都不行。考试变体通常是让你判断“第二次调用时打印什么”,或者把换成L=()和L=set()让你分析。
3.2 *args 与 **kwargs:定义侧与调用侧的两种形态
笔记把*args和**kwargs拆成了定义和调用两层来讲,这个方法在考场上特别有用。
定义侧:
def collect(*args, **kwargs): # args 会组装成一个元组 # kwargs 会组装成一个字典 print(args) print(kwargs) collect(1, 2, 3, name="py", level=3) # (1, 2, 3) # {'name': 'py', 'level': 3}调用侧:
def add(a, b, c): return a + b + c t = (1, 2, 3) print(add(*t)) # 解包元组,等价于 add(1, 2, 3) d = {'a': 1, 'b': 2, 'c': 3} print(add(**d)) # 解包字典,等价于 add(a=1, b=2, c=3)整理成一句话:函数定义时,*args收集多余的位置参数成元组,**kwargs收集多余的关键字参数成字典;函数调用时,*把元组/列表拆开作为位置参数传入,**把字典拆开作为关键字参数传入。两者同时存在时必须*args在前、**kwargs在后,否则语法直接报错。
还有一个实用技巧是配合字典调用函数。笔记里提到“字典里还可以调用函数”,指的是可以用字典把一组函数存起来,再通过键去取:
def add(a, b): return a + b def mul(a, b): return a * b funcs = {'add': add, 'mul': mul} print(funcs['add'](2, 3)) # 5 print(funcs['mul'](2, 3)) # 6这在写菜单程序、命令分发的时候非常常见,期末填空填“字典的 value 可以存函数引用”的概率不低。
3.3 多返回值其实是一个元组
笔记里有一句总结:函数有且只有一个返回值。Python 里写return a, b看着像返回两个值,实际上是把它们打包成了一个元组返回。所以:
def split_point(): return 1, 2 result = split_point() print(type(result)) # <class 'tuple'>这个点在解包赋值里也有体现:x, y = split_point()本质上是用元组解包把两个元素分别赋给两个变量。写代码的时候没问题,但判断题如果问“函数能不能返回多个值”,答案是“不能,只能返回一个元组”。
3.4 装饰器:一个返回函数的函数
笔记里正好有一个计时器例子,也是装饰器的入门套路:
import time def timeit(func): def wrapper(*args, **kwargs): start = time.time() result = func(*args, **kwargs) print(func.__name__, time.time() - start) return result return wrapper def foo(): time.sleep(0.1) return "done" foo = timeit(foo) print(foo())这段代码结合了上一节的*args和**kwargs:wrapper内部用它们原样转发参数,这样计时器就能装饰任何签名的函数。装饰器本质上是“给函数 return 一个封装好了的新函数”,@timeit语法糖等价于手动执行foo = timeit(foo)。
笔记里还提到类方法和静态方法。@classmethod装饰的第一个形参是类对象,习惯叫cls,通常用来访问类属性;@staticmethod不需要传实例也不需要传类。这两个和@property一样,本质都是装饰器,只是作用目标不同。考试常见考法就是给你一段类代码,问哪个方法可以用类直接调用、哪个方法能拿实例调用、谁不能访问实例属性。
4. 多重继承与 MRO:C3 算法不玄学,手算一遍就懂
4.1 Python 3 只剩 C3:经典算法已成往事
笔记里列了三条 MRO 算法:经典方式、Python 2.2 新式算法、Python 2.3 新式算法(C3)。Python 3 里只保留最后一种,也就是 C3 算法。考场上只要题目用的是 Python 3,所有多继承的方法查找顺序都由 C3 决定,不需要再考虑旧算法。
C3 的核心目标是解决两个问题:一是子类永远在父类之前,二是同一个父类在 MRO 里只出现一次。Python 官方库里的大量混入类(mixin)设计都依赖这个顺序,如果顺序错了,同名方法被谁覆盖就成了玄学。
4.2 手算 C3:两条原则加一个消去步骤
笔记给了一串继承遍历结果:D、C1、C2、B1、A1、C3、B2、B3、A2、A3、Obj。这不是随便排出来的,手算时记住两条原则:
- 从子类开始,优先走左边的父类分支;
- 当一个类可以通过多个路径到达时,只保留一次,并且在它依赖的子类全部输出后才能输出。
实际操作可以这样:先写出从 D 出发按“深度优先、从左到右”遍历出来的顺序,然后从头到尾扫描,遇到重复出现的类就把它后面那次删掉。笔记里的“一旦左边有没有子节点的就继续剪掉这个节点”,说的就是深度优先走到头的过程。Obj 永远在最后,因为它是所有新式类的根。
验证手算结果有个更直接的手段,打开解释器直接看类名.__mro__:
class A1: pass class B1(A1): pass class C1(B1): pass class A2: pass class B2(A2): pass class C2(B2): pass class D(C1, C2): pass print(D.__mro__)输出的元组顺序就是最权威的答案。考试时如果题目允许,先在脑内还原继承关系图,再按“从左分支往右分支走、重复的类第一次出现后就不再出现”去排,比硬记公式稳。
4.3 super() 在钻石继承里只走一次
笔记里有一个重要的点:多继承里如果直接用父类名字调__init__,会出现多次初始化;用super().__init__()则按 MRO 顺序查找,一个类只被初始化一次。
class A: def __init__(self): print("A") class M1(A): def __init__(self): print("M1") super().__init__() class M2(A): def __init__(self): print("M2") super().__init__() class D(M1, M2): def __init__(self): print("D") super().__init__() d = D()这段代码会依次打印 D、M1、M2、A,A 只被初始化一次。如果改成M1.__init__(self)和M2.__init__(self)那种写法,A 的__init__会被执行两次。期末考这道题的概率很大,因为它考的既是 C3 顺序,又是 super 的实现机制。
4.4 继承顺序写反的翻车场景
笔记里举了一个继承顺序写反导致方法找不到的例子。父类 Animal 更高级,派生类应该把更基础的类写在左侧。比如class TracDog(Dog, Animal)这种写法如果导致 Animal 的方法优先级低于 Dog 的同名方法,而预期是 Animal 的版本生效,输出就会和设计意图相反。
实际排查时先看__mro__,再定位同名方法定义在哪一层。如果子类没有覆盖某个方法,Python 从左到右找第一个能用的,这个顺序一旦被误解,代码行为就像黑匣子。所以遇到多继承第一件事就是把类名.__mro__打印出来看一眼,比猜快得多。
5. 魔法函数与高频踩坑:迭代器、with、私有属性这样排查
5.1iter与getitem:for 循环的两种协议
笔记里说得很清楚:如果一个类要被for ... in循环使用,就必须实现__iter__(),该方法返回一个迭代对象;Python 的 for 循环不断调用迭代对象的next()方法取值,直到遇到StopIteration退出。
还有一个冷门知识点:如果拿不到迭代器,也就是没实现__iter__,Python 解释器会回头找__getitem__方法,直到整个对象取完。这个特性让某些老式写法也能被 for 遍历:
class SimpleList: def __init__(self, items): self.items = items def __getitem__(self, index): return self.items[index] s = SimpleList([10, 20, 30]) for value in s: print(value)这段代码没有__iter__,但 for 循环能跑,因为解释器发现可以按下标 0、1、2 顺序取,越界抛出IndexError后自动结束。题型常考“以下类能不能被 for 遍历”,答案就是看有没有__iter__,没有就看有没有__getitem__。
5.2enter与exit:with 背后的两句话
笔记里用“看起来充满魔法,但不仅仅是魔法”来形容 with。它的机制其实只有两条:紧跟 with 的对象必须提供__enter__()和__exit__()方法;进入时__enter__()的返回值赋给 as 后面的变量;代码块执行完后自动调用__exit__(),即使出异常也会调用。
class ManagedFile: def __enter__(self): print("open") return self def __exit__(self, exc_type, exc_val, exc_tb): print("close") with ManagedFile() as f: print("working")输出顺序是 open、working、close。期末如果考 with,多半是问两件事:as 后面的变量来自哪个方法的返回值,以及代码块抛异常后__exit__是否还会执行。答案是“来自__enter__的返回值”和“会执行”。
5.3str与repr:print 先调哪个
笔记里说 print 函数会先调用str()去转字符串,而str()会调用对象类里的__str__方法;如果类里没实现__str__,系统会找__repr__来模拟。所以两个方法都写时优先走__str__,只写__repr__也能让 print 正常工作。调试的时候,类里至少写一个__repr__是划算的,这样不管 print 还是直接看交互式输出都能看到可读信息。
5.4 私有属性不是真私有:name mangling 边界
笔记里有一句话很实在:Python 假定没有恶意行为,但留出了类外访问私有属性和方法的方式。双下划线开头的属性会被解释器改名成_类名__属性名,外部用这个改过的名字照样能访问。
class Person: def __init__(self): self.__age = 20 p = Person() print(p._Person__age) # 20期末考私有属性,最常见的题就是p.__age能不能访问、报什么错。答案是能访问但需要写成p._Person__age。这种设计不是为了阻止访问,只是防止无意覆盖。
5.5 深浅拷贝的两个最大误解
笔记里对比了直接赋值、浅拷贝、深拷贝三种情况。直接等号不产生新对象,两个变量 ID 一样;浅拷贝创建新的最外层对象,但里面的嵌套对象还是引用原来的;深拷贝连同嵌套对象全部复制。
import copy a = [[1, 2], [3, 4]] b = copy.copy(a) # 浅拷贝 c = copy.deepcopy(a) # 深拷贝 print(a[0] is b[0]) # True,嵌套列表还是同一个 print(a[0] is c[0]) # False,嵌套列表也被复制了这里还有个容易忘的结论:当最外层对象是不可变类型时,浅拷贝得到的对象指向原对象的内存空间;只有最外层是可变类型时,浅拷贝才会生成新对象。判断深浅拷贝的题,先看最外层是否可变,再看嵌套内容是否为可变,两步就可以锁定答案。
5.6 三条高频翻车记录
第一条:想创建空集合,写了s = {},结果得到的是空字典。现象是后续调用s.add(1)直接报 AttributeError。原因是{}在 Python 里永远表示空字典。解决:空集合必须用set()。
第二条:函数里用了可变默认参数L=[],第二次调用时第一次的数据还在。现象是add_item(1)返回[1],add_item(2)返回[1, 2]。原因是默认参数在定义时只创建一次,后续调用共享同一对象。解决:默认参数写成None,函数内部再初始化。
第三条:浅拷贝后修改嵌套列表,原列表跟着变。现象是改了b[0].append(99),a里也出现了 99。原因是浅拷贝只复制了最外层的引用结构,嵌套列表没有复制。解决:需要完全独立的数据时用copy.deepcopy(),考试写代码题时一定要看清题目要求的是“值独立”还是“浅层独立”。
6. 期末当天的 90 分钟自查:把这份笔记变成可执行清单
最后讲一个我一直在用的复习流程:拿到一份知识点密集的笔记,不要从头再读一遍,而是“看结论、反推例子、构造变体”。具体分三步走。
第一步,挑出笔记里所有带代码片段的知识点,把它们当作判断题:遮住输出结果,自己在解释器里跑一遍,跑之前先写下预测答案。切片方向、setdefault是否写入、默认参数累积、__mro__顺序、浅拷贝的嵌套 id,这些全部是适合预测的考点。预测错了就用红笔标住,因为“看懂了”和“能预测对”是两回事。
第二步,对每个标住的知识点构造一个变体。比如原题是str[-3:-1],变体就把步长改成 -1,再改成 2,看输出变不变;原题是D(C1, C2)多继承,变体就把父类顺序对调,打印__mro__对比差异。这一步花的时间最多,但考试里最拿分的就是这些“看着眼熟、稍微一变就不会”的题。
第三步,考前一个小时只做一件事:把每章的翻车记录过一遍。集合用set()创建、默认参数不用可变对象、私有属性改名规则、with块的__exit__一定执行、浅拷贝不嵌套、super()按 MRO 走一次——每一条对着代码快速盯一遍,相当于给大脑做一层索引。
我自己的血泪教训是:有一年复习到深夜,把所有代码都跑了三遍,觉得稳了,结果第二天一道装饰器叠加的题直接把我打回原形。问题就出在我只验证了装饰器本身,从没想过多个装饰器叠加时执行顺序会反转。从那以后我每次都强制走一遍“预测 → 变体 → 翻车记录”的三轮自查,再也没在期末翻过车。希望帮到你。
本文还有配套的精品资源,点击获取