17 个短代码,检验你 Python 基本功
很多同学跟我说:自己刷了不少 Python 题,真到写业务的时候也都能跑,但是一到面试,面试官丢几行小代码,让你说输出是啥,立刻就慌了。其实这些“几行小代码”,就是在检查你对基础细节有没有真的吃透。
我就按平时带新人那套,把常考的点拆成 17 段非常短的小代码,每一段都围着一个“基本功”展开。你可以先自己在脑子里算一算输出,再看解释,效果会非常好。
下面直接上代码,我会按难度慢慢往上走,不用刻意记,理解就行。
【1】变量和引用:到底改的是谁?
x = [1, 2, 3]
y = x
x.append(4)print("x:", x)
print("y:", y)
大部分人都知道会一起变成 [1, 2, 3, 4],但真正理解原因的人反而不多。
这里 x 和 y 指向的是同一块列表对象的内存地址,append 是“在原地修改”(in-place),所以不管你拿 x 还是拿 y,看到的都是同一个列表。
小结一句:给可变对象起第二个名字,不是复制,是俩人共用一份。
【2】切片拷贝:浅拷贝真的“拷”了吗?
nums = [[0], [1]]
a = nums[:] # 浅拷贝
nums[0].append(99)print("nums:", nums)
print("a :", a)
很多人以为切片就“完全复制”了,其实这里的输出是:
nums: [[0, 99], [1]]
a : [[0, 99], [1]]
原因在于:nums[:] 只复制了“最外层列表”,里面两个小列表还是同一份引用。所以改动 nums[0] 里面的内容,a[0] 也会看到。
如果你想完全复制到最深层,可以用 copy.deepcopy,不过那又是另外一个话题了。
【3】默认参数的坑:为什么一直在“记仇”?
defadd_item(item, container=[]):
container.append(item)
return containerprint(add_item(1))
print(add_item(2))
print(add_item(3))
第一次看的人,经常以为输出是 [1]、[2]、[3],结果真相是:
[1]
[1, 2]
[1, 3]
(注意,后面两次是在前面的基础上继续加)
真正的关键点是:函数定义时,默认参数的列表只创建一次,并一直复用。所以每次不传 container,都是在往同一份列表上 append。
正确写法通常是这样:
defadd_item(item, container=None):
if container isNone:
container = []
container.append(item)
return container
这个坑在面试题里出现频率非常高,属于必背级别。
【4】排序:sort 和 sorted 到底差在哪?
nums = [3, 1, 2]r1 = nums.sort()
print("r1:", r1)
print("nums after sort:", nums)
r2 = sorted(nums)
print("r2:", r2)
print("nums after sorted:", nums)
实际输出会是:
r1: None
nums after sort: [1, 2, 3]
r2: [1, 2, 3]
nums after sorted: [1, 2, 3]
要记住两点:
list.sort()在原地修改列表,**返回值是None**。sorted(iterable)会返回一个新的列表,不会改原对象。
所以如果你写出 nums = nums.sort(),那 nums 之后就变成 None 了,这是很多人真实踩过的坑。
【5】for…else:这个 else 不是真的“否则”
nums = [1, 3, 5, 8]for n in nums:
if n % 2 == 0:
print("found even:", n)
break
else:
print("no even number")
Python 里 for 的 else 很反直觉:只有 for 循环“正常走完”(没有被 break 掉)才会执行 else。
这段代码因为遇到 8 会 break,所以输出只有:
found even: 8
如果你把 8 删掉,那就会变成:
no even number
在实际代码里,它非常适合用来“没 break 到,就走兜底逻辑”的场景。
【6】条件表达式的写法:一行里也能 if-else
labels = ["odd"if n % 2else"even"for n in range(5)]
print(labels)
range(5) 是 0~4,这段会输出:
['even', 'odd', 'even', 'odd', 'even']
这里有两个要点:
列表推导式本身是个表达式。 Python 里可以写 a if condition else b,整个也是个表达式。
组合在一起,就可以写出既简洁又可读的代码,比套一堆 if/else 看着舒服很多。
【7】any / all:一眼看出“有没有”和“是不是都”
flags = [0, "", None, "ok"]print(any(flags))
print(all(flags))
any 和 all 是两个特别实用的小函数:
any(iterable):只要有一个元素为真,就返回真。all(iterable):所有元素都为真,才返回真。
这里 0、空字符串 ""、None 都是“假”,只有 "ok" 是“真”,所以输出:
True
False
在做校验、早退判断的时候,这俩经常能把一长串 if 简化掉。
【8】zip 和“解压”:配对再拆开
names = ["Alice", "Bob", "Cindy"]
scores = [85, 92, 88]pairs = list(zip(names, scores))
print("pairs:", pairs)
unzipped_names, unzipped_scores = zip(*pairs)
print("unzipped_names:", unzipped_names)
print("unzipped_scores:", unzipped_scores)
输出大概是:
pairs: [('Alice', 85), ('Bob', 92), ('Cindy', 88)]
unzipped_names: ('Alice', 'Bob', 'Cindy')
unzipped_scores: (85, 92, 88)
zip 是打包,* 是拆包,所以 zip(*pairs) 等于“把成对的数据再拆分成两列”。在处理 CSV、表格数据时非常顺手。
【9】enumerate:别手动写 i += 1 了
for idx, ch in enumerate("python", start=1):
if ch == "h":
print("index:", idx)
enumerate 会在遍历可迭代对象时,把“索引”和“元素”一起给你。
这里字符串 "python",h 在第 4 个位置,加上 start=1,输出就是:
index: 4
写循环的时候,尽量用 enumerate,可读性比自己控制下标好太多。
【10】拆包:一行拿“头尾”和“中间一截”
data = [10, 20, 30, 40, 50]head, *middle, tail = data
print("head :", head)
print("middle:", middle)
print("tail :", tail)
这个语法挺优雅的:
head拿到第一个元素。tail拿到最后一个元素。*middle把中间剩下的都装进一个列表。
输出是:
head : 10
middle: [20, 30, 40]
tail : 50
平时处理命令行参数、路径、拆列表的时候,这种写法会让代码很“干净”。
【11】*args / **kwargs:参数到底都装哪了?
defshow(a, *args, **kwargs):
print("a =", a)
print("args =", args)
print("kwargs=", kwargs)show(1, 2, 3, x=4, y=5)
执行这段,会看到:
a = 1
args = (2, 3)
kwargs= {'x': 4, 'y': 5}
这里几个点要记住:
*args是“多出来的定位参数”,会变成一个元组。**kwargs是“多出来的关键字参数”,会变成一个字典。三者的顺序必须是:普通参数、 *args、默认参数、**kwargs。
这个是写通用库函数、装饰器时的基本功。
【12】切片和负索引:从尾巴往前数
s = "abcdefg"print(s[-3:-1])
print(s[::-1])
解释一下:
s[-3:-1]:从倒数第 3 个位置开始,到倒数第 1 个之前(不含),所以是"ef"。s[::-1]:步长是-1,从头到尾反着切,所以是"gfedcba"。
负索引是 Python 非常“顺手”的特性,看一眼就能从右边数过去。
【13】生成器和列表:为啥第二次是 0?
nums_list = [i for i in range(5)]
nums_gen = (i for i in range(5))print(sum(nums_list))
print(sum(nums_gen))
print(sum(nums_gen))
真正的输出是:
10
10
0
重点来了:生成器是一次性的,第一次 sum(nums_gen) 把里面的元素都“消费”掉了,第二次再 sum 的时候,已经没有元素可以迭代了,所以是 0。
这个细节在业务里也经常出 bug,比如:
先迭代了一次生成器进行日志打印。 再拿同一个生成器去做真正逻辑。 结果发现什么都没处理到……
【14】字典的 get 和 setdefault:顺便还能写入
d = {}print(d.get("x", 0))
print(d.setdefault("x", 1))
print(d.get("x", 0))
print(d)
逐行分析一下:
d.get("x", 0):字典里没有x,返回默认值0,但不会写回去。d.setdefault("x", 1):如果没有x,就把x=1写进去,并返回1。再 get的时候,x已经存在了,所以拿到1。最后 d就是{'x': 1}。
这俩方法都很常用,区别在于:**get 只是读,setdefault 会顺便写**。
【15】集合去重再排序:一行搞定
nums = [3, 1, 2, 3, 2, 1]
result = sorted(set(nums))print(result)
这里 set(nums) 先把列表变成集合,去重后顺序就不保证了(集合本身是无序的)。再 sorted 一下,就变成一个升序的列表:
[1, 2, 3]
如果你既想去重又想保留“原来出现的顺序”,可以改用字典:
nums = [3, 1, 2, 3, 2, 1]
result = list(dict.fromkeys(nums))
print(result) # [3, 1, 2]
因为 Python 3.7+ 的 dict 会记住插入顺序。
【16】异常处理里的 else / finally:执行顺序别搞混
defdemo(divisor):
try:
print("in try")
result = 10 / divisor
except ZeroDivisionError:
print("catch ZeroDivisionError")
else:
print("in else, result =", result)
finally:
print("in finally")demo(2)
print("-----")
demo(0)
输出顺序是:
in try
in else, result = 5.0
in finally
-----
in try
catch ZeroDivisionError
in finally
几个点:
try里没抛异常:走else,然后无论如何都会走finally。try里抛了异常且被except接住:不走else,仍然会走finally。finally是一定会执行的,哪怕前面return了(只是要注意一些极端情况,比如 os._exit)。
很多人写业务代码时把所有逻辑都塞进 try,其实更推荐把“可能出错”的代码放在 try 里,把“成功时的后续逻辑”放进 else,异常路径和正常路径分得更清晰。
【17】一个最小的上下文管理器:with 背后发生了啥?
classDemo:
def__enter__(self):
print("enter")
return"VALUE"def__exit__(self, exc_type, exc, tb):
print("exit", exc_type)
with Demo() as v:
print("in with:", v)
输出:
enter
in with: VALUE
exit None
with 做了两件事:
调用 __enter__,它的返回值会赋给as后面的变量(这里是v)。不管中间代码块正常结束还是抛异常,都会调用 __exit__。
像 open、threading.Lock、数据库连接这些,能配合 with 使用的,背后都是实现了这两个方法。理解了这点,你也可以很轻松写出自己的上下文管理器,比如做统一的计时、打日志、资源释放等等。
到这里刚好 17 段小代码,基本把 Python 里最常被考的那些“细节基础”过了一遍:引用、拷贝、参数、容器操作、循环、生成器、异常、上下文这些。
你要是想更狠一点,可以找个空文件,新建个 basic_17.py,把这 17 段全敲进去,自己先在脑子里预判输出,再实际跑一遍,对不上就查为啥,对得上就算真正掌握了。等哪天面试官跟你玩这套,你大概率是笑着看完题目那种。
-END-
我为大家打造了一份RPA教程,完全免费:songshuhezi.com/rpa.html
虎哥作为一名老码农,整理了全网最全《python高级架构师资料合集》,总量高达650GB