跳转到内容
新建笔记

Python 函数与内置工具:参数、迭代与执行环境

函数把一项计算或操作封装为可以重复调用的对象。阅读一个函数时,先看它接受什么参数、返回什么结果、是否改变外部对象,再看实现。内置函数也遵守各自的调用约定;名字出现在速查表里,不等于任意对象都能传给它。

本文示例以 Python 3.11 为基线。每段 Python 代码可独立保存并运行;其中的 assert 用于检查结果,运行时不要使用会禁用断言的 -O。类型注解的作用另见 类型注解与 typing。

def 执行时创建函数对象并绑定名字;函数体通常到调用时才执行。return 把值交给调用者,没有显式返回值时结果为 None。print() 则输出文本,它不能替代需要继续计算的返回值。

参数既可以限制位置,也可以限制关键字。下面用一个小例子观察绑定结果,而不是只记住星号的形状。

def combine(first, /, second=0, *extra, scale=1, **labels):
"""Combine numeric inputs and return their labels separately."""
total = (first + second + sum(extra)) * scale
return total, labels
assert combine(2) == (2, {})
assert combine(2, second=3, scale=10) == (50, {})
assert combine(2, 3, 4, 5, scale=2, unit="ms") == (28, {"unit": "ms"})
args = (2, 3, 4)
options = {"scale": 2, "unit": "ms"}
assert combine(*args, **options) == (18, {"unit": "ms"})
# A spelling equal to a positional-only parameter can still occur in **labels.
assert combine(2, first="label") == (2, {"first": "label"})
try:
combine(first=2)
except TypeError:
pass # This keyword did not supply the required positional argument.
else:
raise AssertionError("missing positional argument was accepted")
def report_only(value):
print(value)
assert report_only("done") is None
print("parameter and return checks passed")

这里 / 前的 first 只能按位置提供;second 可以按位置或关键字提供;extra 收集其后的额外位置参数;scale 只能按关键字提供;labels 收集剩余关键字。调用端的 *args、**options 是展开,定义端则是在说明如何接收。重复给同一个普通参数赋值、遗漏必需参数或展开非字符串关键字键,都会报错。函数参数

默认值、可变对象与固定次数循环

跳转到“默认值、可变对象与固定次数循环”

默认值在执行函数定义时求值,不是在每次调用时重新求值。因此,需要每次调用都得到新列表时,可以使用 None 作为默认标记,再在函数体内创建列表。传入已有列表时,是否修改它应成为明确约定。

def append_value(value, bucket=None):
if bucket is None:
bucket = []
bucket.append(value)
return bucket
first = append_value(1)
second = append_value(2)
assert first == [1] and second == [2] and first is not second
shared = [10]
assert append_value(20, shared) is shared
assert shared == [10, 20]
def constant(n: int) -> int:
"""Run a fixed number of iterations; n is deliberately unused."""
count = 0
for _ in range(100_000):
count += 1
return count
assert constant(0) == constant(1_000_000) == 100_000
print("default-value and fixed-loop checks passed")

原笔记的 constant(n) 保留在这里,用来说明复杂度的参照对象:循环次数固定,与 n 无关,所以相对于 n 是常数阶。这不代表执行时间为零,也不代表这个未使用参数的接口适合真实业务。n: int 与 -> int 本身不会在调用时强制类型检查;函数注解语法和后来的 typing 类型工具也不是同一项版本特性。

名字重新绑定与修改对象内容的区别,见 作用域与名字绑定;需要独立对象时,还应区分 浅拷贝与深拷贝。

下表保留原清单中的全部名称,按用途组织。int、list、property 等本身是类型,也可以像函数一样调用;不能把所有可调用名称都理解成同一种普通函数。

用途原清单名称使用时检查什么
数值与进制abs()、divmod()、pow()、round()、bin()、oct()、hex()参数是否支持对应数值协议;round 的中点规则与浮点误差
构造与转换bool()、int()、float()、complex()、str()、list()、tuple()、dict()、set()、frozenset()类型转换可能失败;可变性和元素要求各不相同
字节与字符bytes()、bytearray()、memoryview()、chr()、ord()编码、整数范围、缓冲区是否共享、Unicode 码点
迭代与选择iter()、next()、enumerate()、zip()、map()、filter()、reversed()返回迭代器还是容器、何时耗尽、长度不一致如何处理
汇总与排列len()、all()、any()、sum()、min()、max()、sorted()空输入、真值规则、比较方式与返回值
范围与切片range()、slice()停止值不包含在内;slice 描述索引规则,本身不是可迭代序列
表示与格式repr()、ascii()、format()面向调试还是展示;ascii 会转义非 ASCII 字符;格式规格由对象解释
类型与身份type()、object()、isinstance()、issubclass()、id()、hash()、callable()继承关系、可哈希性、对象生存期;可调用不保证某组参数能调用成功
属性查询与修改getattr()、setattr()、delattr()、hasattr()、dir()、vars()属性访问可能执行描述器或钩子;并非每个对象都有 __dict__
类与方法工具classmethod()、staticmethod()、property()、super()绑定对象、属性描述器和方法解析顺序,见 类与继承
命名空间与代码globals()、locals()、compile()、eval()、exec()代码模式、显式命名空间;不要依赖修改 locals 来重绑定函数局部变量
输入、输出与帮助input()、print()、open()、help()文本输入与转换分开、文件生命周期、交互式帮助的使用环境

