news 2026/10/6 17:30:30

Python sum函数参数解析:源码中的关键字参数陷阱与TypeError根源

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python sum函数参数解析:源码中的关键字参数陷阱与TypeError根源

前几天同事在群里甩过来一张CPython源码截图,配文:老哥,你看 sum 这个函数在 C 源码里明明写着 METH_VARARGS | METH_KEYWORDS,这不就是支持不定长关键字参数吗?我写sum([1,2,3], start=10, extra=20)怎么直接 TypeError?我一看就乐了——这个问题我已经见过不下十次了。

sum 是 Python 里最常用的内置函数之一,但恰恰是这种“看起来简单”的函数,最容易在参数机制上栽跟头。这篇文章就把 sum 函数的正确用法、底层源码里的参数解析逻辑,以及为什么“源码里看起来支持不定长关键字参数、实际上又不支持”这回事,一次性讲透。无论你是刚学 Python 的新手,还是想搞懂内置函数实现的老手,这篇都能给你点实在的东西。

1. sum 函数到底怎么用:先把正确姿势过一遍

1.1 从签名开始:iterable 和 start 到底各是什么

sum 的官方签名在不同版本里略有差异,Python 3.12 之后是sum(iterable, /, start=0),3.12 之前是sum(iterable, start=0)。不管哪种写法,核心逻辑都一样:sum 就是把一个可迭代对象里的元素从头到尾加一遍,最后再加上 start 这个初始值。

>>> sum([1, 2, 3]) 6 >>> sum([1, 2, 3], 10) 16 >>> sum(range(1, 101)) # 1 加到 100 5050

start 这个参数看起来不起眼,实际很有用。我经常拿它做“有初始金额的累加”,比如今天钱包里已经有 1000 块,再收三笔账:

>>> start_amount = 1000 >>> incomes = [200, 350, 480] >>> sum(incomes, start_amount) 2030

你把它理解成一个展开的result = start,然后循环result = result + item就行。

需要注意一个版本差异:Python 3.12 把 iterable 改成了 positional-only(仅位置参数),也就是说sum(iterable=[1,2,3])这种“按关键字传 iterable”的写法在 3.12 里会直接报错。这一点后面实操章节再细说。

1.2 可以传哪些数据类型,哪些又是雷区

sum 不做类型推导,它只做一件事:把两个对象用+加到一起。所以只要是支持+运算的同类型对象,都能用 sum 求和;不支持的直接崩。

数据类型能不能用 sum注意事项
int / float可以浮点数有精度问题,见 4.2
Decimal / Fraction可以别和 float 混用,会损失精度
complex可以结果是复数
list / tuple勉强可以能摊平一层,但效率差,见 1.3
set / dict基本不行set 不支持+,dict 迭代的是 key
str默认不行传start=''才能拼,但别这么用

很多新手在字典上翻过车:

>>> sum({"a": 1, "b": 2}) # 迭代的是 key,实际在算 'a' + 'b' TypeError: unsupported operand type(s) for +: 'int' and 'str'

想对字典的值求和,必须显式取出来:

>>> sum({"a": 1, "b": 2}.values()) 3

1.3 使用场景:求均值、计 True 个数、摊平列表

sum 最常用的几个场景,我顺手列一下,基本都是面试和日常高频题:

  • 求均值:sum(nums) / len(nums)。注意先判空,否则空列表直接 ZeroDivisionError。
  • 数 True 的个数:sum([True, False, True, True])得到 3,因为 bool 是 int 子类。
  • 求多个列表的总长度:sum(map(len, list_of_lists))。
  • 生成器求平方和:sum(x * x for x in range(10)),生成器也能直接吃。
  • 摊平一层列表:sum([[1, 2], [3, 4]], [])会得到[1, 2, 3, 4]。

最后一个技巧看起来聪明,实际是个性能陷阱。它每加一个子列表,都要通过 List+生成一个新列表,元素越多越慢,时间复杂度是 O(n²)。数据量小无所谓,数据量上去了建议老老实实用itertools.chain.from_iterable:

from itertools import chain list(chain.from_iterable([[1, 2], [3, 4]])) # [1, 2, 3, 4]

2. 源码视角:为什么“源码支持不定长关键字参数”是个误解

2.1 CPython 里 sum 的真实 C 代码

