Python技术迷

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 container

print(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]

要记住两点:

  1. list.sort() 在原地修改列表,**返回值是 None**。
  2. 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']

这里有两个要点:

  1. 列表推导式本身是个表达式。
  2. 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}

这里几个点要记住:

  1. *args 是“多出来的定位参数”,会变成一个元组。
  2. **kwargs 是“多出来的关键字参数”,会变成一个字典。
  3. 三者的顺序必须是:普通参数、*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)

逐行分析一下:

  1. d.get("x", 0):字典里没有 x,返回默认值 0,但不会写回去。
  2. d.setdefault("x", 1):如果没有 x,就把 x=1 写进去,并返回 1。
  3. 再 get 的时候,x 已经存在了,所以拿到 1。
  4. 最后 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

几个点:

  1. try 里没抛异常:走 else,然后无论如何都会走 finally。
  2. try 里抛了异常且被 except 接住:不走 else,仍然会走 finally。
  3. 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 做了两件事:

  1. 调用 __enter__,它的返回值会赋给 as 后面的变量(这里是 v)。
  2. 不管中间代码块正常结束还是抛异常,都会调用 __exit__。

像 open、threading.Lock、数据库连接这些,能配合 with 使用的,背后都是实现了这两个方法。理解了这点,你也可以很轻松写出自己的上下文管理器,比如做统一的计时、打日志、资源释放等等。

到这里刚好 17 段小代码,基本把 Python 里最常被考的那些“细节基础”过了一遍:引用、拷贝、参数、容器操作、循环、生成器、异常、上下文这些。

你要是想更狠一点,可以找个空文件,新建个 basic_17.py,把这 17 段全敲进去,自己先在脑子里预判输出,再实际跑一遍,对不上就查为啥,对得上就算真正掌握了。等哪天面试官跟你玩这套,你大概率是笑着看完题目那种。

-END-

我为大家打造了一份RPA教程,完全免费:songshuhezi.com/rpa.html

🔥虎哥私藏精品🔥

虎哥作为一名老码农,整理了全网最全《python高级架构师资料合集》,总量高达650GB