ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

一篇文章弄懂Python中的内建函数

一篇文章弄懂Python中的内建函数

前言


「内建函数」是 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 containercontainer.__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迭代器是不可以

rangerange对象惰性求值可以(本身是序列)



# 适用于 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 的写法。)




RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进