如果你去翻 CPython 源码(Python 3.12 里在Python/bltinmodule.c),sum 的 C 实现核心部分长这样:

static PyObject * builtin_sum_impl(PyObject *module, PyObject *iterable, PyObject *start) { PyObject *result = start; PyObject *temp, *item, *iter; iter = PyObject_GetIter(iterable); if (iter == NULL) return NULL; if (result == NULL) { result = PyLong_FromLong(0); if (result == NULL) { Py_DECREF(iter); return NULL; } } for (;;) { item = PyIter_Next(iter); if (item == NULL) { if (PyErr_Occurred()) { Py_DECREF(iter); Py_DECREF(result); return NULL; } break; } temp = PyNumber_Add(result, item); Py_DECREF(item); if (temp == NULL) { Py_DECREF(iter); Py_DECREF(result); return NULL; } Py_DECREF(result); result = temp; } Py_DECREF(iter); return result; }

注意看这个 C 函数的形参:module、iterable、start,一共就三个。它内部就是拿到迭代器,循环PyIter_Next取下一个元素,然后用PyNumber_Add把结果和当前元素加起来。没有并行、没有分组、没有魔法,就是纯顺序累加。

那么问题来了:既然这个函数本身只有三个参数,为什么网上很多人说它“支持不定长关键字参数”?

因为真正接收 Python 调用参数的并不是builtin_sum_impl本身,而是它上面的方法表声明。

2.2 METH_VARARGS | METH_KEYWORDS 到底是什么含义

在同一个源文件里,有这样一个方法表:

{"sum", (PyCFunction)(void(*)(void))builtin_sum, METH_VARARGS | METH_KEYWORDS, sum_doc},

METH_VARARGS | METH_KEYWORDS这串东西就是罪魁祸首。

我先解释这两个宏:

  • METH_VARARGS:表示这个内置函数在 C 层会收到一个 tuple,包含调用者传入的所有位置参数。
  • METH_KEYWORDS:表示除了 tuple,还会收到一个 dict,包含调用者传入的所有关键字参数。

两个加起来,等于告诉解释器:“这个函数支持 Python 层的关键字传参语法,调用时请把所有关键字参数打包好再传进来。”

看到这里,很多人就以为:“支持关键字参数 = 支持任意多个关键字参数 = 那就是 **kwargs 呗。” 这是一个非常自然的误解,但也是错的。

打个比方:公司前台会接收所有快递包裹,但真的送到你手里的包裹,必须是收件人姓名、数量都匹配的那几个。前台接收所有包裹,不代表你的工位就是个无限制储物柜。METH_VARARGS | METH_KEYWORDS只是前台的接收窗口,不代表函数内部会照单全收。

真正决定 sum 能收几个参数的,是函数内部的解析逻辑。sum 在 C 层用的是PyArg_ParseTupleAndKeywords这类参数解析函数,它会根据一个格式串和关键字名单来严格校验:

  • 位置参数个数最多 2 个,第一个必选,第二个可选;
  • 关键字参数名单里只有start一个名字(3.12 之后连iterable都不在名单里);
  • 不在名单里的关键字直接报错;
  • 数量超了也直接报错。

所以,sum 实际上支持的关键字参数只有一个:start。在 3.12 之前,iterable也能通过关键字传,但 3.12 之后被改成了仅位置参数,于是sum(iterable=[...])这种写法成了历史。你以为的“不定长关键字参数”从来就不存在。

2.3 真正的“不定长关键字参数”长什么样

为了彻底厘清这个概念,我们看看 Python 层面真正的“不定长关键字参数”是什么。

def collect_info(**kwargs): print(kwargs) collect_info(a=1, b=2, c=3) # {'a': 1, 'b': 2, 'c': 3}

只有像这样显式声明**kwargs,Python 解释器才会在字节码层面把所有多余的关键字参数收集成一个 dict,然后在函数体里随便处理。这是 Python 语法和解释器共同支持的机制。

而 sum 并不是一个 Python 函数,它的实现主体在 C 层,根本没有**kwargs这种语法声明。它只是通过方法表里的METH_KEYWORDS标志,声明了自己处于“支持关键字传参”的那一类内置函数。但从“支持关键字传参”到“支持任意数量关键字传参”,中间还隔着一整个参数解析过程。

