ValueError: max() arg is an empty sequence
这种报错我见过不止一次,基本都出现在统计脚本里。比如凌晨跑一批订单,想取最大的支付金额,平时数据都有,一到某个渠道当天没单,脚本直接挂。
很多人第一反应是加个 if len(data) > 0。
能跑,但我不太喜欢。因为你只是把坑挪到调用方了,max 自己的规则反而没搞明白。
Python 的 max 看着简单,其实有几个细节:
max([3, 8, 2])
max(3, 8, 2)
max([], default=0)
max(users, key=lambda x: x["age"])
这几个写法都得支持。
我自己实现一个版本,不追求把 CPython 的异常文案一字不差抄出来,但行为要基本对齐。
_missing = object()
defmy_max(*values, key=None, default=_missing):
ifnot values:
raise TypeError("my_max expected at least one argument")
# max(1, 2, 3) 这种写法
if len(values) > 1:
if default isnot _missing:
raise TypeError("default can only be used with iterable")
iterator = iter(values)
# max([1, 2, 3]) 这种写法
else:
iterator = iter(values[0])
try:
best = next(iterator)
except StopIteration:
if default isnot _missing:
return default
raise ValueError("my_max() arg is an empty sequence")
if key isNone:
best_score = best
for item in iterator:
if item > best_score:
best = item
best_score = item
else:
best_score = key(best)
for item in iterator:
score = key(item)
if score > best_score:
best = item
best_score = score
return best
这段代码里最关键的地方不是比较大小,而是第一行元素怎么拿。
best = next(iterator)
我以前见过有人这么写:
best = None
for item in data:
if best isNoneor item > best:
best = item
这代码看着没问题,实际很容易埋雷。
如果列表里本来就有 None 呢?如果元素不允许和 None 比较呢?如果业务数据是负数呢?这些分支都开始变得不干净。
所以我一般不拿 None 当初始最大值。直接从迭代器里取第一个元素,取不到就说明是空的,该抛错抛错,该走 default 走 default。
试一下普通列表:
scores = [71, 88, 64, 93, 93, 52]
print(my_max(scores))
print(my_max(71, 88, 64, 93))
输出:
93
93
这里还有个小细节。
如果最大值有多个,max 返回的是第一个最大值,不是最后一个。所以比较的时候要写:
if score > best_score:
不要手欠写成:
if score >= best_score:
这个差别在普通数字里不明显,到了对象列表里就能看出来。
比如取年龄最大的用户:
users = [
{"name": "老周", "age": 31},
{"name": "小李", "age": 28},
{"name": "阿凯", "age": 31},
]
print(my_max(users, key=lambda u: u["age"]))
结果应该是:
{'name': '老周', 'age': 31}
不是阿凯。
因为老周先出现,而且两个人的 key 值一样。
空列表也要处理,不然统计脚本迟早炸一次:
today_orders = []
print(my_max(today_orders, default=0))
输出:
0
但下面这种我会直接让它报错:
print(my_max(3, 5, default=0))
因为 default 只给“一个可迭代对象为空”的场景用。多个参数调用时,不存在“空不空”的问题,你都已经传了两个值了。
还有一个容易被忽略的点,max 不要求参数一定是列表。
defread_amounts():
yield18
yield42
yield7
print(my_max(read_amounts()))
这也是为什么代码里一直用 iter() 和 next(),而不是 len()、下标或者切片。
写工具函数我比较忌讳一上来就把数据转成列表:
items = list(values[0])
小数据无所谓,大一点就难看了。日志文件、分页接口、生成器,本来可以一条条扫,你非要全塞内存里,后面查内存飙高还不好定位。
最后补一段简单校验:
cases = [
([3, 1, 9], None),
([-5, -2, -9], None),
([], 100),
]
for arr, fallback in cases:
if fallback isNone:
print(my_max(arr))
else:
print(my_max(arr, default=fallback))
输出:
9
-2
100
自己实现一遍 max,其实不是为了替代内置函数。生产代码里该用内置还是用内置,没人会因为你手写一个 max 就觉得你牛。
它真正有价值的地方,是把几个边界习惯练出来:
空数据怎么处理;
初始值别乱塞;
比较的是原对象还是 key 之后的值;
相等时要不要替换;
能不能支持迭代器。
这些地方想清楚,很多业务代码会少一堆奇怪的 if else。