前言
「内建函数」是 built-in function 的另一种常见译法,和「内置函数」指的是同一个东西,不是第三类神秘存在。网上有些文章把「内置」「内建」「标准库」「系统函数」并列成四个层级,那是不准确的说法——Python 里只有三层:内置命名空间里的名字、标准库模块里的函数、以及你自己写的代码。
很多初学者对内置函数有误解,觉得它们带某种编译期魔法。真相要朴素得多:内置函数就是解释器启动时预置在builtins模块里的一批普通名字,任何作用域都能不带import直接引用它们,仅此而已。理解这一点之后,一堆现象就顺了:为什么能用len当变量名(然后把自己的len用崩)、为什么dir(builtins)能看到完整清单、为什么把builtins打补丁能全局生效。
本文从"名字从哪来"讲到"它们怎么调用你的代码",配可运行示例和一批踩过才知道的坑。示例基于 Python 3.8+。
一、名字从哪里来:builtins 模块与 LEGB
Python 找名字时按 LEGB 顺序:Local(局部)→ Enclosing(闭包)→ Global(模块全局)→ Builtins(内置)。内置那一层就是builtins模块提供的。
# 适用于 Python 3.8+
import builtins
print(builtins.len([1, 2, 3])) # 3,通过模块也能拿到
print(len is builtins.len) # True,是同一个对象
names = [n for n in dir(builtins) if not n.startswith('_')]
print(len(names))
print(sorted(names)[:12])想验证一个名字是不是内置的,可以检查它是否在builtins.__dict__里:
# 适用于 Python 3.8+
import builtins
def describe(name):
obj = getattr(builtins, name, None)
return f"{name}: {obj!r}"
print(describe("sum"))
print("sum" in builtins.__dict__) # True关于__builtins__(注意是两条下划线包着的复数形式):它是每个模块全局命名空间里自动出现的一个名字,内容通常是builtins模块或其字典。具体是模块还是字典,属于 CPython 的实现细节,不要依赖。要访问内置命名空间,稳妥做法永远是显式import builtins,而不是去读__builtins__。
二、它们长什么样:C 实现与类型对象
内置函数大多是 C 实现的,所以它们在 Python 层面没有可读的源码。看一个名字指向什么,最直接的办法是用type():
# 适用于 Python 3.8+
print(type(len)) # <class 'builtin_function_or_method'>
print(type(print)) # <class 'builtin_function_or_method'>
print(type(42)) # <class 'int'>有意思的是,map、filter、zip、enumerate、reversed这几个名字在 CPython 里指向的是类型对象而不是builtin_function_or_method:
# 适用于 Python 3.8+
print(type(map)) # 在 CPython 中输出 <class 'map'>
print(type(zip)) # <class 'zip'>这是 CPython 的实现方式;官方文档统一把它们称作内置函数(built-in function),所以写代码时不必纠结这个分类,知道type()能查就行。
有些内置函数还有属性可读,比如len.__name__、len.__doc__。想批量了解,用help():
# 适用于 Python 3.8+
help(len)help()会打印出签名和文档字符串。注意签名里如果出现/,表示它前面的参数是仅位置参数(positional-only),不能按关键字传;如果出现*,表示它后面的参数是仅关键字参数。
三、内置函数如何"调用"你的代码:数据模型协议
这是"弄懂"的关键:内置函数本身不做所有事,它们把工作转交给对象的协议方法(dunder method)。
| 你写的代码 | 实际被调用的方法 |
|---|
len(obj) | obj.__len__() |
obj in container | container.__contains__(obj) |
iter(obj) | obj.__iter__() |
str(obj) | obj.__str__()(缺省回退到__repr__) |
bool(obj) | obj.__bool__()(缺省回退到__len__) |
obj[key] | obj.__getitem__(key) |
hash(obj) | obj.__hash__() |
abs(obj) | obj.__abs__() |
round(obj, n) | obj.__round__(n) |
# 适用于 Python 3.8+
class Bag:
def __init__(self, items):
self.items = list(items)
def __len__(self):
return len(self.items)
def __contains__(self, x):
return x in self.items
b = Bag(["apple", "pear"])
print(len(b)) # 2,走 __len__
print("pear" in b) # True,走 __contains__
print(bool(Bag([]))) # False,没有 __bool__ 时回退到 __len__所以自定义类能不能被len()、in、for支持,取决于你有没有实现对应协议;内置函数只是在合适的时机去调用它们。
四、惰性还是立即:返回类型决定用法
内置函数里有相当一批是惰性的——它们返回迭代器而不是容器。这是最容易出 bug 的地方,因为迭代器只能消费一次。
| 内置函数 | 返回 | 惰性 | 能否重复遍历 |
|---|
len、sorted、sum、min、max、list、tuple | 具体结果 | 否 | 可以 |
map、filter、zip、enumerate、reversed | 迭代器 | 是 | 不可以 |
range | range对象 | 惰性求值 | 可以(本身是序列) |
# 适用于 Python 3.8+
nums = [1, 2, 3, 4]
evens = filter(lambda x: x % 2 == 0, nums)
print(list(evens)) # [2, 4]
print(list(evens)) # [],迭代器已被消费完
# range 是特例:它是惰性求值的序列,不是一次性迭代器
r = range(3)
print(list(r), list(r)) # [0, 1, 2] [0, 1, 2]range是序列(支持len()、下标、in),而map/filter/zip/enumerate/reversed是一次性迭代器。这个区别决定了你要不要先list(...)物化。
五、覆盖与打补丁:builtins 的两种用法
既然内置函数只是名字,就有两件事可以做:在局部作用域遮蔽它,或者全局替换它。
# 适用于 Python 3.8+
# 1) 遮蔽:作用域内看不到内置的那个了
sum = 0
# print(sum([1, 2])) # TypeError:sum 现在是 0,不是函数
# 2) 全局打补丁(谨慎使用,会影响整个进程)
import builtins
original_open = builtins.open
def loud_open(*args, **kwargs):
print("正在打开:", args[0] if args else "")
return original_open(*args, **kwargs)
builtins.open = loud_open
with open(__file__, encoding="utf-8") as f:
f.readline()
builtins.open = original_open # 用完立刻还原全局打补丁适合调试、埋点、教学演示;不要在生产库的导入期做这件事,它会让别人的代码行为变得难以预期。
常见坑点
坑 1:用内置函数名当变量名。
❌list = [1, 2, 3]、len = 5、id = "abc"—— 之后整个作用域里list()、len()、id()全废。
✅ 换名:items、length、ident。用编辑器高亮能很快发现这类遮蔽。
坑 2:把惰性函数当容器用。
❌if map(f, xs):—— 迭代器对象本身恒为真,条件永远成立。
✅if any(map(f, xs)):或先result = list(map(f, xs))再判断。
坑 3:对着已消费的迭代器再遍历。
❌
z = zip([1, 2], [3, 4])
print(list(z)) # [(1, 3), (2, 4)]
print(list(z)) # []✅ 需要多次用就先pairs = list(z)。
坑 4:把__builtins__当稳定接口。
❌ 直接读__builtins__并假设它是字典(或假设它是模块),在不同执行环境下它可能是另一种。
✅ 显式import builtins,通过builtins.__dict__或getattr(builtins, name)访问。
坑 5:忽略签名里的/和*。
❌ 把sorted的key按位置传:sorted(data, lambda x: x[1])会抛TypeError,因为key与reverse都是仅关键字参数。
✅ 写成sorted(data, key=lambda x: x[1]);不确定时先help(函数名),看清签名里/(其前是仅位置参数)与*(其后是仅关键字参数)的位置。
坑 6:以为sorted和sort一样能原地改。
❌data = data.sort()——sort原地排序并返回None,data变None。
✅data = sorted(data)或data.sort()(不接返回值)。
坑 7:全局打补丁后不还原。
❌ 在库的__init__.py里替换builtins.open却不恢复,污染整个进程。
✅ 只在调试时临时替换,并在finally里还原原对象。
坑 8:以为内置函数比手写循环"总是更快"。
❌ 不加判别地把所有for都换成map+lambda,还期望变快——额外的函数调用开销有时反而更慢。
✅ 优先看可读性;要比较性能就自己用timeit实测,不要采信任何没测过的倍数说法。
总结
| 要点 | 结论 |
|---|
| 名字来源 | builtins模块,解释器启动时注入 |
| 查找顺序 | LEGB:局部 → 闭包 → 全局 → 内置 |
| 本质 | 普通名字;大多 C 实现,type()可查 |
| 与你的代码的关系 | 通过__len__、__iter__等协议方法回调 |
| 返回类型 | 一部分立即返回结果,一部分返回一次性迭代器 |
| 可覆盖 | 作用域内遮蔽、全局builtins打补丁(慎用) |
把内置函数理解成"预置在builtins里、按 LEGB 查得到的普通名字",是打通所有相关疑问的钥匙。剩下的两件事就是:记住哪些函数返回的是只能用一次的迭代器,以及记不清签名时用help()去查,而不是凭印象写参数。
(Python 2 与 Python 3 的内置函数有一处断崖式差异:Python 2 的print是语句、raw_input是读取输入的函数、xrange存在、unicode是内置类型;这些在 Python 3 中分别是函数print()、input()、range、str。Python 2.7 已于 2020 年 1 月 1 日停止维护,本文一律使用 Python 3 的写法。)