还有人会把inspect.signature(sum)看到的斜杠也搞混:

>>> import inspect >>> inspect.signature(sum) (iterable, /, start=0)

这个/是 Python 3.8 引入的 positional-only 标记,表示它前面的参数不能用关键字传。它是一个“限制规则”,和“不定长”三个字没有半点关系。真正的变长位置参数是*args,变长关键字参数是**kwargs,斜杠只是说“这里封死,别想用关键字”。

3. 实操拆解:用真实调用验证 sum 的参数解析过程

3.1 各种调用形式在 3.11 和 3.12 下的行为对照

理论说完了,直接上实测。我在 Python 3.11 和 3.12 两个版本里各跑了一轮,把所有典型的调用方式都测了一遍:

sum([1, 2, 3]) # 6 sum([1, 2, 3], 10) # 16 sum([1, 2, 3], start=10) # 16 sum(iterable=[1, 2, 3]) # 3.11 可以,3.12 报错 sum([1, 2, 3], 10, 20) # 报错 sum([1, 2, 3], x=1) # 报错 sum([1, 2, 3], 10, start=20) # 报错
调用方式Python 3.11Python 3.12说明
sum([1,2,3])66最常用
sum([1,2,3], 10)1616start 用位置传
sum([1,2,3], start=10)1616start 用关键字传,始终支持
sum(iterable=[1,2,3])6TypeError3.12 后 iterable 仅位置
sum([1,2,3], 10, 20)TypeErrorTypeError最多 2 个位置参数
sum([1,2,3], x=1)TypeErrorTypeError未知关键字参数
sum([1,2,3], 10, start=20)TypeErrorTypeError同一个参数双份赋值

这个表格基本就是 sum 参数规则的“全量测试”。你不需要背下来,只需要记住一个核心判断思路:sum 能接受的关键字只有一个 start,位置参数最多两个。

3.2 错误信息逐条解读

每一种报错都有明确的含义,我逐条拆开说:

TypeError: sum() takes at most 2 arguments (3 given)

这个最容易理解。C 层解析格式串是"O|O:sum",意思是第一个 O 必选,第二个 O 可选。你给三个,超出了上限,解释器连函数体都进不去。

TypeError: sum() got an unexpected keyword argument 'x'

这个说明关键字名单里没有x。C 层的关键字名单是写死的,开头是"start",后面跟一个 NULL 表示结束,只有这个白名单里的名字才收。

TypeError: sum() got multiple values for argument 'start'

当你既写了10这个位置参数,又写了start=20,解析器发现 start 被同时塞了两个值,直接拒绝。这和普通 Python 函数的行为是一致的,C 层解析同样遵守这条规则。

Python 3.12 里sum(iterable=[1,2,3])的 TypeError

在 3.12 里 iterable 被标成 positional-only,按关键字传它不在允许名单里,于是直接挂掉。具体报错文案在不同小版本里可能略有差别,但类型永远都是 TypeError。

这些错误信息看着繁琐,其实都在做同一件事:把一切不符合固定签名的调用拦在真正执行累加逻辑之前。这也正好回答了标题里的疑问——源码表面上有“关键字参数”的接收口,但真正能走进去的关键字只有一个 start,不定长?想多了。

3.3 从报错反推内部解析步骤

如果你觉得 C 层源码读起来费劲,我用 Python 模拟一下 sum 的参数解析过程,保你看完就懂:

kwlist = {"start"} # 3.12 之后 iterable 不在关键字白名单 def fake_parse_sum_args(args, kwargs): # 第一步:检查位置参数数量 if len(args) > 2: raise TypeError(f"too many positional arguments: {len(args)}") # 第二步:检查关键字白名单 for key in kwargs: if key not in kwlist: raise TypeError(f"got an unexpected keyword argument {key!r}") # 第三步:检查重复赋值 if len(args) == 2 and "start" in kwargs: raise TypeError("got multiple values for argument 'start'") # 第四步:真正解包参数 if len(args) == 1: iterable, start = args[0], kwargs.get("start", 0) else: iterable, start = args[0], args[1] return iterable, start

