Chapter 16: Built-in Functions
内置函数是 Python 代码最常接触的 runtime 入口。len(items)、iter(items)、getattr(obj, name)、repr(obj)、open(path)、eval(expr) 看起来像普通函数调用,实际承担的任务是把用户代码带到对象协议、类型关系、属性查找、I/O 层和动态编译执行层。读完本章,读者应能定位一个 built-in 调用背后进入了哪个对象协议,判断失败来自对象缺少能力、类型关系错误、属性查找异常、表示方法返回值错误、资源边界错误,还是动态执行 namespace 设置错误。
本章的贯穿材料是一个小型对象 Shelf。它同时暴露容器长度、迭代、属性兜底、可调用对象、字符串表示、格式化和动态求值入口。这个对象覆盖本章所需的几个 protocol surface:容器协议、类型检查、attribute protocol、call protocol、representation protocol、I/O 和 dynamic execution boundary。
class Shelf:
def __init__(self, name, items):
self.name = name
self._items = list(items)
def __len__(self):
return len(self._items)
def __iter__(self):
return iter(self._items)
def __getattr__(self, attr):
if attr == "empty":
return len(self) == 0
raise AttributeError(attr)
def __call__(self, index):
return self._items[index]
def __repr__(self):
return f"Shelf({self.name!r}, {self._items!r})"
def __str__(self):
return f"{self.name}: {len(self)} items"
def __format__(self, spec):
if spec == "count":
return str(len(self))
return format(str(self), spec)
shelf = Shelf("tools", ["hammer", "wrench"])
这个例子里的 builtins 可以按一条稳定路径阅读:先确认调用的是普通名字 len、iter 还是 getattr;再确认这个函数在 Python 语义上要求对象提供什么协议;最后确认 CPython 这类实现如何把请求分派到类型对象的 slot 或普通属性访问函数。Python 官方文档把内置函数列为始终可用的函数和类型,语义边界可回溯到 Python 3 built-in functions 文档;协议与 special method lookup 的细节可回溯到 Python data model。CPython 读源码时,许多 builtins 的外层入口集中在 Python/bltinmodule.c,但真正的对象能力通常继续分派到 Objects/、Python/ 或 _io 相关实现。
16.1 Iteration and size builtins
len、iter、next 共同回答一个问题:对象能否被当作有规模、可遍历、可逐步推进的对象处理。它们把普通函数调用转换成容器协议和 iterator protocol。对 Shelf 来说,len(shelf) 读取 _items 的长度,iter(shelf) 返回 _items 的 iterator,next(iterator) 推进这个 iterator 并在耗尽时触发 StopIteration。
print(len(shelf))
iterator = iter(shelf)
print(next(iterator))
print(next(iterator))
这段代码的 runtime 路径可以拆成三层。第一层是名字查找:当前 frame 找到 builtins namespace 中的 len、iter、next。第二层是 built-in 函数检查参数类型和协议入口。第三层是对象能力分发:len(shelf) 需要 Shelf 的类型提供长度能力,iter(shelf) 需要 Shelf 的类型提供迭代能力,next(iterator) 需要 iterator 自身提供推进能力。
len(obj) 的语义结果是一个非负整数,代表对象长度。对自定义对象,核心入口是 __len__。__len__ 的返回值必须是整数,并且语义上代表对象当前可观察的元素数量。CPython 对长度还有实现边界:长度需要落在 sys.maxsize 可表达范围内,超出时某些路径会抛出 OverflowError。这个边界说明 len 的结果既是 Python 层协议结果,也是 CPython C API 中 Py_ssize_t 等整数表示的约束结果。
iter(obj) 的语义结果是 iterator。优先路径是对象类型上的 __iter__ 返回 iterator。历史兼容路径还允许对象通过序列式 __getitem__ 从索引 0 开始提供元素;如果类型把 __iter__ 明确设为 None,调用 iter(obj) 会直接失败,并以 TypeError 表示对象没有迭代能力。这个细节用于判断一个对象为何无法进入 for 循环:先看类型是否声明了迭代能力,再看返回对象自身是否能被 next 推进。
next(iterator) 的输入已经是 iterator。它调用 iterator 的 __next__,返回一个元素;耗尽时抛出 StopIteration。next(iterator, default) 给出了耗尽时的替代返回值,因此把“正常耗尽”从异常路径转成普通返回路径。这个差异会影响循环外的手写推进逻辑:需要区分“数据耗尽”和“iterator 内部出错”。前者可以由 StopIteration 或 default 处理,后者会保留原异常。
iter(callable, sentinel) 是 iter 的第二种形式。它把一个零参数 callable 包装成 iterator,每次迭代调用 callable,返回值等于 sentinel 时结束。这个形式常用于块读取:I/O 函数负责产生块,sentinel 负责表达结束条件。它说明 builtins 也能把 call protocol 和 iterator protocol 组合起来。
from functools import partial
with open("records.bin", "rb") as file:
for block in iter(partial(file.read, 64), b""):
handle(block)
这段代码里,open 返回 file object,partial(file.read, 64) 是零参数 callable,iter(callable, b"") 生成一个 iterator。每一轮先调用 file.read(64),再比较结果与 b""。相等时,iterator 结束。这里的边界在 I/O 层:块大小、二进制模式、文件关闭时机都影响结果;iter 只负责把 callable 输出转成 iterator 推进规则。
把这三个 builtins 连起来,可以得到容器阅读顺序:先用 len 判断对象是否声明规模,再用 iter 判断对象是否声明遍历入口,最后用 next 判断 iterator 推进和耗尽路径。for 循环内部也是类似结构:获取 iterator,反复推进,遇到 StopIteration 收束循环。
16.2 Type and identity builtins
type、isinstance、issubclass、id 共同回答另一个问题:当前对象是谁,它属于哪个类型关系,它在生命周期内如何被唯一标识。它们查询对象头部、类型对象和继承关系,帮助读者区分 exact type、subclass relation、virtual subclass relation 和 object identity。
print(type(shelf))
print(isinstance(shelf, Shelf))
print(issubclass(Shelf, object))
print(id(shelf))
type(obj) 的单参数形式返回对象的直接类型。对 shelf 来说,结果是 Shelf。这个查询对应对象模型中的 ob_type 或等价类型指针概念:对象本身携带指向类型对象的关系,解释器通过这个关系找到 slot、方法表和类型元数据。type 的三参数形式会创建新的 type object,相当于动态 class statement,本章只使用单参数形式讨论查询语义。
isinstance(obj, classinfo) 判断对象是否属于某个类、其子类,或者由抽象基类注册出的 virtual subclass。它适合表达“这个对象能否按某类语义处理”。type(obj) is SomeClass 表达 exact type 判断,粒度更窄。工程上判断分支时,isinstance 更常用于接收子类扩展;需要固定实现策略、绕开子类重写或做序列化标签时,exact type 才更合适。
issubclass(cls, classinfo) 把对象实例换成类型对象,判断类型之间的继承或虚拟子类关系。它适合在注册表、插件系统、协议适配层中检查“某个类声明或继承了何种能力”。调用前需要保证第一个参数是 class object;把普通实例传入会触发 TypeError。这类错误来自输入对象层级错误,并非来自实例能力缺失。
id(obj) 返回对象 identity 的整数表示。语义保证是:对象生命周期内,这个整数唯一且稳定;生命周期互相错开的对象可以复用同一个数值。CPython 的实现细节通常把 id 表示为对象内存地址,这个结论只适用于 CPython 语境。写跨实现判断时,应使用“生命周期内稳定唯一”这个语义,不把内存地址当作通用规则。
这组 builtins 的边界在于“类型关系”和“协议能力”属于两个层级。isinstance(shelf, Shelf) 成立,说明对象位于这个类型层级内;len(shelf) 成立,说明它的类型暴露长度协议。一个对象通过 isinstance 检查后,仍可能缺少某个具体方法;一个对象未继承某个类,也可能通过 duck typing 提供相同行为。读源码或设计 API 时,先确认代码到底需要类型身份、继承关系,还是协议能力。
16.3 Attribute and callable builtins
getattr、setattr、hasattr、callable 把对象带入 attribute protocol 和 call protocol。它们看起来是工具函数,实际能触发 __getattribute__、descriptor、__getattr__、__setattr__、slot 检查和 __call__ 相关分发。对 Shelf 来说,getattr(shelf, "empty") 会进入属性兜底逻辑,callable(shelf) 会检查类型是否让实例可调用。
print(getattr(shelf, "name"))
print(getattr(shelf, "empty"))
setattr(shelf, "owner", "ops")
print(hasattr(shelf, "owner"))
print(callable(shelf))
print(shelf(0))
getattr(obj, name) 等价于动态属性访问。name 必须是字符串,可以来自变量,因此它能表达点号语法难以表达的动态属性名。常规路径先走对象属性查找:类型的 __getattribute__ 接管读取,descriptor 可能参与绑定,实例字典和类字典可能参与查找;常规查找失败后,__getattr__ 才获得兜底机会。Shelf.__getattr__ 只为 empty 生成一个动态属性,因此 getattr(shelf, "empty") 返回布尔值。
setattr(obj, name, value) 等价于动态赋值。它把字符串属性名和值交给对象的设置路径。常规实例会把新属性写入实例字典;定义了 __slots__、只读 descriptor 或自定义 __setattr__ 的对象可能收紧写入范围。属性名可以是普通点号无法访问的字符串,后续仍可通过 getattr 读取。这说明 attribute protocol 的名字空间比源代码点号语法更宽。
hasattr(obj, name) 的语义可以理解为“尝试读取属性,并把 AttributeError 转成 False”。这给调试带来一个明确检查点:如果属性读取过程中某个 property、descriptor 或 __getattr__ 抛出了 AttributeError,hasattr 也会得到 False。于是 hasattr 适合检查简单属性存在性;对会执行复杂逻辑的属性,直接调用 getattr 并在窄范围内处理异常,能更清楚地区分“属性缺失”和“属性计算内部失败”。
callable(obj) 检查对象是否看起来可调用。类对象天然可调用,因为调用类会进入构造实例路径;实例是否可调用取决于它的类型是否提供 __call__。Shelf 定义了 __call__,所以 callable(shelf) 为真,并且 shelf(0) 会返回第一个元素。这里需要区分能力检查与调用结果:callable 只能说明解释器会尝试调用路径;参数数量错误、索引越界、函数体异常仍会在实际调用时发生。
attribute 和 call builtins 的共同点是:它们把“看起来像语法”的动作转成可传递、可组合的函数调用。点号语法需要属性名写在源码里,getattr 可以把属性名作为数据传递;调用语法需要直接写 obj(...),callable 可以在分派前进行能力查询。工程代码中,插件调度、配置驱动、反射式序列化都会使用这一层能力,但应把动态属性名和允许调用的对象范围收束在明确白名单中。
16.4 Representation and formatting builtins
repr、str、format 共同回答“对象如何变成用户可见文本”。它们进入 representation protocol,并分别服务调试、用户展示和格式化控制。对 Shelf 来说,repr(shelf) 应给出更适合复盘对象状态的字符串,str(shelf) 给出更适合展示的字符串,format(shelf, "count") 进入自定义格式规则。
print(repr(shelf))
print(str(shelf))
print(format(shelf, "count"))
print(f"{shelf:count}")
repr(obj) 调用 __repr__,目标是生成信息充分、便于调试的官方表示。许多内置类型的 repr 会尽量返回可被 eval 重建等价值的表达式;自定义对象在无法保证重建时,也应保留类型名和关键状态。Shelf.__repr__ 返回 Shelf('tools', ['hammer', 'wrench']) 这种形状,读者能直接看到类型、名称和内部元素。
str(obj) 调用 __str__,目标是生成适合人阅读的非正式表示。Shelf.__str__ 返回 tools: 2 items,表达对象摘要。str 的输出通常被 print、日志和错误信息使用。它的重点是可读性和场景表达,不要求满足可重建性。未定义 __str__ 时,默认实现会退到 __repr__,因此只写 __repr__ 也能得到一个最低限度的字符串。
format(obj, spec) 调用 __format__,并把格式说明字符串交给对象解释。f-string 中的格式部分也会使用同一协议,例如 f"{shelf:count}"。内置数字、字符串、日期时间对象都可以解释自己的格式说明。自定义对象应把格式说明的语义写清楚:本章例子中,count 只返回元素数量;其它格式说明交给 str(shelf) 再使用标准字符串格式处理。
表示协议的失败边界很具体。__repr__、__str__、__format__ 都必须返回字符串对象;返回其它类型会触发 TypeError。object.__format__ 对非空格式说明有严格边界,普通对象没有声明某种格式时,传入任意 spec 可能失败。这个规则让格式化调用具有可定位的错误来源:先看对象是否实现对应方法,再看方法是否接收该 spec,最后看返回值是否为字符串。
这组 builtins 也影响调试成本。一个对象的 repr 如果省略关键状态,断点、日志、交互式输出都会缺少复盘信息;一个对象的 str 如果过度详细,用户界面和错误信息会承载太多内部细节;一个对象的 format 如果把 spec 解释得过宽,调用方会难以判断输出契约。稳定做法是让 repr 面向开发者复盘,让 str 面向默认展示,让 format 面向明确格式契约。
16.5 Resource and evaluation builtins
open、eval、exec 把 builtins 从对象协议带到资源和动态执行边界。open 进入文件系统与 I/O 栈,返回 file object;eval 编译并求值表达式,返回表达式结果;exec 编译并执行语句 suite,返回 None 并可能写入 namespace。这三类入口的共同点是:调用方传入的数据会跨越普通对象方法边界,进入外部资源或新代码执行路径。
open(file, mode, encoding, errors, newline, ...) 返回对应的 file object。file 可以是 path-like object,也可以是整数文件描述符;mode 决定读写、追加、创建、文本或二进制;文本模式下编码会影响字节与字符串之间的转换。稳定工程写法是把 open 放进 context manager,让 with 接管关闭时机。
with open("shelf.txt", "w", encoding="utf-8") as file:
file.write(str(shelf))
with open("shelf.txt", "r", encoding="utf-8") as file:
line = file.read()
这段代码里,open 只负责打开资源并返回对象;with 负责在正常路径和异常路径中调用退出逻辑;str(shelf) 决定写入文本内容。分析 I/O 问题时,应分开看三件事:路径和权限是否允许打开,mode 与编码是否符合数据形态,file object 的生命周期是否被正确收束。open 抛出的常见错误属于 OSError 家族,原因可能来自路径、权限、文件描述符、编码或底层系统调用。
eval(source, globals=None, locals=None) 处理表达式。source 可以是字符串或 code object;字符串会先被解析为表达式,再在给定 globals 和 locals 中求值。它返回表达式结果。若 globals 字典缺少 __builtins__,解释器会插入 builtins 引用,使动态表达式能找到内置名字。这个行为说明 eval 不是简单字符串替换,它会创建一条新的编译与名字解析路径。
namespace = {"shelf": shelf, "len": len}
count = eval("len(shelf)", {"__builtins__": {}}, namespace)
print(count)
这段代码把 globals 中的 __builtins__ 设为空字典,并把需要的名字显式放入 locals。表达式 len(shelf) 的名字解析会在这些映射中查找 len 和 shelf。这里可以得到两个判断:动态求值的可见名字由 globals 和 locals 决定;把 __builtins__ 调整为空只能收缩普通名字查找入口,不能把任意非可信代码变成安全沙箱。非可信输入应使用更窄的解析器、白名单表达式或 ast.literal_eval 这类只处理字面量的工具。
exec(source, globals=None, locals=None, *, closure=None) 处理语句 suite。它可以执行赋值、函数定义、类定义等语句,并把结果写入 namespace。只传 globals 时,globals 同时作为 locals 使用;同时传 globals 和 locals 时,执行语义接近 class body:顶层赋值落在 locals,内部函数对这些名字的可见性会体现 class body 风格边界。这个细节常解释“exec 里定义的函数为何读取不到同一段 exec 顶层赋值”的现象。
namespace = {"shelf": shelf}
exec("result = len(shelf)\nlabel = str(shelf)", {"__builtins__": {"len": len, "str": str}}, namespace)
print(namespace["result"])
print(namespace["label"])
这段代码中,exec 没有返回计算结果,结果通过 namespace 观察。result 和 label 是执行后写入 locals 的名字。分析 exec 相关问题时,先看 source 是表达式还是语句;表达式应使用 eval,语句 suite 应使用 exec。再看 globals、locals 和 __builtins__ 的映射关系。最后看执行内容是否触发编译错误、运行时错误或审计事件。
16.6 Builtins as protocol surface
builtins 的统一角色是 protocol surface:它们把语法、对象能力、类型关系、资源入口和动态执行收束成稳定的调用面。这个调用面位于用户代码和 runtime 之间。用户代码调用 len(shelf),看到的是一个普通函数;runtime 看到的是对 Shelf 类型长度 slot 或 __len__ 语义的请求。用户代码调用 format(shelf, "count"),看到的是字符串结果;runtime 看到的是 __format__ 对 spec 的解释。
下面的图把本章贯穿材料中的路径压缩到同一张 runtime 视图。图中的 builtins 负责接收用户代码请求,协议层负责把请求派发给对象类型或外部边界,结果层负责返回值或异常。
这张图的关键点在于:builtins 本身很薄,真正的行为通常由对象类型、协议方法、namespace 或外部系统决定。len 的结果来自对象长度协议,getattr 的结果来自属性查找路径,callable 的结果来自类型的可调用能力,open 的结果来自 I/O 层,eval 和 exec 的结果来自编译执行和名字映射。把 builtins 当作 protocol surface 后,调试顺序会更稳定。
可复用判断顺序可以固定为五步。第一步,确认 built-in 名字是否被当前 namespace 覆盖。第二步,确认输入对象处于哪个层级:实例、类型对象、callable、path-like object、字符串源码或 code object。第三步,确认 built-in 需要哪个协议入口:__len__、__iter__、__next__、__getattribute__、__getattr__、__setattr__、__call__、__repr__、__str__、__format__,或 I/O 和编译执行入口。第四步,检查返回值契约和异常契约,例如长度必须是整数,表示方法必须返回字符串,iterator 耗尽使用 StopIteration,open 失败使用 OSError 家族。第五步,标注 CPython 实现边界,例如 id 与内存地址、len 与 sys.maxsize、builtins 外层源码入口与类型 slot 分发表。
这个顺序能解释一个常见现象:给实例临时写入 __len__,len(obj) 仍可能失败。special method lookup 对内置函数和语法的隐式调用通常从类型对象查找特殊方法,并跳过实例字典中的同名属性。于是 obj.__len__ = lambda: 3 只能让显式属性读取看到这个函数;len(obj) 会继续检查 type(obj) 是否提供长度协议。这个规则把动态属性和协议能力分开,保证解释器能用类型级 slot 做正确而稳定的分发。
class Plain:
pass
plain = Plain()
plain.__len__ = lambda: 3
print(plain.__len__())
# len(plain) 会抛出 TypeError,因为长度协议需要类型级入口。
读 CPython 源码时,可以把 CPython Python/bltinmodule.c 当作 builtins 外层入口索引。看到 builtin_callable 一类函数后,应继续追到 PyCallable_Check 等对象 API;看到 len 相关入口后,应继续追到对象大小 API 和类型 slot;看到 eval、exec 相关入口后,应继续追到编译、frame、globals、locals、builtins 注入和 audit event。源码阅读的目标是确认“这个 built-in 把请求交给了哪个 runtime 层”,再把错误和成本定位到该层。
本章最终建立的理解是:builtins 不是散落的工具集合,而是 Python runtime 向用户代码暴露的协议入口层。掌握这一层后,读者可以从一个简单调用回到 object、type、namespace、protocol、resource 和 execution state,判断行为来自对象自身、类型对象、名字映射、外部资源,还是动态执行环境。
最小自检任务
阅读下面代码,判断每一行输出或异常的来源,并说明它分别触发了哪个 built-in protocol surface。
class Box:
def __init__(self, values):
self.values = list(values)
def __iter__(self):
return iter(self.values)
def __getattr__(self, name):
if name == "size":
return len(self.values)
raise AttributeError(name)
def __repr__(self):
return f"Box({self.values!r})"
box = Box([10, 20])
box.__len__ = lambda: 2
print(hasattr(box, "size"))
print(repr(box))
print(next(iter(box)))
print(len(box))
答案要点
hasattr(box, "size") 返回 True,因为 hasattr 会尝试属性读取,常规查找失败后进入 Box.__getattr__,size 被动态计算为 len(self.values)。这里触发的是 attribute protocol,结果来自 __getattr__ 的兜底路径。
repr(box) 返回形如 Box([10, 20]) 的字符串,因为 repr 调用类型上的 __repr__。这里触发的是 representation protocol,返回值必须是字符串。
next(iter(box)) 返回 10。iter(box) 调用类型上的 __iter__,得到 self.values 的 list iterator;next 推进 iterator 并返回第一个元素。这里触发的是 iterator protocol,耗尽时才会进入 StopIteration 路径。
len(box) 抛出 TypeError。虽然实例字典里有 box.__len__,但 len 这类隐式 special method 调用从类型对象查找长度协议入口。Box 类型没有定义 __len__,所以实例上的同名函数不会让 len(box) 成立。这个结论验证了 builtins 作为 protocol surface 时,动态属性和类型级协议入口属于不同层级。
本章知识点总结
- 内置入口:builtins 把普通函数调用连接到对象协议、类型关系、属性查找、资源边界和动态执行路径。
- 长度协议:
len(obj)需要对象类型提供长度能力,返回值语义上是非负整数,并受 CPython 长度表示边界约束。 - 迭代协议:
iter(obj)负责获取 iterator,next(iterator)负责推进 iterator,耗尽路径使用StopIteration或 default 返回值收束。 - 类型查询:
type(obj)返回直接类型,isinstance和issubclass查询继承或虚拟子类关系,三者服务不同粒度判断。 - 对象身份:
id(obj)在对象生命周期内稳定唯一,CPython 中通常对应内存地址这一实现细节。 - 属性入口:
getattr、setattr、hasattr进入 attribute protocol,并可能触发 descriptor、__getattribute__、__getattr__和__setattr__。 - 调用检查:
callable(obj)只说明对象具备可调用入口,实际调用仍可能因参数、状态或函数体异常失败。 - 表示协议:
repr面向开发者复盘,str面向默认展示,format面向明确格式说明,三者返回值都必须是字符串。 - 资源边界:
open返回 file object,路径、模式、编码、权限和生命周期共同决定 I/O 行为。 - 动态求值:
eval执行表达式并返回结果,exec执行语句 suite 并通过 namespace 暴露状态变化。 - 名字映射:
eval和exec的可见名字由 globals、locals 和__builtins__映射共同决定。 - 协议分发表:special method 的隐式查找通常面向类型对象,实例字典里的同名属性不等于协议能力。
- 源码阅读:阅读 CPython builtins 时,先看
Python/bltinmodule.c外层入口,再追到对象 API、类型 slot、I/O 或编译执行层。 - 判断顺序:分析 built-in 行为时,先查名字覆盖,再查输入层级,再查协议入口,最后检查返回值契约和异常来源。