内置函数的完整版本清单见 Python 3.11 内置函数。上表保留的是这篇原笔记涉及的名称,不声称已经列出该版本所有内置名称。

迭代器、空输入和切片

跳转到“迭代器、空输入和切片”

map()、filter()、zip() 等通常按需产生结果,转成 list 后才在这里完整求值。迭代器耗尽后不会自动从头开始;需要再次遍历时,应保留原容器并重新调用 iter()。

values = [0, 1, 2, 3]
assert list(map(lambda x: x * 2, values)) == [0, 2, 4, 6]
assert list(filter(lambda x: x > 1, values)) == [2, 3]
assert list(enumerate("ab", start=1)) == [(1, "a"), (2, "b")]
assert list(zip([1, 2], ["a"])) == [(1, "a")]
try:
list(zip([1, 2], ["a"], strict=True))
except ValueError:
pass
else:
raise AssertionError("strict zip accepted unequal lengths")
iterator = iter([10])
assert next(iterator) == 10
assert next(iterator, "end") == "end"
assert list(iterator) == []
assert list(reversed(values)) == [3, 2, 1, 0]
assert sorted([3, 1, 2]) == [1, 2, 3]
assert values == [0, 1, 2, 3]
assert all([]) is True and any([]) is False
assert sum([]) == 0
assert min([], default=None) is None
assert max(values) == 3 and len(values) == 4
assert list(range(1, 6, 2)) == [1, 3, 5]
selection = slice(2, 5, 2)
assert "abcdefg"[selection] == "ce"
try:
list(selection)
except TypeError:
pass
else:
raise AssertionError("slice unexpectedly became an iterable")
print("iteration checks passed")

这也修正了原表中 list(slice(...)) 的例子:切片要交给支持索引的对象使用。sorted() 返回新列表;对于列表原地排序应查 list.sort()。all([]) 为真、any([]) 为假是定义明确的空输入结果,不应当作数据已经通过了某个业务完整性检查。

数值、文本与共享字节缓冲区

跳转到“数值、文本与共享字节缓冲区”

chr() 和 ord() 对应 Unicode 码点,不限于 ASCII。round() 遇到两个同样近的候选值时,内置数值类型通常取偶数一侧;二进制浮点表示还可能影响看似处于十进制中点的值。

assert abs(-10) == 10
assert divmod(10, 3) == (3, 1)
assert pow(2, 3) == 8
assert (bin(13), oct(13), hex(13)) == ("0b1101", "0o15", "0xd")
assert round(2.5) == 2 and round(3.5) == 4
assert round(2.675, 2) == 2.67
assert chr(0x4E2D) == "中" and ord("中") == 0x4E2D
assert ascii("中") == "'\\u4e2d'"
assert format(123, "08") == "00000123"
assert repr("a\nb") == "'a\\nb'"
data = bytearray([65, 66, 67])
view = memoryview(data)
view[1] = 90
assert data == bytearray(b"AZC")
snapshot = bytes(data)
view.release()
data[0] = 88
assert snapshot == b"AZC" and data == bytearray(b"XZC")
assert bool([]) is False and bool([0]) is True
assert int("10", 16) == 16
assert float("3.5") == 3.5
assert complex(2, 3) == 2 + 3j
assert str(12) == "12"
assert tuple("ab") == ("a", "b")
assert list("ab") == ["a", "b"]
assert dict(key="value") == {"key": "value"}
assert set([1, 1, 2]) == {1, 2}
assert frozenset([1, 1, 2]) == frozenset({1, 2})
print("numeric, text and buffer checks passed")

memoryview 直接观察支持缓冲区协议的对象;上面的赋值改动了原 bytearray。bytes(data) 创建的是此时的不可变字节值,之后修改原 bytearray 不会改变它。这里释放视图也让缓冲区的使用边界明确。