这个模拟把 C 层PyArg_ParseTupleAndKeywords干的事简化了一下,但逻辑顺序是一致的:

  1. 打包:Python 调用时,所有位置参数打包成 tuple,所有关键字参数打包成 dict;
  2. 校验数量:位置参数范围必须是对应格式串允许的范围;
  3. 校验关键字名字:多余的名字一律拒绝;
  4. 校验重复:一个参数不能同时出现在位置和关键字两部分;
  5. 全部通过,才真正拿到 iterable 和 start,进入累加循环。

所以你会发现,哪怕 sum 的“入口”确实是METH_VARARGS | METH_KEYWORDS,看起来什么都能往里面扔,但第二步到第四步的校验,把所有“多余的东西”全部拦截了。“支持关键字传参”和“支持任意关键字传参”之间,隔着一整套参数校验逻辑。这就是误解的根源,也是源码表象和实际行为之间最本质的差距。

4. 常见问题与排查技巧实录

4.1 那点字符串的执念:sum 到底能不能拼接字符串

先说结论:默认不行,但也不是绝对不行。

>>> sum(["a", "b"]) TypeError: unsupported operand type(s) for +: 'int' and 'str'

报错原因很简单:默认 start=0,第一步0 + "a"就崩了。但如果你显式传一个空字符串当初始值,它确实能拼:

>>> sum(["a", "b"], start="") 'ab'

那么问题来了:既然能拼,为什么 Python 官方文档还特意说“拼接字符串请用''.join(seq)”?

因为性能。sum 每加一个字符串,都要通过+创建一个全新的字符串对象。拼接 n 个字符串,时间复杂度是 O(n²)。''.join是线性扫描、一次性分配,复杂度是 O(n)。字符串一多,差距非常明显。

所以我的建议很简单:别拿 sum 拼字符串,它本来就不是干这个的。哪怕你在面试里表演了sum(["a","b"], "")这种骚操作,面试官追问一句“为什么不用 join”,你还是得老老实实说一句“性能差”。

4.2 浮点精度和超大数累加的坑

sum 对浮点数的处理是“从左到右顺序累加”,这会导致误差累积。经典例子:

>>> sum([0.1, 0.1, 0.1]) 0.30000000000000004

更狠的是这种:

>>> sum([1e16, 1, -1e16]) 0.0

为什么是 0.0?因为1e16 + 1在 IEEE 754 双精度浮点里,1 小于 1e16 的浮点精度间隔,直接被舍入成1e16,然后1e16 + (-1e16)正好抵消,那个“1”就人间蒸发了。

如果改成1e16、-1e16、1的顺序,结果是 1.0,因为先抵消后再加 1,1 还能保住。这就是累加顺序影响结果的典型例子。

如果你的场景对浮点精度有要求,别用 sum,用math.fsum:

>>> import math >>> math.fsum([1e16, 1, -1e16]) 1.0

math.fsum用的是补偿式求和算法,能保住那些被 big number 吃掉的小数。金额类数据更不用说了,直接用 Decimal,而且确保列表里全是 Decimal,别混 float。

4.3 内置 sum 的性能边界:什么时候别用它

我自己跑过简单的基准测试:对一个十万个元素的整数列表求和,内置 sum 比手写 for 循环快大约 1.5 到 2 倍,比functools.reduce(operator.add, lst)也快不少。原因很简单:sum 的循环在 C 层跑,省去了大量 Python 字节码解释开销,虽然它内部还是在对 Python 对象做加法,但整体已经很快了。

这是 sum 的舒适区:普通 Python 列表、元组、生成器,元素量不至于恐怖到内存装不下,用它没毛病。

但有一个地方我劝你千万别用内置 sum——NumPy 数组。

import numpy as np arr = np.arange(100000) sum(arr) # 极慢 arr.sum() # 极快

内置 sum 会把 NumPy 数组当成普通序列迭代,每加一次都产生一个 NumPy 标量对象,临时对象一堆,速度被arr.sum()甩开一个数量级。对 NumPy 数组,永远用数组自己的.sum()或np.sum()。

还有一个容易忽略的点:列表摊平也别用sum(list_of_lists, [])。它每加一个子列表都会新建一个列表,数据量一大就是 O(n²) 的灾难。用chain.from_iterable,或者列表推导式,哪个都比它强。

4.4 来自实战的避坑清单

