参数详解:默认参数、可变参数、关键字参数

上一篇 函数定义与调用 我们讲了函数的基本形态,但参数才是函数的”接口”,也是 Python 最灵活的地方。Python 函数参数系统能玩出比 C/Java 多得多的花样:默认值、可变参数、关键字参数、位置参数、*/* 特殊分隔符……这一篇要把它们全串起来。学完之后你就能读懂 def open(file, mode='r', buffering=-1, encoding=None, ...) 这种复杂签名了。

一、位置参数

最基本的形式:按位置传参

1
2
3
4
def add(a, b):
return a + b

add(3, 5) # 8 按位置

二、关键字参数

调用时可以显式写出参数名,顺序可以变:

1
2
3
4
5
def greet(name, greeting):
print(f"{greeting}{name}")

greet(name="咖飞", greeting="你好")
greet(greeting="早上好", name="小王")

关键字参数一定要在位置参数之后

1
2
greet("咖飞", greeting="你好")     # ✅
greet(name="咖飞", "你好") # ❌ SyntaxError

三、默认参数

给参数一个默认值,调用时可以省略:

1
2
3
4
5
6
def greet(name, greeting="你好"):
print(f"{greeting}{name}")

greet("咖飞") # 你好,咖飞
greet("咖飞", "早上好") # 早上好,咖飞
greet("咖飞", greeting="hi") # hi,咖飞

默认参数一定要放在后面

1
2
def bad(x=1, y):    # ❌ SyntaxError: 非默认参数在默认参数后
...

⚠️ 可变默认参数陷阱(Python 最经典的坑)

1
2
3
4
5
6
def append(item, lst=[]):
lst.append(item)
return lst

append(1) # [1]
append(2) # [1, 2] !! 上一次的 lst 还在

原因:默认值 [] 只在 def 语句执行时求值一次,之后所有调用共享同一个对象。

修复

1
2
3
4
5
def append(item, lst=None):
if lst is None:
lst = []
lst.append(item)
return lst

规则:默认参数值只用不可变对象(数字、字符串、元组、None)。

四、可变位置参数 *args

*args 接受任意多个位置参数,打包成元组

1
2
3
4
5
6
7
def total(*args):
print(type(args)) # <class 'tuple'>
return sum(args)

total(1, 2, 3) # 6
total(1, 2, 3, 4, 5) # 15
total() # 0

名字 args 只是惯例,也可以叫 *nums*values关键是那个 *

五、可变关键字参数 **kwargs

**kwargs 接受任意多个关键字参数,打包成字典

1
2
3
4
5
6
7
8
def show(**kwargs):
print(type(kwargs)) # <class 'dict'>
for k, v in kwargs.items():
print(f"{k}={v}")

show(name="咖飞", age=3)
# name=咖飞
# age=3

六、混合使用

*args**kwargs 可以同时用,顺序固定:

1
def f(位置参数, *args, **kwargs):

例子:

1
2
3
4
5
def log(level, *args, **kwargs):
print(f"[{level}]", *args, kwargs)

log("INFO", "server started", port=8080, host="localhost")
# [INFO] server started {'port': 8080, 'host': 'localhost'}

七、参数解包(调用时的反向操作)

调用函数时,*** 用来”解包”:

1
2
3
4
5
6
7
8
def f(a, b, c):
print(a, b, c)

args = [1, 2, 3]
f(*args) # 等价 f(1, 2, 3)

kwargs = {"a": 1, "b": 2, "c": 3}
f(**kwargs) # 等价 f(a=1, b=2, c=3)

这在函数转发里非常有用:

1
2
3
def wrapper(*args, **kwargs):
# 做点前置逻辑
return original_func(*args, **kwargs)

八、仅位置参数与仅关键字参数(Python 3.8+)

现代 Python 提供两个特殊分隔符:

  • /:它之前的参数只能按位置传
  • *:它之后的参数只能按关键字传
1
2
3
4
5
6
7
def f(pos_only, /, both, *, kw_only):
print(pos_only, both, kw_only)

f(1, 2, kw_only=3) # ✅
f(1, both=2, kw_only=3) # ✅
f(pos_only=1, ...) # ❌ 不允许,pos_only 只能位置
f(1, 2, 3) # ❌ 不允许,kw_only 只能关键字

什么时候用 /

  • 参数名不稳定:不想让调用方依赖参数名字,将来改名不影响;
  • 性能极致:跳过关键字查找(微小优化);
  • API 语义:像内置 abs(x)len(s),这种”名字无关”的参数。

什么时候用 *

  • 参数很多,容易搞错顺序——强制关键字,代码可读性大幅提升;
  • 布尔开关参数sort(items, *, reverse=False),写起来 sort(items, reverse=True)sort(items, True) 清晰得多。

九、参数顺序的完整规则

一个函数的参数顺序可以有五类,按顺序:

1
2
3
4
5
6
7
8
def f(
仅位置, # 出现 / 之前
/,
位置或关键字, # 常规参数
*args, # 可变位置
仅关键字, # 出现在 *args 或 * 之后
**kwargs # 可变关键字
): ...

一个”极限例子”:

1
2
def spec(a, b, /, c, d, *args, e, f=10, **kwargs):
...
  • a, b:仅位置;
  • c, d:位置或关键字;
  • args:可变位置;
  • e:仅关键字,无默认;
  • f:仅关键字,有默认;
  • kwargs:可变关键字。

日常写函数不用同时用全部五种,但你应该看得懂别人写的这种签名。

十、类型注解与参数

参数一起写类型注解:

1
2
3
4
5
6
7
8
9
10
11
def register(
name: str,
age: int,
*emails: str,
active: bool = True,
**extras: str,
) -> dict:
return {"name": name, "age": age, "emails": emails,
"active": active, "extras": extras}

register("咖飞", 3, "a@x.com", "b@x.com", active=False, city="北京")

十一、一个”综合参数”例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# params.py
"""演示各种参数形式的组合用法。"""


def create_user(name: str, /, age: int, *hobbies: str,
city: str = "北京", **extras) -> dict:
"""创建用户档案。

Args:
name: 姓名(仅位置)
age: 年龄
*hobbies: 任意多个爱好
city: 城市,仅关键字,默认"北京"
**extras: 额外自定义属性
"""
return {
"name": name,
"age": age,
"hobbies": hobbies,
"city": city,
**extras,
}


print(create_user("咖飞", 3, "读书", "跑步", city="上海", height=178))

十二、函数转发的经典模式

*args + **kwargs 让”包装函数”变得优雅:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import time
import functools

def timed(func):
"""一个简易装饰器:打印函数耗时。"""
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
print(f"{func.__name__} 耗时 {time.time() - start:.3f}s")
return result
return wrapper

@timed
def compute(n, base=10):
return sum(i ** base for i in range(n))

compute(1000, base=3)

装饰器我们在 装饰器入门 详细讲。

十三、命令行参数解析预告

大多数命令行工具都用 argparse(标准库)解析参数:

1
2
3
4
5
6
7
8
9
import argparse

parser = argparse.ArgumentParser()
parser.add_argument("input")
parser.add_argument("--output", default="out.txt")
parser.add_argument("--verbose", "-v", action="store_true")
args = parser.parse_args()

print(args.input, args.output, args.verbose)

具体用法后面讲。

十四、参数传递的机制:值 or 引用?

Python 的参数传递是 “传对象引用”(pass by object reference):函数拿到的是”和外部同一个对象的引用”。

1
2
3
4
5
6
7
8
9
10
11
12
def f(x):
x = 10 # 让本地名字 x 指向新对象,不影响外部
def g(lst):
lst.append(1) # 就地修改可变对象,外部感知得到

a = 5
f(a)
print(a) # 5,没变

b = []
g(b)
print(b) # [1],变了

规则

  • 重新赋值 → 不影响外部;
  • 就地修改可变对象 → 影响外部。

想让函数”完全不影响外部”,传入之前拷贝:g(b[:]) 或者内部拷贝 lst = lst.copy()

十五、常见陷阱

  1. 可变默认参数:老生常谈,永远用 None + 内部判断。
  2. *args**kwargs 顺序args 一定在 kwargs 前。
  3. f(*args) 里 args 必须可迭代f(*None) 报错。
  4. **kwargs 里 kwargs 必须是字典且 key 是字符串
  5. 同一个参数名多次传值f(1, x=1)x 是第一个参数,会 TypeError。
  6. 默认值参数在 def 时求值,不是调用时:
1
2
3
4
import datetime
def log(msg, ts=datetime.datetime.now()):
print(ts, msg)
# 所有调用都用第一次 def 时的时间,除非 ts=None 再内部生成

正确:def log(msg, ts=None): if ts is None: ts = datetime.datetime.now()

  1. 对 dict 用 ** 解包时 key 冲突
1
f(**{"a": 1}, a=2)   # ❌ TypeError

十六、小结与延伸阅读

  • 位置参数、关键字参数、默认参数、*args**kwargs/* 七种参数形式;
  • 参数顺序:位置 → / → 位置或关键字 → *args → 仅关键字 → **kwargs
  • 可变默认参数是经典陷阱;
  • *args/**kwargs 是包装/转发函数的核心工具;
  • Python 参数传递是”传对象引用”;
  • 学会用 /* 让 API 更严谨。

延伸阅读:

下一篇 作用域与闭包 我们讲变量的”能见度”问题,以及闭包是怎么”记住外层变量”的。

函数定义与调用

到目前为止,我们所有代码都写在一个 .py 文件里”从上到下”跑。这只适合短脚本,写稍微大点的程序就会痛苦——重复代码到处都是、变量名相互干扰、想改一处得翻半天。函数是解决这些问题的第一步:”给一段代码起个名字,以后想用就调用“。这一篇讲清楚函数的最基本形态:定义、调用、返回值、docstring、类型注解。参数详解和作用域下一篇再展开。

一、定义与调用

Python 用 def 关键字定义函数:

1
2
3
4
5
def greet(name):
print(f"你好,{name}")


greet("咖飞") # 你好,咖飞

结构:

  • def:关键字;
  • greet:函数名,遵循 snake_case;
  • (name):参数列表(可以为空 ());
  • 冒号 + 缩进:函数体;
  • 调用:greet("咖飞") 就像加个括号。

二、返回值:return

函数用 return 返回值:

1
2
3
4
5
def add(a, b):
return a + b

result = add(3, 5)
print(result) # 8

没有 return 或空 return 的函数返回 None

1
2
3
4
5
def hello():
print("hi")

result = hello()
print(result) # None

一个函数可以有多个 return——遇到第一个匹配的就退出:

1
2
3
4
5
6
def sign(n):
if n > 0:
return "正"
if n < 0:
return "负"
return "零"

三、返回多个值(其实是一个元组)

1
2
3
4
5
def stats(nums):
return min(nums), max(nums), sum(nums) / len(nums)

low, high, avg = stats([1, 2, 3, 4, 5])
print(low, high, avg) # 1 5 3.0

Python 并没有”多返回值”,只是打包成元组再解包。想返回带名字的多值,用 namedtupledataclass类与对象基础)。

四、文档字符串 docstring

函数体第一行如果是字符串,就是 docstring:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def circle_area(r):
"""计算圆的面积。

Args:
r: 半径,非负实数

Returns:
面积,float
"""
return 3.14159 * r * r


print(circle_area.__doc__)
help(circle_area) # 在 REPL 里查文档

好处:

  • IDE 自动显示;
  • help() 直接可读;
  • 生成 API 文档时被 Sphinx 等工具抓取。

推荐的 docstring 风格:Google 风格(如上例)或 NumPy 风格。选一个,团队里保持一致。

五、类型注解(type hints)

Python 3.5+ 支持给参数和返回值加类型注解:

1
2
3
4
5
def add(a: int, b: int) -> int:
return a + b

def greet(name: str) -> None:
print(f"你好,{name}")

注解不改变运行时行为——传 add("1", "2") 依然能跑(结果 "12")。它是给 IDE / mypy / pylance 看的,能大幅减少低级错误。

复杂类型:

1
2
3
4
5
def top_k(items: list[int], k: int) -> list[int]:
return sorted(items, reverse=True)[:k]

def get_user(id: int) -> dict[str, str] | None:
...

Python 3.10+ 才能用 X | Y 表示 Union。老版本要 from typing import Optional, Union

完整的类型注解见 类型注解 type hints 实战

六、函数是”一等公民”

Python 里函数就是普通对象,可以:

  • 赋值给变量;
  • 作为参数传递;
  • 作为返回值;
  • 存在容器里;
  • 有属性(__name____doc__ 等)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def double(x):
return x * 2

f = double # 变量名指向函数对象
print(f(5)) # 10

# 传给别的函数
list(map(double, [1, 2, 3])) # [2, 4, 6]

# 存进字典(策略模式)
ops = {
"add": lambda a, b: a + b,
"sub": lambda a, b: a - b,
}
print(ops["add"](3, 4)) # 7

这个特性让 Python 天然支持函数式编程,也让”高阶函数””装饰器”成为可能——这些后面几篇会展开。

七、函数属性

1
2
3
4
5
6
7
8
9
def foo():
"""foo 的说明"""
pass

print(foo.__name__) # 'foo'
print(foo.__doc__) # 'foo 的说明'

foo.custom = 123 # 甚至能挂自定义属性
print(foo.custom) # 123

八、匿名函数 lambda

对于极简函数,可以用 lambda

1
2
3
4
5
double = lambda x: x * 2
add = lambda a, b: a + b

# 常用在 sort 的 key
sorted(users, key=lambda u: u.age)

lambda 只能写一个表达式,不能有语句。复杂逻辑还是用 def。详细见 匿名函数 lambda 与高阶函数

九、纯函数 vs 有副作用的函数

  • 纯函数:不依赖外部状态,不修改外部状态,同样输入总得到同样输出;
  • 有副作用:修改全局变量、打印、写文件、发网络请求都算副作用。
1
2
3
4
5
6
7
8
9
10
# 纯函数
def add(a, b):
return a + b

# 有副作用
total = 0
def add_to_total(x):
global total
total += x
print(f"当前总和 {total}")

能写纯函数就写纯函数——好测试、好组合、好并发。副作用集中在少数入口处。

十、默认参数与短小函数

默认参数的完整讲解在 参数详解,这里预告:

1
2
3
4
5
def greet(name, greeting="你好"):
print(f"{greeting}{name}")

greet("咖飞") # 你好,咖飞
greet("小王", "早上好") # 早上好,小王

十一、写函数的几条铁律

作为一个 Python 老手,我觉得写函数最重要的几点:

  1. 一个函数只做一件事(Single Responsibility);
  2. 函数不超过 30 行——超过就该拆;
  3. 参数不超过 4 个——多了改成传字典 / 类;
  4. 优先纯函数
  5. 函数名是动词get_usersave_configis_valid
  6. 必写 docstring(哪怕只有一行);
  7. 给参数和返回值加类型注解
  8. 不写”神奇”的默认参数,尤其是可变默认值(见下节陷阱)。

十二、常见陷阱

陷阱 1:可变默认参数

1
2
3
4
5
6
def append_item(item, lst=[]):
lst.append(item)
return lst

print(append_item(1)) # [1]
print(append_item(2)) # [1, 2] !! 上一次的 lst 还在

原因:默认参数 [] 只在函数定义时求值一次,后续所有调用共享同一个列表。正确做法

1
2
3
4
5
def append_item(item, lst=None):
if lst is None:
lst = []
lst.append(item)
return lst

这是 Python 面试的经典题,一定要记住。

陷阱 2:忘了 return

1
2
3
4
def largest(nums):
m = max(nums) # 忘了 return

print(largest([1, 2, 3])) # None

函数没 return 就返回 None,用返回值时 None 会导致下游报错。

陷阱 3:修改传入的可变对象

1
2
3
def sort_items(lst):
lst.sort() # 就地修改,调用方的 lst 也变了
return lst

如果你不想让调用方感知这个改动:lst = sorted(lst)

陷阱 4:函数名与内置冲突

1
2
3
4
def list(x):
return x

print(list([1, 2, 3])) # 现在 list 是我们的函数,内置 list 被覆盖

避免用 listdictstrtypeidinput 等作为变量或函数名。

陷阱 5:返回不一致

1
2
3
4
5
def find(items, target):
for x in items:
if x == target:
return x
# 找不到时忘了 return,隐式返回 None

明确写 return None,或者抛异常。让”找不到”的语义清晰。

十三、一个完整例子:温度转换工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# temperature.py
"""温度单位转换工具集。"""


def celsius_to_fahrenheit(c: float) -> float:
"""摄氏 → 华氏。"""
return c * 9 / 5 + 32


def fahrenheit_to_celsius(f: float) -> float:
"""华氏 → 摄氏。"""
return (f - 32) * 5 / 9


def celsius_to_kelvin(c: float) -> float:
"""摄氏 → 开尔文。"""
return c + 273.15


def kelvin_to_celsius(k: float) -> float:
"""开尔文 → 摄氏。"""
return k - 273.15


def convert(value: float, from_unit: str, to_unit: str) -> float:
"""通用转换函数。所有非摄氏单位先转成摄氏,再从摄氏转到目标单位。"""
from_unit = from_unit.upper()
to_unit = to_unit.upper()

# 先转成摄氏
if from_unit == "C":
c = value
elif from_unit == "F":
c = fahrenheit_to_celsius(value)
elif from_unit == "K":
c = kelvin_to_celsius(value)
else:
raise ValueError(f"未知单位:{from_unit}")

# 再从摄氏转出
if to_unit == "C":
return c
if to_unit == "F":
return celsius_to_fahrenheit(c)
if to_unit == "K":
return celsius_to_kelvin(c)
raise ValueError(f"未知单位:{to_unit}")


if __name__ == "__main__":
# 冰水混合物
assert celsius_to_fahrenheit(0) == 32
assert convert(100, "C", "F") == 212
assert convert(212, "F", "C") == 100

print(f"25°C = {convert(25, 'C', 'F'):.1f}°F")
print(f"25°C = {convert(25, 'C', 'K'):.2f}K")

代码里演示了:函数拆分、类型注解、docstring、raise 抛错、assert 自测、convert 总调度。

十四、小结与延伸阅读

  • def name(params): 定义函数;
  • return 返回值,没写等于返回 None;
  • 函数是一等公民——可以传递、赋值、放容器里;
  • 别用可变对象做默认参数;
  • 给函数加 docstring 和类型注解;
  • 一个函数一件事,30 行封顶;
  • 避免函数名遮蔽内置。

延伸阅读:

下一篇 参数详解:默认参数、可变参数、关键字参数 我们把函数的”参数”话题彻底讲透。

常用内置函数汇总

Python 提供了 70 多个内置函数(built-in functions),它们不用 import 就能直接用。掌握它们,你能减少 50% 的手写循环代码。这一篇不是一个字典式的罗列,而是按用途分组——数值、序列、迭代、类型转换、面向对象、IO、反射——每类都举例说明什么时候用哪个。收藏本文,之后写代码前先来这里翻一下。

一、数值类

1
2
3
4
5
6
7
abs(-5)              # 5    绝对值
divmod(17, 5) # (3, 2) 同时算商和余数
pow(2, 10) # 1024 等价 2 ** 10
pow(2, 10, 1000) # 24 快速幂取模,密码学常用
round(3.5) # 4 银行家舍入(四舍六入五留双)
round(3.14159, 2) # 3.14
round(2.5) # 2 不是 3!

注意 round 的”银行家舍入”陷阱:round(0.5)round(2.5)round(4.5) 都朝偶数舍入。真正的”四舍五入”要用 decimal.Decimal 或者 math.floor(x + 0.5)

min / max 也算数值类:

1
2
3
4
5
6
7
min(3, 1, 4, 1, 5, 9, 2, 6)
max([3, 1, 4])
min("banana") # 'a' 字符串按字典序
min(users, key=lambda u: u.age) # 按 key 找最小

# default 参数
max([], default=0) # 空序列不报错

sum 只对数字:

1
2
sum([1, 2, 3])
sum([1, 2, 3], start=100) # 106

别用 sum 拼列表——性能巨差。用 itertools.chainfunctools.reduce

二、序列构造与转换

1
2
3
4
5
6
7
8
9
list(iter)           # 转列表
tuple(iter) # 转元组
set(iter) # 转集合
frozenset(iter) # 转不可变集合
dict([("a", 1)]) # 转字典
str(x) # 转字符串
bytes(iter) # 转字节
bytearray(iter) # 转可变字节
range(n) # 数字序列

三、序列聚合

1
2
3
4
5
6
7
8
9
10
11
12
len(seq)             # 长度
sum(seq) # 求和
min(seq), max(seq) # 极值
sorted(seq) # 排序(返回新列表)
sorted(seq, key=..., reverse=True)
reversed(seq) # 反转迭代器
list(reversed([1, 2, 3])) # [3, 2, 1]

any([False, True]) # True,任一为真
all([True, True]) # True,全为真
any([]) # False
all([]) # True 空的"全部为真"是空真

any / all 常和生成表达式搭配:

1
2
3
4
5
if any(x < 0 for x in nums):
print("有负数")

if all(u.active for u in users):
print("所有用户都活跃")

四、迭代辅助

1
2
3
4
5
6
7
8
enumerate(seq, start=0)         # 产生 (index, item)
zip(a, b) # 并行迭代,最短为准
zip(a, b, strict=True) # Python 3.10+,长度不等抛错
map(func, iter) # 每个元素调用 func
filter(pred, iter) # 保留 pred 返回真的
range(start, stop, step) # 数字序列
iter(iterable) # 拿到迭代器
next(iterator, default) # 取下一个

enumerate 常用

1
2
for i, name in enumerate(names, start=1):
print(f"{i}. {name}")

zip 组合

1
2
for a, b, c in zip(list_a, list_b, list_c):
...

map 与 filter:现代 Python 更推荐推导式

1
2
3
4
5
6
7
8
9
# map
uppercase = list(map(str.upper, words))
# 等价推导式(更 Pythonic)
uppercase = [w.upper() for w in words]

# filter
positives = list(filter(lambda x: x > 0, nums))
# 等价推导式
positives = [x for x in nums if x > 0]

五、类型与检查

1
2
3
4
5
6
type(x)                          # 返回类型对象
isinstance(x, (int, float)) # x 是 int 或 float
issubclass(cls, base) # cls 是不是 base 的子类
callable(x) # x 是不是可调用(函数/类/带 __call__)
id(x) # 对象唯一 id,一般不用
hash(x) # 计算哈希,不可哈希会报错

六、字符与编码

1
2
3
4
5
6
7
ord("A")            # 65
chr(65) # 'A'
bin(10) # '0b1010'
oct(10) # '0o12'
hex(255) # '0xff'
ascii("你好") # "'\\u4f60\\u597d'"
repr(x) # 对象的 repr,一般 print(repr(x))

七、IO 与文件

1
2
3
print(*objects, sep=' ', end='\n', file=sys.stdout, flush=False)
input(prompt)
open(path, mode="r", encoding="utf-8")

open 一般搭配 with,见 with 语句与上下文管理器

八、对象与反射

1
2
3
4
5
6
getattr(obj, "name", default)    # 取属性,可给默认
setattr(obj, "name", value) # 设属性
hasattr(obj, "name") # 有没有该属性
delattr(obj, "name") # 删属性
dir(obj) # 列出所有属性/方法
vars(obj) # 相当于 obj.__dict__

反射适合写”框架”代码,日常业务不用太深入。

getattr 的实用套路

1
2
3
4
5
6
7
# 根据用户输入的操作名调用方法
op = input("操作:")
handler = getattr(controller, f"do_{op}", None)
if handler:
handler()
else:
print("未知操作")

九、动态执行

1
2
3
eval("1 + 2")              # 3    求值一个表达式
exec("x = 5") # 执行任意语句
compile(src, "<string>", "exec") # 编译代码对象

警告eval / exec 危险——用户输入直接放进去等于给攻击者开后门。除非你完全控制输入且没有别的选择,否则不要用。

十、面向对象辅助

1
2
3
4
object                    # 所有类的祖先
super() # 调用父类方法
staticmethod, classmethod, property # 装饰器
type("MyClass", (Base,), {"x": 1}) # 动态建类

十一、迭代进阶:sorted/reversed/enumerate 深入

sorted 详解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
sorted(iter, key=None, reverse=False)

# 按长度排
sorted(words, key=len)

# 按元组第 2 项
sorted(pairs, key=lambda p: p[1])

# 稳定排序,可以多次 sort 组合多字段
records.sort(key=lambda r: r.age)
records.sort(key=lambda r: r.name) # 主排序:name,次排序:age

# 或者一次搞定
records.sort(key=lambda r: (r.name, r.age))

reversed 只能对序列或实现 __reversed__ 的对象

1
2
3
list(reversed([1, 2, 3]))         # [3, 2, 1]
list(reversed(range(5))) # [4, 3, 2, 1, 0]
list(reversed({1, 2, 3})) # ❌ set 无序,不能 reversed

十二、数学与工具(要 import 但很常用)

不是严格意义的内置函数,但用得跟内置一样多:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import math
math.sqrt(16) # 4.0
math.floor(3.7) # 3
math.ceil(3.2) # 4
math.pi, math.e # 常量

import random
random.random() # [0.0, 1.0)
random.randint(1, 10) # [1, 10]
random.choice([1, 2, 3])
random.shuffle(lst) # 就地打乱

import statistics
statistics.mean([1, 2, 3])
statistics.median([1, 2, 3])

十三、Python 3.10+ / 3.11+ 新增

1
2
3
4
5
6
7
8
9
10
11
# 3.10:zip strict
zip([1, 2, 3], [4, 5], strict=True) # ValueError

# 3.11:ExceptionGroup(并发场景多个异常聚合)
try:
...
except* ValueError as eg:
...

# 3.12:type 语法糖
type Vector = list[float]

十四、”常见但不常见”的冷门内置

一些容易被遗忘但偶尔很好用的:

1
2
3
4
5
6
memoryview(b"hello")       # 零拷贝的内存视图
slice(1, 5, 2) # 可复用的切片对象
zip(*matrix) # 矩阵转置的一行代码

# 组合技:一次拿多个属性
attrgetter, itemgetter, methodcaller # 在 operator 模块

来自 operator 模块(虽然要 import,但极其常用):

1
2
3
4
5
6
7
8
9
10
from operator import attrgetter, itemgetter

# 按属性排序
users.sort(key=attrgetter("age"))

# 按 dict 的 key 排序
data.sort(key=itemgetter("score"))

# 多字段
data.sort(key=itemgetter("category", "price"))

这几乎是所有排序场景最快的写法,比 lambda 更快、更清晰。

十五、一个综合例子:数据集统计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# stats.py
"""对一个学生成绩表做各种统计,尽量用内置函数。"""

students = [
{"name": "张三", "score": 92, "class": "A"},
{"name": "李四", "score": 78, "class": "B"},
{"name": "王五", "score": 85, "class": "A"},
{"name": "赵六", "score": 60, "class": "C"},
{"name": "孙七", "score": 95, "class": "B"},
]

from operator import itemgetter

# 总人数、总分、平均、最大/最小
print("人数:", len(students))
print("总分:", sum(map(itemgetter("score"), students)))
print("平均:", sum(s["score"] for s in students) / len(students))
print("最高:", max(students, key=itemgetter("score")))
print("最低:", min(students, key=itemgetter("score")))

# 60 分以下
print("不及格:", list(filter(lambda s: s["score"] < 70, students)))

# 是否全员及格
print("全过:", all(s["score"] >= 60 for s in students))

# 是否有满分
print("有满分:", any(s["score"] == 100 for s in students))

# 排序(成绩降序)
print("排名:")
for i, s in enumerate(sorted(students, key=itemgetter("score"), reverse=True), 1):
print(f"{i}. {s['name']} {s['score']}")

# 按班级分组(简单版)
from itertools import groupby
data_sorted = sorted(students, key=itemgetter("class"))
for cls, group in groupby(data_sorted, key=itemgetter("class")):
group = list(group)
print(f"班级 {cls}: 平均 {sum(g['score'] for g in group)/len(group):.1f}")

十六、常见陷阱

  1. round 是银行家舍入round(0.5) == 0。要传统四舍五入用 decimal
  2. sum 别用来拼字符串sum(strings, "") 报错,也慢。用 "".join
  3. filter/map 返回迭代器filter(...) 结果需要 list() 转才能反复用。
  4. sortedsort:一个返回新列表,一个就地。前者接受任何可迭代,后者只在 list 上有。
  5. any([]) 是 False,all([]) 是 True:空的”全部为真”是”空真”(vacuous truth)。
  6. eval 别接用户输入:安全事故大坑。
  7. hash(-1) 是 -2:CPython 实现细节,-1 被用作错误信号。
  8. isinstance(True, int) 是 True:bool 是 int 的子类。

十七、小结与延伸阅读

  • Python 内置函数约 70 个,日常写代码高频用到的不到 30 个;
  • 数值类:abs / sum / min / max / round / pow / divmod
  • 序列构造:list / tuple / set / dict / str
  • 序列聚合:sorted / reversed / any / all / len
  • 迭代:enumerate / zip / map / filter / range
  • 类型/反射:isinstance / type / getattr / setattr / hasattr
  • operator 引入 itemgetter / attrgetter 加速排序;
  • 有困惑先 help(内置函数)dir() 看看再上网搜。

延伸阅读:

到这里模块二(数据结构)8 篇结束! 下一篇我们进入 模块三:函数与模块,从 函数定义与调用 开始。

序列切片深入解析

切片(slicing)是 Python 里最”看起来简单,实际藏了很多细节”的语法。前面 列表元组字符串 都用过 a[1:3],但你可能不知道:切片有第三个参数(步长)、切片赋值可以改变长度、slice 对象是可以复用的、numpy 数组切片是视图而不是拷贝……这一篇要把切片的”心智模型”讲通,让你以后遇到任何切片问题都能自己推导出来。

一、切片的三段式

完整语法:

1
序列[start:stop:step]
  • start:起始索引(含),默认为 0step > 0)或 -1step < 0);
  • stop:结束索引(不含),默认为 len(seq)-len(seq)-1
  • step:步长,默认为 1

三个参数都可以省略,冒号不能省。

1
2
3
4
5
6
7
8
9
a = [10, 20, 30, 40, 50, 60, 70]

a[2:5] # [30, 40, 50] 索引 2 到 5(不含 5)
a[:3] # [10, 20, 30] 从头到 3
a[3:] # [40, 50, 60, 70] 从 3 到末尾
a[:] # 完整拷贝
a[::2] # [10, 30, 50, 70] 步长 2
a[1::2] # [20, 40, 60] 奇数索引
a[::-1] # 反转

二、切片的”半开区间”

Python 切片遵循数学的”半开区间” [start, stop)含 start,不含 stop

好处:

  • len(a[i:j]) == j - i(当区间合法时),很好算;
  • 拼接方便:a[:i] + a[i:] 就是原列表;
  • a[i:i] 就是空切片,长度 0。

三、负数索引与负步长

1
2
3
4
5
6
a = [10, 20, 30, 40, 50]

a[-1] # 50
a[-3:-1] # [30, 40]
a[::-1] # 反转
a[-1:-4:-1] # [50, 40, 30] 步长 -1,从右往左

规则:负数索引 = 长度 + 索引a[-1] 就是 a[len(a)-1]

负步长时:默认 start 是最后一个元素,stop 是”越过第一个的位置”。

四、越界不报错

切片的一个奇妙特性是从不越界报错

1
2
3
4
a = [1, 2, 3]
a[10] # ❌ IndexError
a[10:20] # ✅ 返回 []
a[-100:2] # ✅ 返回 [1, 2]

Python 会自动把越界的索引”夹紧”到合法范围。

五、切片赋值

列表(可变序列)支持切片赋值,这是切片最强大的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
a = [1, 2, 3, 4, 5]

# 长度相同:直接替换
a[1:3] = [20, 30] # [1, 20, 30, 4, 5]

# 长度不同:延伸/收缩
a[1:3] = [10, 20, 30] # [1, 10, 20, 30, 4, 5]
a[1:4] = [] # [1, 4, 5] 删除元素

# 用切片插入元素
a = [1, 2, 3]
a[1:1] = [10, 20] # [1, 10, 20, 2, 3]

# 步长赋值:长度必须严格相等
a = [1, 2, 3, 4, 5, 6]
a[::2] = [100, 200, 300] # [100, 2, 200, 4, 300, 6]
a[::2] = [100, 200] # ❌ ValueError: 长度不匹配

注意:字符串和元组是不可变的,不支持切片赋值。

六、切片是浅拷贝

b = a[:] 是列表的浅拷贝

1
2
3
4
a = [[1, 2], [3, 4]]
b = a[:]
b.append([5, 6]) # a 不变
b[0][0] = 999 # a[0] 也变!内层引用共享

要独立的深拷贝:copy.deepcopy(a)

七、slice 对象

[start:stop:step] 本质是 slice(start, stop, step) 对象:

1
2
3
s = slice(1, 5, 2)
a = [10, 20, 30, 40, 50, 60]
print(a[s]) # [20, 40] 等价于 a[1:5:2]

好处:

  • 复用切片规则s = slice(2, None) 起名 HEAD_TWO_SKIP,在多个数组里复用;
  • 传参:函数接收 slice 参数比接收 start/stop/step 三个参数干净;
  • NumPy 里切片对象大量使用。

八、字符串切片

字符串切片行为跟列表一致,但返回新字符串(因为不可变):

1
2
3
4
5
6
7
8
9
s = "abcdefg"

s[2:5] # 'cde'
s[::-1] # 'gfedcba' 反转
s[::2] # 'aceg'

# 判断回文
def is_palindrome(s: str) -> bool:
return s == s[::-1]

九、字节和 bytearray

1
2
3
4
5
6
b = b"hello"
print(b[1:4]) # b'ell'

ba = bytearray(b"hello")
ba[1:4] = b"XYZ"
print(ba) # bytearray(b'hXYZo')

bytes 是不可变的(像 str),bytearray 是可变的(像 list of bytes)。

十、多维切片(NumPy 预告)

Python 内置容器只支持一维切片。多维切片是 NumPy 的强项:

1
2
3
4
5
6
7
8
9
10
11
12
13
import numpy as np
arr = np.arange(20).reshape(4, 5)
# [[ 0 1 2 3 4]
# [ 5 6 7 8 9]
# [10 11 12 13 14]
# [15 16 17 18 19]]

arr[1:3, 2:4] # 取子矩阵
# [[ 7 8]
# [12 13]]

arr[:, 0] # 第一列
arr[::2, ::2] # 隔一行隔一列

注意:NumPy 切片返回视图(view)而不是拷贝,修改视图会修改原数组。这跟内置的 list/str/tuple 相反。

十一、切片的性能特性

  • 内置容器切片是 O(k),k 是切片长度(要复制数据);
  • 切片会分配新对象,频繁切片会增加内存压力;
  • 单纯”跳过前 n 个”若数据量大,考虑用 itertools.islice
1
2
3
4
5
from itertools import islice
# 逐条读 100 万行文件的第 100-200 行
with open("huge.log") as f:
for line in islice(f, 100, 200):
print(line)

islice 支持任何可迭代对象且懒惰,比先 list() 再切片省内存。

十二、切片常用套路

1. 反转

1
2
s[::-1]              # 字符串/列表反转
list(reversed(s)) # 等价但返回列表

2. 拷贝

1
2
b = a[:]
# 更清晰:b = list(a) 或 a.copy()

3. 分块

1
2
3
4
5
6
def chunks(lst, n):
"""把 lst 按每 n 个一组切分。"""
for i in range(0, len(lst), n):
yield lst[i:i+n]

list(chunks([1,2,3,4,5,6,7], 3)) # [[1,2,3], [4,5,6], [7]]

4. 弹出多个

1
2
head, *rest = a          # 用解包
head, tail = a[0], a[1:] # 用切片

5. 移除头/尾

1
2
3
a = a[1:]        # 去掉第一个
a = a[:-1] # 去掉最后一个
a = a[1:-1] # 去掉首尾

6. 保留最后 n 个

1
last_n = a[-n:]

7. 交换两段

1
2
mid = len(a) // 2
a = a[mid:] + a[:mid] # 前后半段交换

十三、切片与索引的边界

1
2
3
4
5
6
7
8
9
10
11
12
a = [1, 2, 3, 4, 5]

# 单索引
a[0] # 1
a[-1] # 5
a[10] # IndexError

# 切片
a[0:0] # []
a[10:20] # []
a[-10:10] # 完整列表
a[:0] # []

初学者常常混淆索引和切片的边界,简单的记忆:

  • 单索引:,越界报错;
  • 切片:start 含,stop 不含,越界不报错

十四、一个综合例子:滚动窗口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# rolling.py
"""对一个序列做定长滚动窗口,计算窗口内平均值。"""

from statistics import mean

def rolling_avg(data: list[float], window: int) -> list[float]:
"""每个窗口的平均值。data 长度 n → 输出长度 n-window+1。"""
if window > len(data):
return []
return [mean(data[i:i+window]) for i in range(len(data) - window + 1)]


def paginate(lst: list, page_size: int) -> list[list]:
"""按 page_size 分页。"""
return [lst[i:i+page_size] for i in range(0, len(lst), page_size)]


def head_tail(lst: list, n: int) -> tuple[list, list]:
"""返回头 n 个和尾 n 个(不重叠)。"""
if len(lst) < 2 * n:
return lst, []
return lst[:n], lst[-n:]


if __name__ == "__main__":
data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
print("滚动平均 (window=3):", rolling_avg(data, 3))
print("分页 (size=4):", paginate(data, 4))
print("头尾 3 个:", head_tail(data, 3))

十五、常见陷阱

陷阱 1:s[::-1] 反转对 dict/set 无效

只有序列(list/tuple/str/bytes/range 等)支持切片。dict 和 set 是无序的,不能切片。

1
{1, 2, 3}[::-1]     # ❌ TypeError

陷阱 2:切片赋值时步长冲突

1
2
3
a = [1, 2, 3, 4]
a[::2] = [10, 20, 30] # ❌ 步长切片必须长度严格匹配
a[::2] = [10, 20] # ✅

陷阱 3:a[:] = ba = b 不同

1
2
3
4
5
6
7
8
9
a = [1, 2, 3]
c = a
a[:] = [10, 20] # 修改原对象,c 也变
print(c) # [10, 20]

a = [1, 2, 3]
c = a
a = [10, 20] # a 指向新对象,c 不变
print(c) # [1, 2, 3]

a[:] = b 是”就地替换 a 的全部元素”,a = b 是”让 a 名字指向新对象”。

陷阱 4:字符串切片”高开销”

Python 字符串切片会复制内存。逐字符处理超大字符串时,切片可能比想象中慢。

陷阱 5:NumPy 切片是视图,Python list 是拷贝

1
2
3
4
5
lst = [1, 2, 3]
lst[1:2][0] = 999 # 什么都没发生,lst 不变

arr = np.array([1, 2, 3])
arr[1:2][0] = 999 # arr[1] 变成 999!

十六、小结与延伸阅读

  • 切片语法 [start:stop:step],含头不含尾;
  • 越界不报错,索引越界才报错;
  • 负数索引 = 长度 + 索引;
  • 切片是浅拷贝,嵌套对象共享;
  • 列表支持切片赋值,可变增/删;
  • slice(start, stop, step) 是可复用的切片对象;
  • 反转 s[::-1]、拷贝 s[:]、分块用切片;
  • 大数据流用 itertools.islice
  • NumPy 切片是视图,行为不同。

延伸阅读:

下一篇 常用内置函数汇总 我们把 Python 内置的 70+ 个函数一次梳理清楚。

字符串格式化进阶(f-string / format / %)

前面 字符串基础与常用方法输入输出与格式化字符串 都提过 f-string,但那只是入门。字符串格式化是 Python 里最需要”知道更多”的话题之一——你以为你会写 f"{x:.2f}",但一遇到”数字千分位加两位小数”、”日期时间”、”补零对齐”、”表达式调试”,就抓瞎了。这一篇把三种格式化姿势(f-string、str.format、%)的语法一次讲透,重点是 format-spec 迷你语言——这才是格式化的”内核”。

一、三代格式化

新代码请用 f-string,其他两代只用于阅读老代码:

1
2
3
4
5
6
7
8
9
10
name, age = "咖飞", 3

# %(1990 年代)
"我叫 %s,%d 岁" % (name, age)

# str.format()(Python 2.6+)
"我叫 {},{} 岁".format(name, age)

# f-string(Python 3.6+,推荐)
f"我叫 {name}{age} 岁"

三者共享的”格式规范”

无论哪种方式,冒号后面的部分(format spec) 都是同一套迷你语言:

1
2
3
"{:.2f}".format(3.14159)       # '3.14'
f"{3.14159:.2f}" # '3.14'
"%.2f" % 3.14159 # '3.14'

学好 format spec 是关键。

二、format spec 完整语法

格式规范的完整语法(简化版):

1
[[fill]align][sign][#][0][width][,_][.precision][type]
  • fill:填充字符(默认为空格);
  • align:对齐 <(左)、>(右,数字默认)、^(居中)、=(数字符号后填充);
  • sign+(正数显示 +)、-(默认,只显示负号)、 (正数留空格);
  • #:显示进制前缀(0x、0b、0o);
  • 0:数字用 0 填充;
  • width:最小总宽度;
  • , 或 _:千分位分隔符;
  • .precision:小数位数或字符串截断长度;
  • typed/b/o/x/X(进制)、f/e/g(浮点)、s(字符串)、%(百分比)。

三、宽度与对齐

1
2
3
4
5
6
f"{42:5d}"       # '   42'      默认右对齐
f"{42:<5d}" # '42 '
f"{42:^5d}" # ' 42 '
f"{42:*^5d}" # '**42*' 用 * 填充
f"{'py':>10}" # ' py'
f"{'py':-^10}" # '----py----'

四、补零

1
2
3
f"{7:04d}"       # '0007'   4 位宽度,前补 0
f"{-7:04d}" # '-007' 带负号也占位
f"{7:=04d}" # '0007' 显式指定"=对齐"(数字默认)

五、小数位与千分位

1
2
3
4
5
6
7
f"{3.14159:.2f}"       # '3.14'
f"{3.14159:.5f}" # '3.14159'
f"{3:.2f}" # '3.00' 整数也能格式化成 .2f

f"{1234567:,}" # '1,234,567'
f"{1234567.89:,.2f}" # '1,234,567.89'
f"{1234567:_}" # '1_234_567' Python 3.6+ 下划线分隔

六、百分比

1
2
3
f"{0.256:.1%}"    # '25.6%'
f"{0.256:.0%}" # '26%'
f"{-0.05:+.1%}" # '-5.0%' 显式加号

% 会自动 *100 并加百分号,非常方便。

七、进制

1
2
3
4
5
6
f"{255:b}"       # '11111111'
f"{255:o}" # '377'
f"{255:x}" # 'ff'
f"{255:X}" # 'FF'
f"{255:#x}" # '0xff'
f"{255:#010b}" # '0b11111111' # 表示带前缀,010 表示 10 位宽补零

八、正负号

1
2
3
f"{3:+}"         # '+3'
f"{-3:+}" # '-3'
f"{3: }" # ' 3' 正数留空格

九、字符串格式化

1
2
3
f"{'py':10}"     # 'py        '  默认左对齐
f"{'py':>10}" # ' py' 右对齐(字符串默认左)
f"{'long text':.4}" # 'long' 截断到 4 字符

十、日期时间

datetimedatetime 都支持 strftime 格式:

1
2
3
4
5
6
7
from datetime import datetime, date

now = datetime.now()
f"{now:%Y-%m-%d %H:%M:%S}" # '2025-09-10 22:03:19'
f"{now:%Y年%m月%d日}" # '2025年09月10日'
f"{now:%A}" # 'Wednesday'
f"{date.today():%Y/%m/%d}" # '2025/09/10'

十一、动态宽度和精度

宽度、精度可以是变量:

1
2
3
4
5
w = 10
p = 3

f"{3.14159:{w}.{p}f}" # ' 3.142'
f"{3.14159:{'>'}{w}.{p}f}" # 同上,动态对齐

十二、= 调试语法(Python 3.8+)

f-string 的”表达式调试神器”:

1
2
3
4
5
6
7
x, y = 42, 10
print(f"{x=}") # x=42
print(f"{x+y=}") # x+y=52
print(f"{x*y=:.2f}") # x*y=420.00

user = {"name": "咖飞", "age": 3}
print(f"{user['name']=}") # user['name']='咖飞'

以前需要 print("x =", x),现在 f-string 直接搞定,是最实用的现代 Python 语法之一。

十三、r 和 !r 强制 repr

1
2
3
4
5
6
7
8
name = "咖飞"
print(f"{name}") # 咖飞
print(f"{name!r}") # '咖飞' 带引号,输出 repr
print(f"{name!s}") # 咖飞 默认,等价 str
print(f"{name!a}") # '咖飞' ascii-only

# 在调试语法里
print(f"{name=!r}") # name='咖飞'

!r 在打印/日志里非常有用——能明显区分字符串和数字。

十四、嵌套 f-string

f-string 里可以嵌套 f-string,但要小心引号冲突:

1
2
3
4
name = "咖飞"
# Python 3.11 前,引号不能跟外层一样
f'{f"内层 {name}":>10}'
# Python 3.12+ 允许同类型引号嵌套

十五、str.format 的独有能力

format 版本在从外部读格式模板时优于 f-string(因为 f-string 是编译期解析):

1
2
3
4
5
6
7
8
9
10
11
12
template = "{name} 得了 {score} 分"
config_format = template.format(name="咖飞", score=99)

# 命名参数
"{last} {first}".format(first="咖", last="飞")

# 索引参数(可以重复使用)
"{0} {1} {0}".format("a", "b")

# 属性/索引访问
"{p.name}".format(p=some_obj)
"{d[key]}".format(d={"key": "value"})

f-string 里也能做属性和索引访问,只是不能”延迟求值”。

十六、% 老式格式化

只在维护老代码时会用到:

1
2
3
"%s is %d years old" % ("咖飞", 3)
"%-10s | %5.2f" % ("cpu", 3.14)
"%(name)s %(age)d" % {"name": "咖飞", "age": 3}

新代码请一律用 f-string

十七、Locale 与国际化

想要”按当前地区规则显示数字”(比如欧洲用 . 做千分位):

1
2
3
4
5
import locale
locale.setlocale(locale.LC_ALL, "")

# n 格式会用 locale 规则
f"{1234567.89:n}" # 中文环境:'1234567.89'

标准的 , 千分位是”美式”,locale 提供跨地区支持。

十八、一个综合例子:账单打印

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# invoice.py
"""格式化打印一份账单,展示各种 format spec 用法。"""

from datetime import datetime

items = [
("苹果", 10, 3.50),
("面包", 2, 8.00),
("咖啡豆", 1, 128.00),
("巧克力", 5, 15.90),
]

# 表头
print(f"{'咖飞小店':^40}")
print(f"{'账单':^40}")
print(f"{datetime.now():%Y-%m-%d %H:%M:%S}")
print("-" * 40)
print(f"{'商品':<10}{'数量':>6}{'单价':>10}{'小计':>12}")
print("-" * 40)

total = 0
for name, qty, price in items:
subtotal = qty * price
total += subtotal
print(f"{name:<10}{qty:>6d}{price:>10,.2f}{subtotal:>12,.2f}")

print("-" * 40)
print(f"{'合计':<10}{'':>6}{'':>10}{total:>12,.2f}")
print(f"{'税率':<10}{'':>6}{'':>10}{0.13:>12.1%}")
print(f"{'含税':<10}{'':>6}{'':>10}{total * 1.13:>12,.2f}")

输出效果整齐对齐:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
                  咖飞小店
账单
2025-09-10 22:03:19
----------------------------------------
商品 数量 单价 小计
----------------------------------------
苹果 10 3.50 35.00
面包 2 8.00 16.00
咖啡豆 1 128.00 128.00
巧克力 5 15.90 79.50
----------------------------------------
合计 258.50
税率 13.0%
含税 292.11

十九、常见陷阱

  1. f-string 内的引号冲突(3.11 之前):f"{d["key"]}" → 报错。改成 d['key']
  2. 忘了 f 前缀"{name}" 只是字面串,不会替换。
  3. {} 字面:想输出 {,写 {{`;`}}
  4. 格式化 Nonef"{None:.2f}" 会报错,先判空或转字符串。
  5. %s 后面变量个数不匹配"%s %s" % ("a",) 会 TypeError。f-string 没这个问题。
  6. str.format(**d) 里 key 是数字或有非法字符:得用位置或 format_map
  7. 精度对整数无效f"{100:.2f}" 会输出 100.00(自动转 float),但 f"{100:.2d}" 报错——d 是整数格式,不接受精度。

二十、小结与延伸阅读

  • 新代码永远用 f-string;模板从外部读时用 str.format()
  • 冒号后面是 format spec 迷你语言:宽度、对齐、填充、千分位、精度、进制;
  • % 系列被 f-string 全面取代,只出现在老代码;
  • = 调试语法、!r 强制 repr、日期时间 %Y-%m-%d 都要掌握;
  • Python 3.12 前 f-string 内引号有限制;
  • 千分位用 ,,百分比用 .1%,日期用 strftime 语法。

延伸阅读:

下一篇 序列切片深入解析 我们要把 Python 切片的门道彻底讲清。

列表推导式与生成表达式

推导式(comprehension)是 Python 里最有辨识度的语法糖。当你在别人的代码里看到 [x*2 for x in nums if x > 0] 这种写法,第一反应是”哇,好 Pythonic”。这一篇要把推导式从最基础的列表推导式讲到字典推导、集合推导,以及”看似很像但性能差很多”的生成表达式。学完这一篇,你写的代码会立刻精简 30%。

一、为什么要有推导式

先看不用推导式的写法:

1
2
3
squares = []
for x in range(10):
squares.append(x * x)

用列表推导式:

1
squares = [x * x for x in range(10)]

好处:

  1. 简洁:一行搞定;
  2. 明确[...] 一眼看出是列表;
  3. 更快:CPython 里推导式比 append 循环快 20%-50%。

二、列表推导式的语法

1
[表达式 for 变量 in 可迭代对象 if 条件]
  • 表达式:每次迭代的输出;
  • for 子句:数据源,可以有多个;
  • if 子句:过滤,可选,可以有多个。

基本形式

1
2
3
4
5
6
7
8
# 平方
[x*x for x in range(5)] # [0, 1, 4, 9, 16]

# 过滤:只要正数
[x for x in [-2, -1, 0, 1, 2] if x > 0] # [1, 2]

# 表达式可以是函数调用
[str(x).zfill(3) for x in range(5)] # ['000', '001', '002', '003', '004']

多个 if

1
2
3
4
# 既大于 0 又是偶数
[x for x in nums if x > 0 if x % 2 == 0]
# 等价于
[x for x in nums if x > 0 and x % 2 == 0]

if-else 在表达式部分

1
2
# 正数保留,负数变 0
[x if x > 0 else 0 for x in nums]

注意语法位置:

  • 过滤iffor 后面;
  • 三元if-else 在表达式部分,for 前面。

多层 for

1
2
3
4
5
6
7
8
# 所有 (i, j) 对
[(i, j) for i in range(3) for j in range(3)]
# [(0, 0), (0, 1), (0, 2), (1, 0), ..., (2, 2)]

# 展平二维列表
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flat = [x for row in matrix for x in row]
# [1, 2, 3, 4, 5, 6, 7, 8, 9]

读法:从左到右读 for,就像嵌套 for 循环从外到里。

但不要超过 2 层,否则可读性崩塌,改回普通循环。

三、字典推导式

用大括号,表达式部分写成 key: value

1
2
3
4
5
6
7
8
9
10
11
12
13
# 平方表
{n: n*n for n in range(5)}
# {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

# 反转字典
inv = {v: k for k, v in d.items()}

# 过滤字典
adults = {name: age for name, age in people.items() if age >= 18}

# 从两个列表 zip 出字典
d = {k: v for k, v in zip(keys, values)}
# 更简单:d = dict(zip(keys, values))

四、集合推导式

大括号加”没有冒号的表达式”:

1
2
3
4
{x % 3 for x in range(10)}       # {0, 1, 2}

# 从文本抽取所有独立单词
{w.lower() for w in text.split() if w.isalpha()}

五、生成表达式

把中括号换成小括号,就是生成表达式

1
2
3
4
5
6
7
gen = (x*x for x in range(10))
print(gen) # <generator object ...>
print(next(gen)) # 0
print(next(gen)) # 1

for x in gen:
print(x)

看起来像列表推导式,但它不会立刻算出所有元素——只在你需要时才产生下一个。

什么时候用生成表达式

巨大数据、只遍历一次、不需要索引 时用生成器:

1
2
3
4
5
# ❌ 内存爆炸:一次性生成 1 亿个数字
sum([x*x for x in range(10**8)])

# ✅ 内存友好:逐个产生
sum(x*x for x in range(10**8))

函数调用作为唯一参数时,括号可以省略:

1
2
sum(x*x for x in range(10))               # 括号省略
max((x.age for x in users), default=0) # 有多个参数就不能省

生成器还有很多深入内容,我们在 生成器 yield 深入 单独讲。

六、推导式的常见套路

1. 元素转换

1
2
uppercased = [w.upper() for w in words]
strs = [str(x) for x in nums]

2. 过滤

1
positives = [x for x in nums if x > 0]

3. 转换 + 过滤

1
even_squares = [x*x for x in range(10) if x % 2 == 0]

4. 从 (key, value) 序列构造字典

1
{k: v for k, v in pairs}

5. 交换字典 key/value

1
inv = {v: k for k, v in d.items()}

6. 分类计数(不用 Counter)

1
2
3
4
5
6
7
8
from collections import defaultdict
counts = defaultdict(int)
for w in words:
counts[w] += 1

# 用 Counter 更好
from collections import Counter
counts = Counter(words)

7. 二维矩阵

1
matrix = [[0] * cols for _ in range(rows)]

8. 转置矩阵

1
2
3
4
5
# 用 zip
transposed = list(zip(*matrix))

# 用推导式
transposed = [[row[i] for row in matrix] for i in range(len(matrix[0]))]

七、变量作用域

Python 3 里,推导式有自己的作用域,循环变量不泄漏:

1
2
3
4
5
6
7
[x for x in range(3)]
print(x) # NameError!

# 对比 for 循环
for x in range(3):
pass
print(x) # 2,泄漏了

生成表达式也有自己的作用域。这个隔离是好事,避免污染外层。

八、什么时候不要用推导式

推导式虽然简洁,但过度使用会毁掉可读性。以下情况请回归普通循环:

  1. 逻辑复杂——表达式部分有 3 个以上函数调用,或多个三元;
  2. 副作用——推导式的核心是”产生值”,如果只是想 print 或调用 side-effect 函数,用循环;
  3. 多层嵌套——超过两层 for 或含多个 if 时;
  4. 需要 break——推导式没法 break,配合 next(g for g in ... if ...) 更好。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# ❌ 反面教材:又长又难懂
result = [
(u.name.strip(), age)
for u in users
if u.active
for age in [max(0, u.age - u.exp)]
if age > 18
]

# ✅ 拆成普通循环,可读性强
result = []
for u in users:
if not u.active:
continue
age = max(0, u.age - u.exp)
if age > 18:
result.append((u.name.strip(), age))

九、性能对比

以计算 100 万个平方为例:

方式 耗时(相对)
列表推导式 1.0(基准)
for + append 1.3–1.5
map + list 0.9
生成表达式 + sum 内存 0,耗时 0.9

规律:推导式一般是”简洁+性能”的甜点。如果只是要 sum/max/any/all,生成表达式更省内存。

十、一个综合例子:日志聚合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# logs.py
"""从一堆日志行里聚合 IP 请求数(推导式风暴演示)。"""

logs = [
"2025-09-10 08:00:11 GET /index 200 1.2.3.4",
"2025-09-10 08:00:12 GET /login 200 5.6.7.8",
"2025-09-10 08:00:13 POST /login 401 1.2.3.4",
"2025-09-10 08:00:14 GET /profile 500 1.2.3.4",
"2025-09-10 08:00:15 GET /index 200 9.9.9.9",
]

# 1. 解析每一行为字段
parsed = [line.split() for line in logs]

# 2. 提取所有 IP
ips = [row[-1] for row in parsed]

# 3. 去重 IP 集合
unique_ips = {row[-1] for row in parsed}

# 4. IP → 请求次数
from collections import Counter
ip_counts = Counter(ips)

# 5. 只保留 5xx 错误的日志
errors = [row for row in parsed if row[3].startswith("5")]

# 6. 每个 IP 的成功率
attempts = Counter(row[-1] for row in parsed)
successes = Counter(row[-1] for row in parsed if row[3] == "200")
rates = {ip: successes[ip] / attempts[ip] for ip in attempts}

print("IP 请求:", dict(ip_counts))
print("错误:", errors)
print("成功率:", rates)

十一、常见陷阱

  1. if-else 位置搞错:过滤 if 在末尾,三元 if-else 在表达式部分。
  2. 在推导式里改变外部状态:推导式的语义是”产生一个新容器”,副作用会让人读起来头大。
  3. 同一变量名多层使用[x for x in xs for x in ys] 语法合法但含义奇怪,别写。
  4. 生成器只能遍历一次gen = (x for x in range(3)); list(gen); list(gen) 第二次是空的。
  5. 列表推导式内存占用:处理海量数据时先想想能不能改成生成表达式或 map
  6. 和 map/filter 的选择
    • 简单转换:[str(x) for x in nums]list(map(str, nums)) 都行,推导式更清晰;
    • 有过滤:推导式更好,别 list(filter(..., map(...)))

十二、小结与延伸阅读

  • 推导式是 Python 最有辨识度的语法糖,简洁又高效;
  • 列表 [...]、字典 {k: v ...}、集合 {...}、生成器 (...)
  • 结构:表达式 for 变量 in 数据 if 过滤
  • 三元 A if cond else B 放在表达式部分;
  • 多层 for、多个 if 都行,但超过 2 层就该拆;
  • 大数据、只遍历一次 → 生成表达式,省内存;
  • 复杂逻辑请回归普通循环。

延伸阅读:

下一篇 字符串格式化进阶 我们把字符串格式化的所有姿势彻底讲透。

集合 Set 与集合运算

集合(set)是 Python 中经常被”低估”的容器。很多人只知道它”能去重”,但其实它还是判断存在性最快的数据结构,也是数学意义上”集合运算”(交并差)的最佳载体。这一篇把 set 与 frozenset 讲透,让你知道它什么时候比列表和字典都好用。

一、什么是集合

集合是一个 可变无序不重复 的容器:

1
2
s = {1, 2, 3, 4, 3, 2}    # 自动去重
print(s) # {1, 2, 3, 4}

关键约束:

  • 元素必须可哈希:跟 dict 的 key 一样,list/dict/set 不能进 set;
  • 元素唯一:重复元素自动合并;
  • 无序for x in s 的顺序没有保证(跟 dict 不同,set 不保证插入顺序)。

二、创建集合

1
2
3
4
5
6
7
8
9
10
11
12
# 字面量
s1 = {1, 2, 3}

# 空集合注意:不能写 {},那是空字典!
empty_set = set()

# 从可迭代对象
s2 = set("hello") # {'h', 'e', 'l', 'o'}
s3 = set([1, 2, 2, 3]) # {1, 2, 3}

# 集合推导式(第 15 篇)
s4 = {x*x for x in range(5)} # {0, 1, 4, 9, 16}

三、增删改查

1
2
3
4
s = {1, 2, 3}
s.add(4) # {1, 2, 3, 4}
s.update([5, 6, 7]) # 批量添加,可以传任何可迭代对象
s.update({8, 9}, [10]) # 多个也行

1
2
3
4
s.remove(1)          # 不存在会抛 KeyError
s.discard(999) # 不存在也不报错,推荐用它
popped = s.pop() # 弹出任意一个元素(因为无序,"任意"就是任意)
s.clear() # 清空

1
2
3
2 in s          # O(1),超快
9 not in s # 也 O(1)
len(s) # 元素个数

四、集合运算

这是 set 的真正杀手锏——数学意义上的集合操作

1
2
3
4
5
6
7
8
9
10
11
12
13
a = {1, 2, 3, 4}
b = {3, 4, 5, 6}

print(a | b) # 并集 {1, 2, 3, 4, 5, 6}
print(a & b) # 交集 {3, 4}
print(a - b) # 差集 {1, 2}
print(a ^ b) # 对称差 {1, 2, 5, 6}

# 也可以用方法名
a.union(b)
a.intersection(b)
a.difference(b)
a.symmetric_difference(b)

方法名版本比运算符更强:可以接受任何可迭代对象,不必是 set:

1
2
a.union([5, 6, 7])           # 传列表 OK
a & b # 只能是集合

就地版

1
2
3
4
5
6
7
8
9
a |= b       # a = a | b
a &= b
a -= b
a ^= b

a.update(b)
a.intersection_update(b)
a.difference_update(b)
a.symmetric_difference_update(b)

子集与超集

1
2
3
4
5
{1, 2}.issubset({1, 2, 3})       # True
{1, 2, 3}.issuperset({1, 2}) # True
{1, 2}.isdisjoint({3, 4}) # True,无交集
{1, 2} <= {1, 2, 3} # True,子集
{1, 2} < {1, 2, 3} # True,真子集

五、去重的三种姿势

list → set → list 是 Python 里最短的去重代码:

1
2
3
lst = [1, 2, 2, 3, 3, 3]
unique = list(set(lst))
print(unique) # [1, 2, 3],但顺序不保证!

如果要保序去重(Python 3.7+):

1
2
3
4
5
6
7
8
9
10
# 利用 dict 的插入顺序
unique = list(dict.fromkeys(lst)) # [1, 2, 3],顺序保留

# 或者手写
seen = set()
result = []
for x in lst:
if x not in seen:
seen.add(x)
result.append(x)

六、set 用于快速查找

set 的 in 是 O(1) 平均,比 list 的 O(n) 快得多:

1
2
3
4
5
6
7
8
9
10
11
# ❌ 慢
def has_dupes(items):
for i, x in enumerate(items):
for j, y in enumerate(items):
if i != j and x == y:
return True
return False

# ✅ 快
def has_dupes(items):
return len(set(items)) != len(items)

处理”两个大列表的共同项”、”某个 id 是否在黑名单里”,都应该先把小的一方转成 set。

七、frozenset:不可变集合

frozenset 是 set 的不可变版本——一旦创建不能改:

1
2
fs = frozenset([1, 2, 3])
fs.add(4) # ❌ AttributeError

用途:

  • 可以作为 dict 的 key(普通 set 不行,因为可变);
  • 可以作为另一个 set 的元素;
  • 表示”这份集合是配置常量,不许改”。
1
BAD_WORDS = frozenset({"spam", "junk"})

八、性能特性

  • inaddremovediscard:平均 O(1);
  • 集合运算 O(|a| + |b|);
  • 每个 set 起步内存约 200 字节;
  • 元素必须可哈希,且 __hash____eq__ 一致(自定义类要小心)。

九、一个综合例子:日活跃用户分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# users.py
"""对每天登录用户做集合分析。"""

# 日 → 登录用户 id 集合
daily = {
"2025-09-01": {1, 2, 3, 4, 5},
"2025-09-02": {2, 3, 5, 6, 7},
"2025-09-03": {1, 3, 6, 7, 8},
}


def total_users(daily: dict) -> set:
"""全部有过登录的用户。"""
result = set()
for users in daily.values():
result |= users
return result


def core_users(daily: dict) -> set:
"""所有日子都登录的用户(交集)。"""
days = list(daily.values())
return days[0].intersection(*days[1:])


def new_users(daily: dict) -> dict:
"""每一天相比前一天新增的用户。"""
dates = sorted(daily)
seen = set()
result = {}
for date in dates:
result[date] = daily[date] - seen
seen |= daily[date]
return result


def churn(daily: dict) -> dict:
"""每一天相比前一天流失的用户。"""
dates = sorted(daily)
result = {}
for prev, curr in zip(dates, dates[1:]):
result[curr] = daily[prev] - daily[curr]
return result


if __name__ == "__main__":
print("总用户:", total_users(daily))
print("核心用户:", core_users(daily))
print("新用户:", new_users(daily))
print("流失:", churn(daily))

十、常见陷阱

陷阱 1:{} 是空字典不是空集合

1
2
e1 = {}         # 空字典
e2 = set() # 空集合

陷阱 2:不可哈希元素

1
2
{[1, 2]}        # ❌ TypeError: unhashable type: 'list'
{(1, 2)} # ✅ 元组可以

陷阱 3:set 不保证顺序

1
2
3
s = {"a", "b", "c"}
for x in s:
print(x) # 输出顺序在不同版本/进程里可能变

需要顺序,用有序容器(list、dict)。

陷阱 4:集合运算的”就地”版本容易搞错

1
2
3
a = {1, 2, 3}
b = a & {2, 3, 4} # b 是新集合,a 不变
a &= {2, 3, 4} # a 就地更新

陷阱 5:修改 set 里的可变元素后,哈希失效

自定义类如果 __hash__ 依赖可变字段,改了字段后 set 的行为会崩坏:

1
2
3
4
5
6
7
8
class User:
def __init__(self, id): self.id = id
def __hash__(self): return hash(self.id)
def __eq__(self, o): return self.id == o.id

u = User(1)
s = {u}
u.id = 2 # ❌ 改了 hash 依赖的字段,s 里再也找不到 u

结论:可哈希对象的哈希应该基于不可变字段。

十一、set 与 dict 的关系

set 可以理解为”只有 key 没有 value 的 dict”:

  • 都用哈希表实现;
  • 元素/key 都要可哈希;
  • in 都是 O(1)。

甚至 Python 内部的实现,set 就是从 dict 简化来的。

十二、小结与延伸阅读

  • set 是可变、无序、不重复的容器;
  • 元素必须可哈希;
  • 支持并集/交集/差集/对称差;
  • 空集合用 set(),不是 {}
  • 大量查找用 set 而不是 list;
  • 不可变版本 frozenset 可以做 dict key;
  • 保序去重用 dict.fromkeys

延伸阅读:

下一篇 列表推导式与生成表达式 我们讲 Python 里最优雅的语法糖之一。

字典 Dict 的使用

字典是 Python 里另一个”灵魂容器”。它把”键 → 值”的映射用最直观的方式呈现出来,几乎所有 Python 项目都离不开它——JSON、配置、缓存、计数器、图的邻接表,能用字典表达的场景多到数不清。这一篇要把字典讲透:创建、增删改查、遍历、常用方法、Python 3.7+ 顺序保证、以及新手最容易踩的键类型和默认值陷阱。

一、什么是字典

字典(dict)是一个 可变 的、键值对(key-value) 集合。用大括号加冒号创建:

1
2
3
empty = {}
person = {"name": "咖飞", "age": 3, "city": "北京"}
config = {"host": "localhost", "port": 8080, "debug": True}

关键约束:

  • key 必须是可哈希的:字符串、数字、元组(内部也是不可变)都行;list/dict/set 不行;
  • key 唯一:同名 key 后写的覆盖先写的;
  • value 什么都行:任意 Python 对象。

二、创建字典的多种方式

1
2
3
4
5
6
7
8
9
10
11
12
13
# 字面量
d1 = {"a": 1, "b": 2}

# dict() 构造函数
d2 = dict(a=1, b=2) # 关键字参数
d3 = dict([("a", 1), ("b", 2)]) # 元组列表
d4 = dict(zip(["a", "b"], [1, 2])) # zip 出来

# 字典推导式(下一节讲)
d5 = {c: ord(c) for c in "abc"} # {'a': 97, 'b': 98, 'c': 99}

# 从 keys 构造相同 value
d6 = dict.fromkeys(["a", "b", "c"], 0) # {'a': 0, 'b': 0, 'c': 0}

陷阱dict.fromkeys(["a", "b"], []) 会让所有 key 指向同一个空列表!要独立列表用推导式:{k: [] for k in keys}

三、增删改查

1
2
3
4
5
6
7
8
9
10
d = {"name": "咖飞", "age": 3}

print(d["name"]) # 咖飞
print(d["gender"]) # KeyError!

print(d.get("gender")) # None,没有 key 返回 None
print(d.get("gender", "未知")) # 未知,可指定默认值

print("name" in d) # True
print("gender" not in d) # True

规则:确定 key 存在用 d[k],可能不存在用 d.get(k)。前者更快,后者更安全。

改与增

1
2
3
4
5
d["age"] = 4              # 修改已有 key
d["gender"] = "unknown" # 新增 key

d.update({"age": 5, "city": "上海"}) # 批量更新
d.update(age=6, city="广州") # 关键字参数形式

Python 3.9+ 还有合并运算符 |

1
2
3
4
a = {"x": 1, "y": 2}
b = {"y": 20, "z": 30}
print(a | b) # {'x': 1, 'y': 20, 'z': 30} b 覆盖 a
a |= b # 就地更新

1
2
3
4
5
6
7
d = {"a": 1, "b": 2, "c": 3}

del d["a"] # 找不到会 KeyError
d.pop("b") # 返回 2 并删除
d.pop("z", None) # 找不到不报错,返回 None
d.popitem() # 弹出并返回最后插入的 (key, value)
d.clear() # 清空

四、遍历字典

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
score = {"张三": 90, "李四": 85, "王五": 78}

# 遍历 key(默认)
for name in score:
print(name)

# 显式遍历 keys
for name in score.keys():
print(name)

# 遍历 value
for s in score.values():
print(s)

# 同时遍历 key 和 value(最常用)
for name, s in score.items():
print(name, s)

keys / values / items 是视图

它们不是列表,是动态视图——字典变,它们也跟着变:

1
2
3
4
d = {"a": 1, "b": 2}
keys = d.keys()
d["c"] = 3
print(keys) # dict_keys(['a', 'b', 'c']),视图自动看到新 key

需要真正的列表用 list(d.keys())

注意:遍历字典时不能修改字典结构(增删 key):

1
2
3
for k in d:
if d[k] == 1:
del d[k] # RuntimeError!

正确做法:遍历 list(d) 或用推导式重建:

1
d = {k: v for k, v in d.items() if v != 1}

五、Python 3.7+ 有序性保证

Python 3.7 起,dict 官方保证按插入顺序遍历

1
2
3
4
5
d = {}
d["c"] = 1
d["a"] = 2
d["b"] = 3
list(d) # ['c', 'a', 'b'] 顺序保留

3.6 里 CPython 已经这样实现了,但只是”实现细节”;3.7 才写进语言规范。之前需要有序字典要用 collections.OrderedDict,现在不需要了,除非你需要 OrderedDict 独有的 move_to_end 方法。

六、字典常用方法速查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
d = {"a": 1, "b": 2}

d.keys() # dict_keys(['a', 'b'])
d.values() # dict_values([1, 2])
d.items() # dict_items([('a', 1), ('b', 2)])

d.get(k, default) # 查(不抛异常)
d.pop(k, default) # 删(不抛异常)
d.setdefault(k, default) # k 存在返回值,不存在则设为 default 并返回

# setdefault 常用于分组
groups = {}
for x in items:
groups.setdefault(x.type, []).append(x)

七、字典推导式

推导式(第 15 篇讲)也适用于字典:

1
2
3
4
5
6
7
8
9
# 平方表
squares = {n: n*n for n in range(5)}
# {0: 0, 1: 1, 2: 4, 3: 9, 4: 16}

# 反转 key 和 value
inv = {v: k for k, v in d.items()}

# 带条件
adults = {name: age for name, age in people.items() if age >= 18}

八、嵌套字典

字典的值可以是任何对象,包括字典本身:

1
2
3
4
5
6
7
users = {
"u1": {"name": "咖飞", "age": 3},
"u2": {"name": "小王", "age": 20},
}

print(users["u1"]["name"]) # 咖飞
users["u1"]["age"] = 4

深层嵌套的取值和赋值容易出 KeyError,几种应对:

1
2
3
4
5
6
7
8
9
10
# 1. 逐层 get
name = users.get("u1", {}).get("name", "匿名")

# 2. try
try:
x = users["u1"]["profile"]["email"]
except KeyError:
x = None

# 3. defaultdict 或专门的库(jmespath / dpath)

九、defaultdict:自动生成缺失值

collections.defaultdict 是字典的增强版:访问不存在的 key 时自动创建

1
2
3
4
5
6
7
8
9
10
11
from collections import defaultdict

# 分组
groups = defaultdict(list)
for name, dept in employees:
groups[dept].append(name) # 不用 setdefault

# 计数
counter = defaultdict(int)
for word in words:
counter[word] += 1 # 不用先判空

十、Counter:计数专用字典

collections.Counter 是专为计数设计的字典:

1
2
3
4
5
6
7
8
9
10
11
12
13
from collections import Counter

words = ["a", "b", "a", "c", "b", "a"]
c = Counter(words)
print(c) # Counter({'a': 3, 'b': 2, 'c': 1})
print(c.most_common(2)) # [('a', 3), ('b', 2)]
print(c["z"]) # 0,不存在返回 0 而不是 KeyError

# 加减、交集、并集
c1 = Counter("apple")
c2 = Counter("banana")
print(c1 + c2) # 元素级求和
print(c1 & c2) # 取共同的最小计数

十一、字典作为高性能查找表

字典的 x in dd[x] 都是 平均 O(1),比列表的线性查找快得多。所以大量存在性判断或映射查找请优先用字典或集合。

1
2
3
4
5
6
7
8
9
10
# ❌ O(n * m):慢
result = []
for x in big_list:
for y in small_list:
if x == y:
result.append(x)

# ✅ O(n + m):快
lookup = set(small_list)
result = [x for x in big_list if x in lookup]

十二、性能与内存

  • 每个 dict 起步内存约 200-300 字节;
  • 存储 1 亿个 int→int 大约 5-10GB,别指望在 8GB 内存里塞进去;
  • Python 3.6 起 dict 内存布局做过优化,比早期版本省 20%-25%;
  • 大量小 dict 场景可以考虑 dataclass(slots) 或者 namedtuple。

十三、一个综合例子:词频统计器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# wordfreq.py
"""统计一段文本中的词频,输出 Top N。"""

import re
from collections import Counter


def tokenize(text: str) -> list[str]:
"""粗糙分词:抽取所有连续字母。中文用户请上 jieba。"""
return re.findall(r"[a-zA-Z一-龥]+", text.lower())


def top_n(text: str, n: int = 5) -> list[tuple[str, int]]:
words = tokenize(text)
return Counter(words).most_common(n)


def stats(text: str) -> dict:
words = tokenize(text)
counter = Counter(words)
return {
"total_words": len(words),
"unique_words": len(counter),
"top_5": counter.most_common(5),
"longest": max(words, key=len) if words else "",
}


if __name__ == "__main__":
sample = """
Python is great. Python is powerful. Python is easy.
Python 是伟大的编程语言,Python 简单易学。
"""
print(stats(sample))

十四、常见陷阱

  1. 可变对象做 keyd[[1, 2]] = "x" → TypeError,list 不可哈希。用元组 (1, 2)
  2. 同一 key 被覆盖{"a": 1, "a": 2} 结果是 {"a": 2},不会警告。
  3. dict.fromkeys(..., []) 共享列表:改一个动全部。
  4. 遍历中修改字典:会抛 RuntimeError。遍历 list(d)d.copy()
  5. 拷贝浅d2 = d1.copy() 是浅拷贝,嵌套字典/列表要用 copy.deepcopy
  6. Python 3.7 前顺序不保证:老代码依赖顺序的话检查一下 Python 版本。
  7. in 只查 key:判断 value 是否存在要 x in d.values()

十五、小结与延伸阅读

  • 字典是最常用的容器之一,几乎所有映射场景都用它;
  • key 必须可哈希,value 任意;
  • get / setdefault / update / pop 是最常用的方法;
  • Python 3.7+ 保证插入顺序;
  • keys/values/items 是视图,不是列表;
  • 遍历中不要修改字典结构;
  • collections.defaultdict / Counter / OrderedDict 是字典家族的强力扩展;
  • 大批量查找永远用 dict/set 而不是 list。

延伸阅读:

下一篇 集合 Set 与集合运算 我们讲字典的兄弟——集合。

元组 Tuple 与不可变序列

初学者常问:”元组不就是不能改的列表吗?为什么 Python 还要专门搞一个?” 简短答案:元组不是”列表的阉割版”,它有自己的核心用途——表示不可变的记录。列表适合”一堆同类型的东西”,元组适合”一件事的多个字段”。学完这一篇,你会理解为什么函数返回多值用元组、为什么字典 key 可以是元组、为什么大量 Python 代码里都是圆括号包起来的一小串数据。

一、创建元组

元组用小括号:

1
2
3
4
5
6
7
empty = ()
one = (1,) # 只有一个元素时必须加逗号!
two = (1, 2)
three = 1, 2, 3 # 括号可以省,逗号才是元组的标志

point = (3.14, 2.71)
person = ("咖飞", 25, "北京")

只有一个元素的元组必须带逗号,否则 Python 会当成”括号 + 表达式”:

1
2
3
a = (1)      # a 是整数 1
b = (1,) # b 是元组 (1,)
c = 1, # c 也是元组 (1,)

tuple() 构造函数

1
2
3
tuple("abc")           # ('a', 'b', 'c')
tuple([1, 2, 3]) # (1, 2, 3)
tuple(range(3)) # (0, 1, 2)

二、索引与切片

元组和列表在读取上几乎一样:

1
2
3
4
5
6
7
8
t = (10, 20, 30, 40, 50)

print(t[0]) # 10
print(t[-1]) # 50
print(t[1:3]) # (20, 30)
print(t[::-1]) # (50, 40, 30, 20, 10)
print(len(t)) # 5
print(30 in t) # True

区别在于:

1
2
t[0] = 99          # ❌ TypeError: 'tuple' object does not support item assignment
t.append(60) # ❌ AttributeError: no 'append'

元组不可变——一旦创建,不能修改、不能增加、不能删除元素。

三、元组的常用方法

元组的方法只有两个

1
2
3
t = (1, 2, 3, 2, 1)
print(t.count(2)) # 2
print(t.index(3)) # 2

对比列表 30+ 个方法,元组显得非常”精简”,这正是它的定位——尽量减少 API,只做”记录”用

四、元组的核心用途

1. 表达一条记录(结构化数据)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 一个点是 (x, y)
p = (3, 4)

# 一个用户是 (id, name, age)
user = (1, "咖飞", 25)

# 一批数据
users = [
(1, "咖飞", 25),
(2, "小王", 30),
(3, "小李", 28),
]

for uid, name, age in users:
print(f"{uid} {name} {age}")

这种”轻量记录”场景,元组比列表更合适——语义是”一个整体”,字段个数固定,顺序有意义。

2. 多值返回

Python 函数返回多值本质上就是返回元组:

1
2
3
4
5
def divmod_(a, b):
return a // b, a % b # 返回元组

q, r = divmod_(17, 5) # 元组解包
print(q, r) # 3 2

不用 return q, r 打包成元组,再解包,一步到位。

3. 作为字典的 key

元组不可变,所以可哈希,能做 dict 的 key(列表不行):

1
2
3
# 二维网格的位置 → 值
grid = {(0, 0): "起点", (2, 3): "宝箱"}
print(grid[(0, 0)])

4. 作为集合元素

同理,元组能进 set:

1
seen = {("a", 1), ("b", 2)}

5. 函数参数打包/解包

1
2
3
4
5
6
7
def sum_all(*args):     # args 是元组
return sum(args)

sum_all(1, 2, 3) # 6

nums = (1, 2, 3)
print(sum(nums)) # 也能直接用

五、序列解包

元组和”解包赋值”是最佳搭档:

1
2
3
4
5
6
7
8
9
10
11
# 简单解包
x, y = (3, 4)

# 允许任何可迭代对象
x, y = [3, 4]
a, b, c = "abc"

# 交换变量:一行搞定
a, b = 1, 2
a, b = b, a
print(a, b) # 2 1

扩展解包(Python 3.0+)

1
2
3
first, *rest = [1, 2, 3, 4]      # first=1, rest=[2, 3, 4]
*init, last = [1, 2, 3, 4] # init=[1, 2, 3], last=4
a, *mid, b = [1, 2, 3, 4, 5] # a=1, mid=[2, 3, 4], b=5

*rest 收集剩余的元素成为列表(不是元组,注意)。

忽略部分值

1
2
3
4
_, _, age = user       # 只想要 age

for _, name, _ in users:
print(name)

_ 只是约定俗成的”我不关心这个变量”,本身还是一个普通变量。

六、元组不可变 ≠ 内容不可变

这是新手最容易误解的一点:

1
2
3
4
5
t = ([1, 2], [3, 4])
t[0].append(99) # 元组本身没变,但内部的列表变了
print(t) # ([1, 2, 99], [3, 4])

t[0] = [7, 8] # ❌ 不能替换外层槽位

元组的”不可变”只保证内部指针不换,指向的对象本身如果是可变对象,仍然可以被修改。要真正的完全不可变,用 frozenset 或者 namedtuple 存不可变元素。

七、namedtuple:给元组的字段起名字

用元组的最大痛点:字段是按位置访问的(user[1]),代码可读性差。collections.namedtuple 解决这个问题:

1
2
3
4
5
6
7
8
9
10
11
from collections import namedtuple

Point = namedtuple("Point", ["x", "y"])
p = Point(3, 4)

print(p.x, p.y) # 3 4
print(p[0], p[1]) # 3 4,也能按索引
print(p) # Point(x=3, y=4)

# 解包也支持
x, y = p

namedtuple 的优点:

  • 不可变(继承了元组);
  • 有名字(比裸元组可读得多);
  • 兼容元组(旧代码不用改);
  • 内存开销小(跟普通元组一样,比 class 省内存)。

Python 3.6+ 还有更强大的 typing.NamedTuple

1
2
3
4
5
6
7
8
9
10
11
12
13
from typing import NamedTuple

class Point(NamedTuple):
x: float
y: float
label: str = "" # 默认值

def distance_to(self, other):
return ((self.x - other.x)**2 + (self.y - other.y)**2) ** 0.5

p = Point(3, 4)
q = Point(0, 0, label="origin")
print(p.distance_to(q)) # 5.0

八、元组 vs 列表:什么时候用哪个

场景 首选
一堆同类型元素,数量会变 list
一条固定字段的记录 tuple / namedtuple
函数返回多个值 tuple
作为 dict 的 key、set 的元素 tuple
数据在整个生命周期不变 tuple
需要 append / sort / 反复修改 list

心法列表是同质异量,元组是异质同量

九、性能对比

  • 元组比列表创建更快、占内存更小(列表要额外维护容量);
  • 迭代速度差不多;
  • 元组可缓存哈希,可以做 dict key;
  • 元组一般更适合作为常量数据。
1
2
3
import sys
print(sys.getsizeof((1, 2, 3, 4, 5))) # 元组约 80 字节
print(sys.getsizeof([1, 2, 3, 4, 5])) # 列表约 120 字节

(具体数字随平台变,趋势不变。)

十、一个综合例子:坐标聚类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# clustering.py
"""按整数网格给一组坐标分桶。"""

from collections import defaultdict, namedtuple

Point = namedtuple("Point", ["x", "y", "label"])


def bucket_key(p: Point, size: float) -> tuple:
"""把点归入 size*size 的网格,返回 (bx, by) 作为桶 key。"""
return (int(p.x // size), int(p.y // size))


def cluster(points: list[Point], size: float) -> dict:
buckets = defaultdict(list)
for p in points:
buckets[bucket_key(p, size)].append(p)
return dict(buckets)


if __name__ == "__main__":
pts = [
Point(1.2, 3.4, "A"),
Point(1.8, 3.9, "B"),
Point(4.5, 4.5, "C"),
Point(4.1, 4.7, "D"),
Point(9.9, 9.9, "E"),
]
result = cluster(pts, size=2.0)
for k, group in result.items():
print(f"网格 {k}: {[p.label for p in group]}")

代码用到了 namedtuple、元组做 dict key、defaultdict 等特性。

十一、常见陷阱

  1. 单元素元组必须加逗号(1) 是整数,(1,) 才是元组;
  2. 元组不可变但可以嵌套可变对象
  3. +* 会创建新元组(因为元组不能就地修改);
  4. t += (x,) 也是创建新元组,如果原元组被别的名字引用,那个名字看到的还是旧元组;
  5. 括号有时会被当成分组符(1) 不是元组,别忘了逗号;
  6. 元组不能 sortsorted(tuple) 返回列表,不是元组;要保留元组:tuple(sorted(t))

十二、小结与延伸阅读

  • 元组用小括号(可省略),单元素必须加逗号;
  • 元组不可变,索引/切片只读;
  • 核心用途:结构化记录、多值返回、字典 key、扩展解包;
  • 元组不可变 ≠ 内部对象不可变;
  • 需要给字段起名字用 namedtuple / typing.NamedTuple;
  • 数据不需要变就用元组,需要 append/sort 就用列表。

延伸阅读:

下一篇 字典 Dict 的使用 我们讲 Python 里另一个”半壁江山”的容器——字典。

列表 List 详解

列表(list)是 Python 里使用最频繁的容器类型。要说 Python 的”灵魂容器”,字典可能占一半,列表就占另一半。学完基础后,几乎每一个项目、每一段脚本,你都会用到列表。这一篇要把列表讲通透:怎么创建、怎么增删改查、切片、常用方法、性能特性、以及一些新手才踩得到的坑。

一、什么是列表

Python 的列表是一个 可变有序可存放任意类型元素的容器。用中括号创建:

1
2
3
empty = []
nums = [1, 2, 3, 4, 5]
mixed = [1, "hello", 3.14, True, None, [1, 2]] # 什么都能装

跟 C 的数组、Java 的 ArrayList 类似,但 Python 的列表不要求元素类型一致。底层是一段连续的指针数组,指向真实对象。

二、创建列表的多种方式

1
2
3
4
5
6
7
8
9
10
11
12
13
# 字面量
a = [1, 2, 3]

# list() 构造函数(从可迭代对象转)
b = list("abc") # ['a', 'b', 'c']
c = list(range(5)) # [0, 1, 2, 3, 4]
d = list((1, 2, 3)) # [1, 2, 3]

# 列表推导式(第 15 篇讲)
e = [x * 2 for x in range(5)] # [0, 2, 4, 6, 8]

# 重复元素
zeros = [0] * 5 # [0, 0, 0, 0, 0]

陷阱[[]] * 3 得到的是三个指向同一个内层列表的引用!

1
2
3
4
5
6
grid = [[]] * 3
grid[0].append(1)
print(grid) # [[1], [1], [1]] 三行都变了

# 正确写法
grid = [[] for _ in range(3)]

三、索引与切片

列表支持索引(正/负)和切片:

1
2
3
4
5
6
7
8
9
a = [10, 20, 30, 40, 50]

print(a[0]) # 10
print(a[-1]) # 50
print(a[1:4]) # [20, 30, 40]
print(a[:3]) # [10, 20, 30]
print(a[2:]) # [30, 40, 50]
print(a[::-1]) # [50, 40, 30, 20, 10] 反转
print(a[::2]) # [10, 30, 50] 步长 2

索引越界抛 IndexErrora[10] 会崩,但 a[10:20] 只会返回空列表。切片比索引更”宽容”。

切片返回新列表——修改切片不影响原列表:

1
2
3
4
b = a[1:3]
b[0] = 999
print(a) # 原列表不变
print(b) # [999, 30]

想拷贝整个列表:b = a[:]b = list(a)b = a.copy()

四、修改列表

列表可变,可以直接改:

1
2
3
4
5
6
7
8
9
a = [1, 2, 3]
a[0] = 99 # 单个元素赋值
print(a) # [99, 2, 3]

a[1:] = [7, 8, 9] # 切片赋值,可以替换任意长度
print(a) # [99, 7, 8, 9]

a[1:3] = [] # 切片删除
print(a) # [99, 9]

切片赋值非常强大,可以在中间插入元素:

1
2
3
a = [1, 2, 3]
a[1:1] = [10, 20] # 在索引 1 处插入 10, 20
print(a) # [1, 10, 20, 2, 3]

五、增删改查方法

添加

1
2
3
4
5
6
7
8
a = [1, 2, 3]
a.append(4) # 尾部添加单个:[1, 2, 3, 4]
a.extend([5, 6]) # 尾部添加多个:[1, 2, 3, 4, 5, 6]
a.insert(0, 0) # 在索引 0 处插入:[0, 1, 2, 3, 4, 5, 6]

# extend 与 + 的区别
a + [7] # 返回新列表,a 不变
a += [7] # 就地修改,等价于 a.extend([7])

性能提示append 是 O(1),insert(0, x) 是 O(n)(要移动所有元素)。频繁头部插入用 collections.deque

删除

1
2
3
4
5
6
7
8
9
10
a = [10, 20, 30, 20, 40]

a.remove(20) # 按"值"删除第一个匹配:[10, 30, 20, 40]
popped = a.pop() # 弹出并返回最后一个:40,a = [10, 30, 20]
popped = a.pop(0) # 弹出索引 0:10,a = [30, 20]

del a[0] # 按索引删除:a = [20]
del a[:] # 清空

a.clear() # 清空

陷阱remove 只删第一个匹配的,全部删除要用循环或列表推导式。

查找

1
2
3
4
5
6
a = [10, 20, 30, 20, 40]

print(20 in a) # True
print(a.index(20)) # 1,第一次出现的位置
print(a.index(20, 2)) # 3,从索引 2 开始找
print(a.count(20)) # 2,出现次数

找不到会抛 ValueError,所以查找前先 in 检查,或者用 try:

1
2
3
4
try:
i = a.index(99)
except ValueError:
i = -1

修改

除了索引赋值,还有排序和反转:

1
2
3
4
5
6
7
8
9
a = [3, 1, 4, 1, 5, 9, 2, 6]

a.sort() # 就地升序:[1, 1, 2, 3, 4, 5, 6, 9]
a.sort(reverse=True) # 降序
a.reverse() # 就地反转

# 不修改原列表,返回新列表
b = sorted(a)
c = list(reversed(a))

复杂排序

sortsorted 都支持 key 参数:

1
2
3
4
5
words = ["Banana", "apple", "cherry"]
words.sort(key=str.lower) # 忽略大小写

people = [("张三", 25), ("李四", 20), ("王五", 30)]
people.sort(key=lambda p: p[1]) # 按年龄

多级排序:

1
records.sort(key=lambda r: (r.grade, -r.score))   # 先按 grade,再按 score 降序

六、遍历

前面 循环语句 for 与 while 已讲过:

1
2
3
4
5
6
7
8
for x in a:
print(x)

for i, x in enumerate(a):
print(i, x)

for x, y in zip(a, b):
print(x, y)

七、常用统计与聚合

1
2
3
4
5
6
7
a = [3, 1, 4, 1, 5, 9, 2, 6]

print(len(a)) # 8
print(min(a)) # 1
print(max(a)) # 9
print(sum(a)) # 31
print(sum(a) / len(a)) # 平均

想要中位数、方差、标准差:statistics 模块。

1
2
3
4
import statistics
statistics.median(a) # 3.5
statistics.mean(a) # 3.875
statistics.stdev(a) # 标准差

八、拷贝:浅拷贝 vs 深拷贝

前面提到 a[:] 拷贝,这是浅拷贝——只复制最外层,内层对象仍是共享的:

1
2
3
4
5
6
7
8
9
10
11
import copy

original = [[1, 2], [3, 4]]

shallow = original[:] # 或者 original.copy()
shallow[0][0] = 999
print(original) # [[999, 2], [3, 4]] 内层被改了!

deep = copy.deepcopy(original)
deep[0][0] = 111
print(original) # 不受影响

规则:

  • 元素都是不可变(int/str/tuple)时,浅拷贝够用;
  • 元素是可变对象(list/dict/自定义类)且需要独立修改,用 copy.deepcopy

九、列表作为栈和队列

(LIFO)用 list 完美:

1
2
3
4
stack = []
stack.append(1) # push
stack.append(2)
stack.pop() # pop → 2

队列(FIFO)用 list 也行,但 pop(0) 是 O(n)。频繁头部弹出请用 collections.deque

1
2
3
4
5
6
from collections import deque
q = deque([1, 2, 3])
q.append(4) # 右侧添加
q.appendleft(0) # 左侧添加
q.popleft() # 左侧弹出 → 0,O(1)
q.pop() # 右侧弹出 → 4

十、二维列表(矩阵)

1
2
3
4
5
6
7
8
9
10
11
# 3x4 全 0 矩阵
matrix = [[0] * 4 for _ in range(3)]

# 访问
matrix[1][2] = 9

# 遍历
for row in matrix:
for cell in row:
print(cell, end=" ")
print()

别用 [[0]*4] * 3——那是三个指向同一行的引用!这是新手最常翻车的地方之一。

十一、性能特性

  • 索引访问:O(1)
  • append / pop(尾部):均摊 O(1)
  • insert(0, x) / pop(0):O(n)
  • x in list:O(n)(用 set 更快,见 集合 Set
  • list.remove(x):O(n)
  • 排序:O(n log n),用的是 Timsort,稳定排序
  • 内存:一个元素约 8 字节指针 + 对象自身

十二、一个综合例子:图书管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
# books.py
"""极简图书管理,演示列表增删改查、排序、查找。"""

books = []

def add(title: str, author: str, year: int) -> None:
books.append({"title": title, "author": author, "year": year})

def remove(title: str) -> bool:
for b in books:
if b["title"] == title:
books.remove(b)
return True
return False

def find_by_author(author: str) -> list[dict]:
return [b for b in books if b["author"] == author]

def sort_by_year(desc: bool = False) -> list[dict]:
return sorted(books, key=lambda b: b["year"], reverse=desc)

def top_n_recent(n: int) -> list[dict]:
return sort_by_year(desc=True)[:n]


if __name__ == "__main__":
add("Python 编程", "Guido", 2020)
add("Fluent Python", "Ramalho", 2022)
add("Effective Python", "Slatkin", 2019)
add("Python Tricks", "Bader", 2018)

print("=== 全部(按年份升序)===")
for b in sort_by_year():
print(b)

print("=== 最新两本 ===")
for b in top_n_recent(2):
print(b)

print("=== Guido 的书 ===")
for b in find_by_author("Guido"):
print(b)

remove("Python Tricks")
print(f"删除后剩 {len(books)} 本")

十三、常见陷阱

陷阱 1:[[0]*n] * m 二维列表

1
2
3
4
grid = [[0] * 3] * 2
grid[0][0] = 1
print(grid) # [[1, 0, 0], [1, 0, 0]]
# 正确:grid = [[0] * 3 for _ in range(2)]

陷阱 2:循环中修改列表

1
2
3
for x in items:
if not valid(x):
items.remove(x) # 索引错乱,跳漏元素

用推导式重建:items = [x for x in items if valid(x)]

陷阱 3:默认参数是列表

1
2
3
def add(item, lst=[]):     # ❌
lst.append(item)
return lst

多次调用会共享同一个列表。改成:

1
2
3
4
5
def add(item, lst=None):
if lst is None:
lst = []
lst.append(item)
return lst

陷阱 4:== 与 is

1
2
[1, 2] == [1, 2]    # True
[1, 2] is [1, 2] # False

判断值相等永远用 ==

陷阱 5:+=+ 的区别

1
2
3
4
5
6
7
8
9
a = [1, 2]
b = a
a += [3] # 就地修改,b 也变
print(b) # [1, 2, 3]

a = [1, 2]
b = a
a = a + [3] # 创建新列表,a 指过去,b 不变
print(b) # [1, 2]

十四、小结与延伸阅读

  • 列表是可变、有序、任意类型的容器;
  • 增:append/extend/insert;删:remove/pop/del;改:索引/切片赋值;
  • 排序有 list.sort(就地)和 sorted(返回新),都支持 key
  • 拷贝分浅/深,可变元素警惕引用共享;
  • 头部操作慢,用 deque;
  • 二维列表用 [[0]*n for _ in range(m)]
  • 遍历时不要改容器。

延伸阅读:

下一篇 元组 Tuple 与不可变序列 我们讲列表的孪生兄弟——元组。