属性、身份与可调用对象

跳转到“属性、身份与可调用对象”

id() 在对象存活期间唯一且稳定;对象销毁后该数值可能复用。将它称为“内存地址”是特定解释器的实现描述,不能当成 Python 程序跨实现依赖的接口。hash() 用于哈希容器,不能代替持久 ID;字符串的哈希值也不应被当作跨进程稳定结果。

class Item:
category = "sample"
def __init__(self, value):
self.value = value
def __call__(self, offset):
return self.value + offset
item = Item(10)
alias = item
assert item is alias and id(item) == id(alias)
assert type(item) is Item
assert isinstance(item, Item) and issubclass(Item, object)
assert callable(item) and item(2) == 12
try:
item()
except TypeError:
pass
else:
raise AssertionError("callable() unexpectedly checked required arguments")
assert getattr(item, "value") == 10
assert getattr(item, "missing", None) is None
setattr(item, "label", "A")
assert hasattr(item, "label") and "label" in dir(item)
assert vars(item)["label"] == "A"
delattr(item, "label")
assert not hasattr(item, "label")
assert vars(Item)["category"] == "sample"
assert hash((1, 2)) == hash((1, 2))
try:
hash([])
except TypeError:
pass
else:
raise AssertionError("mutable list unexpectedly became hashable")
assert object() is not object()
print("attribute and callable checks passed")

hasattr() 会实际尝试属性访问,并按是否抛出 AttributeError 判断,因此不能把它理解成完全没有副作用的名字查询。dir() 方便交互式探索,也不保证列出对象在所有动态情况下可能提供的每个属性。自定义属性钩子和 __call__ 的协议细节见 特殊方法。

globals、locals 与显式执行环境

跳转到“globals、locals 与显式执行环境”

globals() 返回当前模块的全局命名空间字典。locals() 在这里用于读取函数局部变量;Python 3.11 下不要依赖修改它来改变函数实际局部绑定。确实要执行动态代码时,显式提供并读取命名空间字典。

下面只执行程序中固定的示例字符串。eval、exec 不是解析任意外部输入的通用数据接口;去掉几个内置名称也不构成安全隔离。

module_value = 10
assert globals()["module_value"] == 10
def inspect_local():
local_value = 20
return locals()["local_value"]
assert inspect_local() == 20
exec('print("Hello, World!")')
namespace = {"greeting": "Hello"}
code = compile(
"def greet(name):\n"
" return greeting + ', ' + name\n"
"def add(a, b):\n"
" return a + b\n"
"result = add(3, 5)\n",
"<example>",
"exec",
)
assert exec(code, namespace) is None
assert namespace["greet"]("Alice") == "Hello, Alice"
assert namespace["result"] == 8
assert eval(compile("x + y", "<expression>", "eval"), {"x": 2, "y": 3}) == 5
global_space = {"greeting": "Hello"}
local_space = {}
exec("message = greeting + ', Bob'", global_space, local_space)
assert local_space["message"] == "Hello, Bob"
exec("value = 7\ndef read_value():\n return value\n", global_space, local_space)
assert local_space["value"] == 7
try:
local_space["read_value"]()
except NameError:
pass # The function resolves value in global_space, not local_space.
else:
raise AssertionError("separate exec namespaces were conflated")
global_space["value"] = 9
assert local_space["read_value"]() == 9
print("explicit-namespace checks passed")

同一个字典同时用作全局与局部环境时,赋值结果和新定义函数使用的全局环境保持一致。分开两个字典时,顶层赋值可落在局部字典,而函数体中的全局查询使用全局字典,这正是上面 read_value() 先失败后返回 9 的原因。需要输出结果时读回显式字典,比假定 exec() 一定替调用函数创建了可直接访问的新局部变量更清楚。动态代码执行

compile() 的 "eval" 模式用于表达式,"exec" 模式用于语句组;编译得到代码对象还没有执行其中的操作。处理 JSON 等数据时使用对应数据格式工具,而不是把数据改成 Python 源码再执行。

input() 返回文本,读到输入结束时可能抛出 EOFError;需要整数时再显式转换并处理错误。open() 的文本模式应明确编码,并用 with 限定文件使用时间;详见 文件、路径与进程环境。help() 适合在交互式解释器中查询对象签名和说明,例如 help(print),不必把整份帮助输出写进业务日志。

原笔记中的 super() 五类用法——调用重写方法、初始化、多个基类、转交参数和访问继承属性——集中到 类与协作继承,其中会区分方法解析顺序与“简单从左到右依次调用所有父类”的错误理解。