简介:围绕Python面试高频考点整理的55道题及答案文档,面向准备技术面试的初中级开发者与转行学习者,可用于面试前自测、查漏补缺和梳理知识脉络。内容覆盖变量作用域、列表与集合去重求和、多层循环与条件筛选、装饰器执行顺序、try-except-finally、字典引用传递、os.walk目录遍历、正则match与search及贪婪匹配、文件与文件夹删除、logging应用场景、json序列化及datetime定制等典型题型,并给出答案与关键解释,尤其适合定位装饰器顺序、变量作用域与异常流程等高频失分点。整包仅含1个docx文件,约80KB,轻量易读,适合按知识点快速检索和碎片化复习。目前已有1108人学习下载。对于需要系统过一遍Python核心语法、数据结构、异常处理、文件操作与常用标准库的读者,这份题目集可作为面试前的浓缩练习清单,也可用于日常开发中的概念复盘。
1. 从一道去重求和题说起:Python面试题到底在考什么
很多人第一次看到「找出数组中没有重复的值的和」这种题,第一反应是——这不就是去重吗,一个 set 就完事了。真写下去才发现,set 会把所有重复元素都干掉,而题目要的是「只出现过一次的那些数」,出现过两次的得整个剔掉。一道不到十行的小题,就这么把「集合去重」和「按出现次数筛选」两件事拆开了。这也是 Python面试题 这类材料真正的价值所在:它不是让你背答案的题库,更像一张体检报告单,每道题对应一个知识指标,指标异常的地方,就是你理解的薄弱环节。这份 55 道的资料把语法、数据结构、函数、异常处理、面向对象、文件操作、装饰器这些高频考点切成了一道道小题,每道题背后都站着一个必须搞懂的概念。概念通了,题只是它的影子;概念不通,换个问法照样翻车。
2. 变量、作用域与可变对象:那些「看着对却跑错」的题
有些题拿给人看,十个人里有八个会说「这不就是 4 吗」,然后运行结果是报错。这类「看着对却跑错」的题,恰恰是面试里最值钱的部分——它暴露的不是语法记没记住,而是你对 Python 运行机制的理解停在哪一层。下面三道题分别对应三个不同的机制:名字的作用域、对象的可变性、函数作为一等公民。
2.1 为什么 a+=3 会直接报错:全局与局部的身份之争
原题是这样:外面有a = 1,函数体里写a += 3,然后调用函数。答案不是 4,是运行出错,具体报的是 UnboundLocalError。
打个比方。函数像一个独立房间,房间里有一排自己的储物柜。a += 3这个动作拆开是「先把 a 取出来,加 3,再放回去」。问题在于,Python 在编译函数体的时候会先扫一遍:只要函数里任何位置出现了对 a 的赋值动作(+=也算赋值),它就把 a 登记成本房间的本地物品,压根不去外面找。等到执行到a += 3要「取出」时,本地柜子还是空的,于是报错——「局部变量 a 在赋值前被引用了」。
所以要改全局变量,必须显式声明归属:
a = 1 def bar(): global a # 声明 a 引用的是全局作用域里的那个名字 a += 3 # 此时才是「读全局 + 写全局」 bar() print(a) # 4global本身不创建变量,它只是告诉解释器「这个名字别登记在本地」。如果全局压根没有 a,那么global a之后再赋值,会在全局新建一个。函数里只是读、不写的话,完全不需要 global,Python 会顺着作用域链一层层往外找:局部、外层函数(闭包)、全局、内置,这四层就是常说的 LEGB。闭包里要改外层函数的变量,则用nonlocal,语义和 global 对称。
提示:
a += 3对 int、str、tuple 这类不可变对象等价于「重新绑定一个新对象」;对列表写lst += [1]是原地扩展,但名字归属的判断规则完全一样,函数里只要出现lst += ...,lst 依然是本地名。
理解这一层之后,很多「玄学」现象就有了解释力:为什么有的变量莫名其妙变成局部的,为什么for循环里的变量能跑到循环外面用。都是同一个作用域规则在起作用。
2.2 字典传进函数为什么被改了:可变对象与引用的真相
原题:mydict = {'a': 1, 'b': 2},扔进函数里执行d['a'] = 0,函数返回 d,外面再给 mydict 加个c,最后打印出来是{'a': 0, 'b': 2, 'c': 2}。
很多人以为函数参数是「复制一份进去」,所以外面不该受影响。但字典是可变对象,函数拿到的不是字典的副本,而是指向同一个字典的引用。通俗地说,函数拿到的是一把家门钥匙的复制品——钥匙是复制的,门是同一扇。你进去把沙发挪了位置,主人回家当然看得到。想验证这一点,打印一下id(d) == id(mydict)就明白了。
真正容易混的是另一种写法:
def rebind(d): d = {'a': 0} # 这是把钥匙扔了,换了把新钥匙 return d def mutate(d): d['a'] = 0 # 这是拿原钥匙开门,改屋里的东西 data = {'a': 1, 'b': 2} rebind(data) print(data) # {'a': 1, 'b': 2} 没变 mutate(data) print(data) # {'a': 0, 'b': 2} 变了d = {...}是重新绑定,把形参这个局部名字指向了一个全新对象,跟外面那个字典断开了关系;d['a'] = 0是原地修改,改的是同一个对象的内容。Python 的参数传递常被叫作「传对象引用」,既不是纯值传递也不是纯引用传递——传的是引用的副本。这个说法听起来绕,但落到操作上就一句话:改内容会影响外面,换对象不会。
同样的道理可以解释为什么可变默认参数是个经典坑:def f(lst=[])里那个空列表只在函数定义时创建一次,之后所有调用共享它,第一次 append 进去的东西,第二次调用还在。要避开就写def f(lst=None),进函数再lst = lst or []。
2.3 装饰器为什么从下往上执行:函数也是对象
原题给了两个装饰器 decorator_a 和 decorator_b,叠在函数 f 上面,问调用 f(1) 的输出顺序。答案是先打印两个装饰器函数体里的内容(a 在前、b 在后),再打印两层 inner(b 在前、a 在后),最后是 f 本身。
这个顺序之所以让人懵,是因为它把两个不同时刻混在了一起:装饰发生的时刻,和调用发生的时刻。
@装饰器这行语法糖,等价于把下面被装饰的函数整个传给装饰器,用返回值替换掉原名。所以:
@decorator_b # 第二步:f = decorator_b(inner_a) @decorator_a # 第一步:inner_a = decorator_a(f) def f(x): ...离函数最近的装饰器先执行,这就是「从下往上装饰」。decorator_a 先被调用,它打印一句、返回 inner_a;接着 decorator_b 拿到 inner_a,打印一句、返回 inner_b;最终 f 这个名字指向 inner_b。注意,这两句 print 是在函数定义完成的那一刻就跑完的,之后你再调用多少次 f,都不会再看到它们。
调用阶段则相反。f(1) 实际调用的是最外层的 inner_b,它打印、然后调 inner_a,inner_a 打印、再调最初的 f,f 打印并返回结果。像一层层剥洋葱,从外往里。所以是「从上往下调用」。
import functools def decorator_a(func): print('Get in decorator_a') # 装饰阶段,只执行一次 @functools.wraps(func) # 保留原函数的 __name__、__doc__ def inner_a(*args, **kwargs): print('Get in inner_a') # 调用阶段,每次调用都执行 return func(*args, **kwargs) # 透传任意位置参数和关键字参数 return inner_a*args和**kwargs的作用是把调用方传进来的参数原样打包再拆开,这样装饰器不用关心被装饰函数到底几个参数。少了functools.wraps,被装饰后的函数名字会变成 inner_a,日志和调试时看不出原函数是谁,这个坑踩过一次就记住了。
3. 从列表到正则:容器、字符串与文件处理的考点地图
这一层考的是「手上的工具箱熟不熟」。列表、集合、字典是容器,正则和文件操作是跟外部世界打交道的手段,json 是数据出门前要换的衣服。题目看着零散,其实都在问同一件事:遇到一个具体任务,你知道该拿哪个工具、以及这个工具的边界在哪。
3.1 列表、集合与去重:为什么 set 更快却不一定够用
回到第 1 章那道题。第一种写法用两个列表,边遍历边判断「在不在已收集的列表里」,列表的in是逐个比对,元素一多就慢;第二种写法用 set 去重再配合 count 筛选,看着聪明,但 count 每次都要把整个列表扫一遍,外层再套一层,整体还是 O(n²)。
set 快的原因在于它底层是哈希表:存的时候把元素算成一个哈希值决定放哪个格子,查的时候直接算哈希去那个格子看,平均 O(1)。代价是它不保序、不能放不可哈希的东西(比如列表)。所以「去重后还要保持原顺序」这种需求,set 单独搞不定,得配合 dict(Python 3.7 起字典保插入顺序)或者手动记录。
| 结构 | 查找 | 保序 | 可重复 | 典型用途 |
|---|---|---|---|---|
| list | O(n) | 是 | 是 | 有序集合、频繁按下标访问 |
| set | O(1) | 否 | 否 | 去重、成员判断 |
| dict | O(1) | 是(3.7+) | 键不可重复 | 键值映射、计数 |
| Counter | O(1) | 是 | 计数 | 频次统计 |
真要把这道题写漂亮,标准库里的 Counter 是最省事的路子:
from collections import Counter def sum_unique(nums): cnt = Counter(nums) # 一次遍历完成频次统计 return sum(x for x, c in cnt.items() if c == 1) print(sum_unique([3, 4, 1, 2, 5, 6, 6, 5, 4, 3, 3])) # 1 + 2 = 3Counter是 dict 的子类,键是元素、值是出现次数,items()拿到的就是「元素—次数」对。整个函数一次遍历建表、一次遍历求和,O(n)。相比前两种写法,它把「统计」和「筛选」两件事分开了,读起来也更像人话。面试里能主动说出「这里可以换成 Counter」,通常比硬写循环加分。
3.2 match 与 search、贪婪与非贪婪:正则里的两对孪生概念
match 和 search 的差别只有一句话:match 只在字符串的第 0 个位置尝试匹配,search 会把整个字符串扫一遍找第一个匹配。原文那个邮箱例子正好踩在这个点上——待匹配的字符串前面带了一串空格,用 match 会直接返回 None,因为位置 0 是空格不是邮箱首字符。换成 search 才有结果。
import re text = ' 1403179190@example.com ' pattern = r'[a-zA-Z0-9_-]+@[0-9A-Za-z]+(\.[0-9a-zA-Z]+)+' print(re.match(pattern, text)) # None,因为开头是空格 print(re.search(pattern, text)) # 匹配到邮箱贪婪和非贪婪是另一对。正则里的量词(*、+、?、{m,n})默认是贪婪的,也就是在能满足整个表达式的前提下尽量多吞字符;在量词后面加一个?就变成非贪婪,能少吞就少吞。
html = '<div>a</div><div>b</div>' print(re.findall(r'<div>.*</div>', html)) # ['<div>a</div><div>b</div>'] print(re.findall(r'<div>.*?</div>', html)) # ['<div>a</div>', '<div>b</div>']贪婪版本会一路吃到最后一个</div>,非贪婪版本吃到第一个就收手。写爬虫解析 HTML 时这两种结果的差别是灾难性的,也是为什么正规做法是用解析库而不是正则硬怼。另外要留意,re.match从头匹配、re.fullmatch要求整串匹配、re.findall返回所有不重叠的匹配,三个函数名字像但行为差很远。
3.3 os.walk 与递归删目录:文件系统操作的两条路径
遍历目录有两种典型写法:os.walk自上而下生成三元组,os.listdir配合递归自己往下钻。前者是 Python 帮你把递归写好了,每次迭代给出(当前目录, 子目录名列表, 文件名列表);后者你要自己判断某个路径是不是目录,是就再调一次自己。
import os def walk_dir(root): for cur, dirs, files in os.walk(root): # cur 是当前层路径 for name in dirs: print(os.path.join(cur, name)) # 拼出子目录完整路径 for name in files: print(os.path.join(cur, name)) # 拼出文件完整路径 walk_dir('./demo_dir')os.walk默认自上而下,加了topdown=False就变成自下而上,删目录时常用这个参数——先删子再删父。拼接路径一定要用os.path.join而不是手写斜杠,因为 Windows 和类 Unix 的分隔符不一样,手拼迟早出事。
删除这块有三个函数,用途完全不同:
| 函数 | 删什么 | 注意 |
|---|---|---|
os.remove(path) | 单个文件 | 路径是目录会报错 |
os.removedirs(path) | 空目录,并一路向上删空的父目录 | 路径写错可能连带删掉上层空目录 |
shutil.rmtree(path) | 整棵目录树,非空也删 | 不可恢复,执行前务必确认路径 |
注意:
os.removedirs的行为是「删掉这个空目录后,如果父目录也空了就继续往上删」,很多人以为它只删一层。真要清空整棵目录树,用shutil.rmtree语义更明确。
3.4 json 序列化为什么遇 datetime 就翻车
json 这套格式只认几种类型:字符串、数字、布尔、null、数组、对象。Python 的 datetime、set、自定义类实例,它一个都不认识,直接 dumps 会抛 TypeError。
解决办法是告诉它「遇到不认识的类型怎么办」。最轻量的写法是给default参数传一个函数,复杂一点就继承 JSONEncoder 重写default方法:
import json from datetime import datetime from json import JSONEncoder class ComplexEncoder(JSONEncoder): def default(self, obj): if isinstance(obj, datetime): return obj.strftime('%Y-%m-%d %H:%M:%S') # 转成字符串 return super().default(obj) # 其它不认识的类型继续抛错 data = {'name': '示例', 'created': datetime(2024, 5, 18, 19, 52, 5)} print(json.dumps(data, cls=ComplexEncoder, ensure_ascii=False)) # {"name": "示例", "created": "2024-05-18 19:52:05"}ensure_ascii=False这个参数值得单独记一笔:默认是 True,中文会被转成\uXXXX转义序列,人眼没法读、传输体积也更大;设为 False 才原样输出中文。接口返回中文乱码,一大半是这个开关没关。super().default(obj)那行也别省,它保证未知类型仍然是明确报错,而不是被静默吞成字符串——把什么都转成 str 看着能跑,出了问题时排查起来是场血泪经验。
4. 面向对象那张网:继承、MRO 与鸭子类型
面向对象的题在面试里出现频率极高,但大部分人答得像背书:封装就是隐藏细节,继承就是子承父业,多态就是同一接口不同实现。这三句话没错,但面试官往下追一句「那 Python 的多态靠什么实现」,很多人就卡住了。这一章把这张网拆开:三大特性是结果,鸭子类型、MRO、名字改写是原因。
4.1 三大特性与鸭子类型:先分清「是什么」和「像什么」
封装是把数据和操作数据的方法捆在一起,对外只暴露必要的接口;继承是复用父类的属性和方法,避免重复写;多态是同一个调用在不同对象上产生不同行为。这三者里,多态最容易被误解成「必须靠继承实现」,而 Python 恰恰不是。
鸭子类型的说法是:走起来像鸭子、叫起来像鸭子,那它就是鸭子。判断一个对象能不能用,不看它继承了谁、实现了什么接口,只看它有没有需要的方法和属性。任何实现了__len__的对象都能被len()调用,任何实现了__iter__的对象都能被 for 遍历,不管它的类谱系长什么样。
class FakeList: def __init__(self, data): self._data = data def __len__(self): return len(self._data) print(len(FakeList([1, 2, 3]))) # 3,它压根没继承 list这就是动态类型语言的风格:接口是隐式的、靠约定,不靠声明。好处是灵活、不用为了满足某个抽象基类写一堆样板;坏处是类型错误要到运行时才暴露,所以大型项目里常配合类型注解和静态检查工具来兜底。
4.2 MRO 与 C3 算法:多继承时方法到底去哪儿找
单继承的查找顺序没什么好说的,一路往上。多继承就麻烦了:如果 B 和 C 都继承自 A,D 又同时继承 B 和 C,调用一个方法时到底先去谁家找?答案由 MRO(方法解析顺序)决定,Python 3 用 C3 线性化算法算出这个顺序。
C3 的核心约束有三条:子类一定排在父类前面;多个父类按声明顺序排;整条序列保持单调性(不能出现某个类在两个位置都出现)。算出来的结果可以直接看:
class A: pass class B(A): pass class C(A): pass class D(B, C): pass print(D.__mro__) # (<class 'D'>, <class 'B'>, <class 'C'>, <class 'A'>, <class 'object'>)搜索顺序就是 D → B → C → A → object。注意 B 在 C 前面,因为class D(B, C)里 B 写在前面。如果 C3 算不出合法序列(比如两个父类的继承顺序互相矛盾),定义类的时候直接抛 TypeError,不会等到运行时才炸。
顺着这个再理解 super() 就通了:super() 不是「调用父类的方法」,而是「沿当前实例的 MRO 找下一个」。在 D 的实例上,B 里的 super() 指向的是 C,不是 A。这个差别在多继承里体现得最明显,很多「明明调了 super 却没执行到」的问题都源于此。顺便说一句,Python 3 里所有类都是新式类,都继承自 object,老版本新式类、经典类并存带来的那些差异已经不存在了。
4.3 单下划线与双下划线:保护、私有与名字改写
单下划线开头的成员习惯上叫「保护成员」,意思是「这是内部用的,外面别碰」,但 Python 不会真的阻止你访问,它只是一个给读代码的人的约定。用from module import *导入模块时,单下划线开头的名字默认不会被带出来(除非模块里定义了__all__)。
双下划线开头的是另一套机制。Python 会对这类名字做「名字改写」:在类Foo里定义的__x,实际会被改成_Foo__x。所以外面写obj.__x访问不到,但写obj._Foo__x是能访问到的——它不是真私有,只是把名字藏了一下。
class Foo: def __init__(self): self.__x = 1 # 实际存成 _Foo__x f = Foo() print(f._Foo__x) # 1,绕一下还是能拿到名字改写还带来一个副作用:父类的__x在子类里写self.__x会变成_子类__x,两者根本不是同一个属性,所以子类没法直接覆盖父类的双下划线成员。这常被描述成「父类私有属性子类无法修改」,机制上是名字改写造成的,不是访问控制。真想做「只能读不能改」的属性,用 property 加只读实现更靠谱。
4.4 实例方法、类方法、静态方法:三种「方法」的边界
这三者的差别,看第一个参数和装饰器就够了:
| 类型 | 装饰器 | 第一个参数 | 调用方式 | 典型用途 |
|---|---|---|---|---|
| 实例方法 | 无 | self(实例) | 实例调用 | 操作实例数据 |
| 类方法 | @classmethod | cls(类) | 实例或类都能调 | 备选构造函数、操作类状态 |
| 静态方法 | @staticmethod | 无 | 实例或类都能调 | 纯工具函数,不碰类也不碰实例 |
class Circle: pi = 3.14159 def __init__(self, r): self.r = r def area(self): # 实例方法:要用 self.r return Circle.pi * self.r ** 2 @classmethod def from_diameter(cls, d): # 类方法:常用来做备选构造 return cls(d / 2) @staticmethod def is_valid(r): # 静态方法:跟类状态无关 return r > 0from_diameter用 cls 而不是写死 Circle,是为了子类继承时也能正确构造出子类实例——写成Circle(d/2)就把子类锁死了,这是继承场景下的常见失误。静态方法本质上是挂在类命名空间下的普通函数,既拿不到 self 也拿不到 cls,一旦你发现自己在里面想访问类属性,就该改成类方法了。
5. 易混概念辨析:面试题里最容易答偏的几个点
有些概念单独看都懂,放在一起就糊。这一章挑出五组最容易答偏的,每组按「哪里容易混 → 为什么会混 → 怎么区分」的顺序讲清楚,都是面试里被追问时最容易露馅的地方。
5.1 type 与 isinstance:为什么子类判断会翻车
易混点:两者都是判断对象类型的,写出来形式也差不多,很多人以为可以互换。
为什么会混:日常用的时候,判断一个对象是不是某个类型,两个都能给出答案,于是形成了「一样用」的错觉。差别只在遇到继承关系时才暴露。
怎么区分:type 返回对象的精确类型,只认「恰好是这一类」,不认子类;isinstance 会顺着继承链往上查,只要祖先里有这个类就算命中。原题那个例子很典型:B 继承自 A,ba = B()之后,type(ba) == A是 False,而isinstance(ba, A)是 True。
class A: pass class B(A): pass obj = B() print(type(obj) is A) # False,精确类型是 B print(isinstance(obj, A)) # True,B 是 A 的子类所以判断「是不是某类或它的子类」一律用 isinstance。它第二个参数还能传元组,一次判断多种类型:isinstance(x, (int, float))。反过来,如果你确实只想在「类型完全相同」时才放行(比如序列化分发),才用 type。另外 type 还有个冷门用法是动态创建类,type('Foo', (), {})能造出一个新类,但这已经属于元编程范畴,面试里问得不多。
5.2 实例变量与类变量:共享与独享的一线之隔
易混点:类里定义的所有变量,好像都能通过实例访问,看起来没区别。
为什么会混:读的时候确实没区别。self.x找不到会去类里找,所以实例能读到类变量的值,这个「向上查找」的行为掩盖了两者的存储位置不同。
怎么区分:看赋值动作发生的位置。self.x = ...创建的是实例自己的属性,只属于这一个实例;类名.x = ...或cls.x = ...改的才是类属性,所有实例都能看到。
class Config: items = [] # 类变量,所有实例共享同一个列表 a = Config() b = Config() a.items.append(1) # 原地修改,改的是共享的那个列表 print(b.items) # [1],b 也看到了 a.items = [9] # 重新绑定,a 有了自己的实例属性 print(b.items) # [1],b 不受影响这段代码把「共享」和「独享」的分界线画得很清楚:append 是改共享对象的内容,赋值是给当前实例新建一个同名属性,两者从此分道扬镳。可变类变量是最容易出事的地方,多个实例互相污染数据,排查起来像黑匣子;要共享状态就用类变量配合明确的修改方法,要独享就在__init__里初始化。
5.3 栈与队列、单例模式:写法多不等于都对
易混点:同一个功能有好几种实现,代码都能跑通,于是被认为等价。
为什么会混:面试题常问「尽可能多的方法」,答出几种就以为过关,忽略了不同写法在效率和正确性上的鸿沟。
怎么区分:拿栈来说,用列表尾部 append/pop 是 O(1),而用两个队列倒腾的实现,每次 pop 都要把一个队列的元素搬到另一个队列,复杂度是 O(n)。原文那道「两个队列实现栈」的题,注释里特意写了一句「队列的这种方法效率低」,就是在提示这层差别。再比如用列表模拟队列,如果出队写pop(0),每次都要把后面所有元素前移一位,元素一多就是灾难;正确做法是collections.deque,两端操作都是 O(1)。
单例模式同理。用__new__加锁是一种,用元类重写__call__是另一种。前者管的是实例创建,后者管的是「实例化整个流程」,包括创建和初始化。如果只在__new__里做拦截,__init__还是会在每次调用时执行一遍,有副作用的初始化就会重复跑。多线程场景下还得考虑双重检查加锁,不然两个线程同时判断「还没创建」就都进去了。能跑通和正确,中间隔着的是边界条件。
5.4 可调用对象与上下文管理器:加括号和 with 背后是什么
易混点:以为只有函数才能加括号调用,以为 with 只能配合文件用。
为什么会混:平时见到的可调用对象几乎都是函数,见到的 with 几乎都跟着 open(),样本太单一。
怎么区分:判断能不能调用用callable(),而它背后检查的是类型有没有实现__call__。可调用对象一共七类:用户自定义函数、内置函数、内置方法、类中定义的方法、类本身(调用类就是实例化)、实现了__call__的实例、生成器函数。最后这一类最反直觉——一个类的实例只要定义了__call__,obj()就是合法调用,框架里拿它做配置化回调很常见。
with 的门槛则是「上下文管理协议」:对象实现了__enter__和__exit__两个方法,就能被 with 管理。进入 with 块时调__enter__,不管块里是正常结束还是抛异常,退出时一定调__exit__,异常信息会作为参数传进去。这就是为什么用 with 打开文件不用手写 close——清理动作被放进了__exit__,想漏都漏不掉。自己写一个能配合 with 的类,也就是补上这两个方法的事。
5.5 双下划线方法与名字改写的交叉地带
易混点:__init__、__new__、__call__、__str__这些双下划线方法,和 4.3 讲的「双下划线私有属性」是不是一回事。
为什么会混:都是双下划线开头,看起来像同一套规则。实际上完全是两码事。特殊方法(也叫魔术方法)是 Python 协议的一部分,双下划线是固定命名约定,解释器在特定语法下会自动去调它们,比如 print 一个对象会找__str__,len()会找__len__,对象加括号会找__call__。而 4.3 讲的__x是用户自己起的名字,触发的是名字改写。
怎么区分:记住「解释器会不会主动来找你」。__init__负责初始化实例,__new__负责创建实例(可以返回别的对象),__call__让实例可调用,__str__决定 print 出来的样子,__getattr__在属性找不到时兜底,__setattr__在设置属性时触发。这些名字不能被随便改,改了协议就失效;而你自己写的__private只是被改了名,跟协议无关。顺带提一句,__doc__是类的注释,不会被继承,这个细节很多资料没写清楚。
6. 什么时候该刷这类题:一份使用边界清单
说了这么多概念,最后落到实际判断:这 55 道题到底该在什么阶段用、什么阶段别碰。
先说该用的时候。如果你已经写过一些 Python 代码,但总觉得知识是散的——知道装饰器怎么写,说不出它什么时候执行;知道 super() 能调父类,说不清多继承下的查找顺序——那这份按题组织的材料正好当索引。每道题是一个入口,卡住的地方顺着往下挖一层机制,比从头读一遍语法书效率高得多。准备面试前两三周用它来自测也合适:遮住答案先手写,写完对照,错的那道就是你的薄弱点,比刷一百道会的题有价值。
再说别用的时候。如果你完全没写过 Python,想靠这 55 道题入门,大概率会变成一个背答案的人——能说出「装饰器从下往上执行」,但写不出一个能用的装饰器。这类材料是查漏的,不是打地基的,地基得靠动手写小项目。另一种情况是把它当面试预测题背:题目会变,机制不会变,背答案的人换个问法就露馅,理解机制的人没见过的题也能推出来。
| 场景 | 适不适合 | 原因 |
|---|---|---|
| 有基础,想系统查漏 | 适合 | 题目按知识点分布,能快速定位盲区 |
| 面试前两周自测 | 适合 | 手写对照,比只看答案有效 |
| 零基础入门 | 不适合 | 缺少循序渐进的引导,容易变成背题 |
| 背答案应付面试 | 不适合 | 换个问法就失效,追问机制必露馅 |
| 当日常速查手册 | 适合 | 遇到模糊概念时按题定位,比翻文档快 |
具体怎么用,我自己的习惯是这样的:第一遍只挑那些「一眼觉得简单」的题做,因为翻车往往就翻在这种题上——第 2 章那道a += 3就是典型,看着是加法,实际考作用域。做完把错题按机制归类,同一类错两次的,说明不是记性问题,是理解断层,得回去把那一章补上。第二遍隔一周再做,专门做第一遍错过的,能一遍过才算真懂。
从那以后我每次遇到「这题我会」的瞬间,都会强迫自己先合上答案手写一遍,再解释一遍为什么这样写而不是那样写。讲不清楚的,就是没懂。这份 55 道的整理适合放在手边当索引,哪道题卡住就翻到对应那道,顺着它把背后的机制捋一遍,比一次性刷完记得牢。希望帮到你。
本文还有配套的精品资源,点击获取