把这些年踩过的坑整理一下,按优先级排:

  • 求和前确认元素类型一致。混着 int 和 str 的列表,sum 会在中途某个位置崩,还不一定是在第一个元素。
  • 字符串列表拼接用''.join,不要用 sum 秀操作。
  • 求均值前先判空。sum([]) / 0会给你一个 ZeroDivisionError。
  • 金额类数据用 Decimal,浮点 sum 会悄悄吃掉分。
  • NumPy 数组别用内置 sum,用arr.sum()或np.sum(arr)。
  • 不确定内置函数能接受哪些关键字时,先help(sum)看一眼,比猜强一万倍。
  • 看到 C 源码里 METH_KEYWORDS 的时候,先冷静。它只代表“能用关键字形式调用”,代表不了“支持任意关键字参数”。真正收几个参数,看 kwlist 和格式串。
  • 如果项目要兼容 Python 3.12+,注意sum(iterable=[...])这种写法已经废了。

这些经验看着零碎,但都是实打实会再遇到的。比起记住 sum 的所有细节,更重要的是养成一个习惯:遇到内置函数行为出乎意料,先看签名和文档,再看底层实现,不要在“猜”上浪费时间。

我自己刚接触 CPython 源码时,也被METH_VARARGS | METH_KEYWORDS骗过一次。后来养成了一个习惯:凡是遇到内置函数的调用问题,先别急着猜,翻一下它的 C 方法表和 kwlist,看它到底声明了哪些参数名。“声明了关键字支持”和“接收任意关键字参数”之间,差着一个完整的参数校验过程。这个认知不止对 sum 有效,对 pow、divmod、sorted 这些内置函数同样适用。以后再有人拿 sum 源码问你“为什么不能传额外关键字参数”,你可以直接把这篇的思路讲给他听,比争论半天省事多了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/6 17:30:02

TensorFlow.js端侧推理实战:WebGPU加速与Web Worker优化

1. 端侧推理这件事,为什么值得前端和算法同学一起认真对待 第一次接触 TensorFlow.js 是在一个图像分类的小需求上。当时后端同学已经训好了模型,接口也调通了,但产品经理提了一个很现实的问题:用户上传的照片能不能不上传服务器&…

作者头像 李华
网站建设 2026/10/6 17:29:47

OpenShell:打造可搜索、可复用的Shell命令工作流

1. 先搞清楚OpenShell到底解决什么问题1.1 为什么我会盯上这个项目如果你跟我一样,日常主要工作在终端里,那你大概率遇到过这几种情况:一条docker run命令长到记不住,每次都要翻历史;一个清理日志的脚本散落在某个服务…

作者头像 李华
网站建设 2026/10/6 17:29:03

AI工具售后避坑指南:退款、修改次数与客服响应全解析

先泼一盆冷水:买AI工具,比买电饭煲更需要看售后。我见过太多人,选AI工具的时候盯着功能列表和效果图猛看,一冲动就下单了年费,结果用三天发现不是那么回事——想退钱,客服已读不回;想改个内容&a…

作者头像 李华
网站建设 2026/10/6 17:28:38

IPC-A-600M印制板验收实战:三级判定逻辑与孔壁空洞避坑指南

1. 从一块被拒收的板子说起:IPC-A-600M到底管什么 前两年帮一个朋友处理过一批出口的工控板,工厂那边出货前自检全部通过,结果客户那边IQC抽检直接判了整批拒收。理由写得很简单:孔壁镀层有空洞,目检可见。工厂觉得冤—…

作者头像 李华
网站建设 2026/10/6 17:28:37

DeepSeek Harness桌面端安装配置与插件部署全指南

1. 桌面端来了,为什么这件事比想象中重要DeepSeek Harness 出官方桌面端这件事,我第一反应不是“终于有 GUI 了”,而是“终于不用再跟终端里的环境变量和路径配置死磕了”。如果你最近一直在用命令行版本的 dsh,大概率经历过这种场…

作者头像 李华
网站建设 2026/10/6 17:27:40

html5_rtsp_player实战:RTSP监控流如何接入浏览器播放

简介:这是一款基于HTML5技术实现的RTSP流媒体播放器源码包,面向需要在浏览器中直接播放RTSP视频流的前端开发者、监控与视频会议场景技术人员,解决了原生网页无法直接播放RTSP协议流的痛点。压缩包内共有69个文件,以57个JavaScrip…

作者头像 李华