Skip to content
This page has been auto-translated and may contain errors.View in English

函数

随着程序变大,你会在多个地方写出相同的逻辑。函数让你只写一次逻辑,给它命名,然后到处使用。在一个地方修复它,每一次调用都会自动获得修复。

函数是代码复用和抽象的主要单元。它们封装一种行为,赋予其名称,定义清晰的接口(参数和返回值),并可在任何地方被调用。命名良好的函数本身就是文档:validate_email() 让你不用读代码就知道一段代码在做什么。

在 Python 中,def 创建一个函数对象,并将其绑定到当前作用域中的一个名称。函数是一等对象:它们可以被赋值给变量、存储在集合中、作为参数传递,并从其他函数中返回。闭包从外层作用域捕获自由变量。理解这一点会让高阶编程变得自然。

python
def greet(name):
    return f"Hello, {name}!"

print(greet("小明"))   # "Hello, 小明!"
print(greet("小红"))   # "Hello, 小红!"

写一次,到处使用,一处修复。

定义函数

def 关键字开启一个函数定义,后面跟着名称、括号、冒号和缩进的函数体。函数在被调用之前什么都不做。用 def 定义它,然后用名称加 () 调用它。

def 是一条语句,它创建一个函数对象并将其绑定到当前作用域中的给定名称。函数体在定义时不会被执行;只有在函数被调用时才运行。没有 return 语句的函数隐式返回 None

def 是一条复合语句,它将函数体编译为一个代码对象,并将一个新的函数对象绑定到当前命名空间中的名称上。函数对象存储对其代码对象的引用、其默认参数值,以及对外层作用域的引用(用于闭包)。函数体仅在调用时执行。

python
def say_hello():
    print("Hello!")

say_hello()   # 调用函数

参数与实参

参数是你的函数期望的输入。在括号内列出它们。当你调用函数时,你传入的值会按顺序与参数匹配。

参数定义了函数的接口。实参是在调用时传入的具体值。位置实参按位置匹配;关键字实参按名称匹配。默认值使参数变为可选。

Python 有四种参数类型:位置或关键字(默认)、仅关键字(* 之后)、仅位置(/ 之前,Python 3.8+)和可变参数(*args**kwargs)。在调用时,位置实参从左到右绑定;关键字实参按名称绑定。冲突会引发 TypeError。默认值在函数定义时被计算一次,而不是每次调用时。

python
def greet(name, greeting):
    print(f"{greeting}, {name}!")

greet("小明", "Hello")    # "Hello, 小明!"
greet("小红", "Hi")       # "Hi, 小红!"

参数 vs 实参

参数是函数定义中的名称。实参是你调用函数时传入的实际值。在实际使用中,人们常常混用这两个词。阅读文档时知道这个区别即可。

默认值

你可以给参数赋一个默认值。如果调用者没有提供该实参,就使用默认值。带默认值的参数必须位于没有默认值的参数之后。

默认值使参数变为可选。它们在定义时被计算一次,而不是每次调用时。这对可变默认值很重要:def f(items=[]) 在所有调用中共享同一个列表。解决方法是使用 None 作为默认值,并在函数体内创建列表。

默认值作为 f.__defaults__(位置)和 f.__kwdefaults__(仅关键字)存储在函数对象上。它们在 def 执行时被计算一次,而不是每次调用时。可变默认值陷阱(def f(x=[]))是经典的 Python 坑:列表被创建一次,并在多次调用之间被原地修改。惯用的修复方法是:def f(x=None): if x is None: x = []

python
def greet(name, greeting="Hello"):
    print(f"{greeting}, {name}!")

greet("小明")           # "Hello, 小明!"
greet("小明", "Hi")     # "Hi, 小明!"

带默认值的参数必须位于没有默认值的参数之后。

关键字参数

调用函数时,你可以为实参命名。这让调用变得可读,尤其对于有许多参数的函数,并且可以以任何顺序传递它们。

关键字实参让函数调用具有自我说明性。你可以混合使用位置和关键字:位置实参必须排在前面。对于带有布尔标志或多个相似类型参数的函数,关键字实参可以防止因传错顺序而产生的隐性错误。

关键字实参按名称绑定,而不是按位置。在调用时,位置实参必须位于关键字实参之前。同一个实参既按位置又按名称传递会引发 TypeError。要强制使用仅关键字参数,将它们放在参数列表中裸 * 之后:def f(a, *, b) 使 b 成为仅关键字参数。

python
def describe_player(name, score, level):
    print(f"{name} | Score: {score} | Level: {level}")

describe_player("小明", 87, 5)                        # 位置参数
describe_player(name="小明", level=5, score=87)       # 关键字,任意顺序
describe_player("小明", level=5, score=87)            # 混合:位置参数在前

返回值

return 将一个值返回给调用者。没有 return,函数会返回 None。一旦 return 执行,函数立即退出。该块中后续的任何代码都会被跳过。

return 退出函数并将一个值传递给调用者。没有显式 return 的函数隐式返回 Nonereturn 可以出现在函数体的任何位置,并可以多次使用;第一个被执行到的会结束函数。这使得早期返回对守卫子句(guard clauses)很有用。

return 发出一条 RETURN_VALUE 字节码,它弹出栈顶并将其交给调用者的栈帧。一个执行到末尾的函数隐式返回 None。多个 return 语句是可以的,而且常比带有复杂条件的单一 return 更整洁("早期返回"模式)。try 块内的 return 仍会执行任何相关的 finally 子句。

python
def add(a, b):
    return a + b

result = add(3, 4)   # result = 7
print(result)

return 也会立即退出函数。该块中之后的任何代码都不会运行。

返回多个值

Python 允许通过用逗号分隔来返回多个值。调用者会收到它们作为一个元组,并可以在一行中将它们解包到不同的名称中。

使用逗号返回多个值会把它们打包到一个元组中。调用者用匹配的名称解包。这是 Python 中函数自然产生多个结果时的惯用法。它不是特殊功能;它就是元组的打包和解包。

return a, b 通过隐式打包将这些值打包成一个元组。调用者用 x, y = f() 解包,这会调用返回元组的 __iter__。为了类型提示中的清晰,可以将返回类型注释为 tuple[int, str] 或使用命名元组。对于少量的值,返回普通元组没问题;对于多于两三个值,考虑命名元组或数据类。

python
def min_max(numbers):
    return min(numbers), max(numbers)

low, high = min_max([3, 7, 1, 9, 4])
print(low, high)   # 1 9

low, high = ... 语法是解包:Python 将每个返回值赋给相应的名称。

作用域

在函数内部创建的变量只存在于该函数内部。你无法从外部看到它们。在所有函数之外定义的变量在任何地方都可见,但如果没有显式声明,你无法在函数内部修改它们。

Python 有局部作用域(在函数内部)、外层作用域(嵌套函数的外部函数中)、全局作用域(模块级)和内置作用域。名称查找按顺序遵循 LEGB 规则。局部变量会遮蔽同名的外层变量。global 声明一个名称引用模块级绑定。

Python 的 LEGB 名称解析:Local(局部)、Enclosing(闭包)、Global(模块)、Built-in(内置)。每个 def 创建一个新的局部命名空间。读取全局变量会自动工作;写入它需要 global x 以避免创建一个局部遮蔽。nonlocal x 访问最近的外层(非全局)作用域,使闭包能够修改捕获的变量。过度使用 global 会让代码难以推理;优先使用参数和返回值。

python
def calculate():
    result = 42   # 该函数的局部变量
    return result

calculate()
print(result)   # NameError, result 在外面不存在
python
count = 0

def increment():
    global count    # 声明你想修改全局变量
    count += 1

increment()
print(count)   # 1

使用 global 应该是最后的手段。它使代码更难推理。优先使用传入值并返回值的方式。

*args 和 **kwargs

有时你不知道函数会接收多少个实参。*args 将任意数量的位置实参收集到一个元组中。**kwargs 将任意数量的关键字实参收集到一个字典中。argskwargs 这两个名称只是约定;星号才是关键。

*args 将多余的位置实参收集到一个元组中。**kwargs 将多余的关键字实参收集到一个字典中。两者都可以与常规参数组合。常规参数排在最前面,然后是 *args,然后是仅关键字参数,然后是 **kwargs。它们对于将参数传递给另一个函数的包装函数很有用。

*args 从剩余的位置实参创建一个 tuple**kwargs 从剩余的关键字实参创建一个 dict。参数顺序:位置或关键字、*args、仅关键字、**kwargs。在调用处,*iterable 解包位置实参,**mapping 解包关键字实参。它们是对称的:签名中的 * 收集;调用处的 * 解包。

python
def total(*args):
    return sum(args)

total(1, 2, 3)          # 6
total(1, 2, 3, 4, 5)   # 15
python
def display(**kwargs):
    for key, value in kwargs.items():
        print(f"{key}: {value}")

display(name="小明", score=87, level=5)

你可以将它们与常规参数混合使用。常规参数排在最前面:

python
def describe(title, *tags, **metadata):
    print(f"{title} | tags: {tags} | meta: {metadata}")

describe("Python intro", "beginner", "python", author="小明", year=2024)

文档字符串

文档字符串是函数顶部的一个字符串,描述它做什么。Python 编辑器和工具用它来在你悬停在函数调用上时显示帮助。使用三引号,对简单函数写一行。

文档字符串作为 f.__doc__ 存储,并由 help() 显示。约定是一行摘要,后面可选地跟一个空行和更多细节。对于面向公众的函数,文档字符串是工具可以呈现的文档;对于从多个地方调用的任何内容,它不是可选的。

文档字符串是放在函数、类或模块体第一条语句的字符串字面量。它们作为对象上的 __doc__ 存储。PEP 257 定义了约定。Sphinx、pydoc 和 IDE 等工具都依赖于 __doc__。对于文档字符串中的类型信息,使用 Google、NumPy 或 Sphinx 风格;对于现代代码,签名中的类型提示优于文档字符串中的类型注释。

python
def normalise(value, min_val, max_val):
    """Scale a value to the 0-1 range given the known min and max."""
    return (value - min_val) / (max_val - min_val)
python
def build_url(base, version, resource, *, secure=True):
    """
    Build an API endpoint URL.

    Returns a fully-qualified URL string. If secure is False,
    the URL will use http instead of https.
    """
    scheme = "https" if secure else "http"
    base   = base.replace("https://", "").replace("http://", "")
    return f"{scheme}://{base}/{version}/{resource}"

为任何从名称和签名无法明显自解释的函数写一个文档字符串。

类型提示

类型提示让你注释函数期望和返回的类型。Python 在运行时不强制执行它们,但编辑器使用它们在你运行任何东西之前捕获错误。冒号前的 -> 指定返回类型。

类型提示是工具会验证的文档。编辑器和类型检查器(mypy、pyright)使用它们在运行时之前捕获类型不匹配。它们在标准 Python 中没有运行时效果。对于没有返回值的函数,-> None 是正确的注释。对于泛型容器,使用 list[int]dict[str, int](Python 3.9+)。

类型提示在运行时由 typing.get_type_hints() 处理,但对执行没有直接影响。静态类型检查器在运行时之前分析它们。PEP 484 引入了注释系统;PEP 585 允许在 Python 3.9+ 中使用内置泛型如 list[int] 而不需要 typing 导入。函数签名上的 -> None 既表示"什么都不返回",也表示"不应在表达式上下文中使用"。对于复杂类型,typing.Protocoltyping.TypeVartyping.overload 提供了完整的静态类型能力。

python
def greet(name: str, score: int) -> str:
    return f"{name} scored {score}"
python
def log(message: str) -> None:
    print(f"[LOG] {message}")
python
def top_scores(scores: list[int], n: int) -> list[int]:
    return sorted(scores, reverse=True)[:n]

类型提示是可选的,但对任何会从多个地方被调用的函数都是有价值的。它们是工具可以验证的文档。

函数作为值

Python 中的函数是值,就像字符串或数字一样。你可以将它们赋值给变量,并将它们传递给其他函数。这就是 sorted() 接受 key= 函数的方式。

函数是一等对象:它们有一个类型(function),可以存储在变量和集合中,可以作为实参传递或作为值返回。这是 sorted(key=...)map()filter() 等高阶函数的基础。

函数是 function 类型的对象,具有属性:__name____doc____annotations____defaults____code____closure__。它们按引用传递,不被复制。如果内部函数引用了外部作用域的变量,从函数返回一个函数就会创建一个闭包:这些变量存储在 f.__closure__ 中。

python
def double(x):
    return x * 2

def apply(func, value):
    return func(value)

apply(double, 5)   # 10

将函数作为实参传递在 sorted()map()filter() 中经常出现。你也会在 Lambda、推导式和 zip 章节中看到它。

实战

两个协同工作的函数:letter_grade 将分数转换为字母,summarise 为列表中的每个分数调用它:

python
def letter_grade(score: int) -> str:
    if score >= 90:
        return "A"
    elif score >= 80:
        return "B"
    elif score >= 70:
        return "C"
    else:
        return "F"

def summarise(scores: list[int]) -> None:
    total  = sum(scores)
    avg    = total / len(scores)
    grades = [letter_grade(s) for s in scores]
    print(f"Average: {avg:.1f}")
    print(f"Grades: {', '.join(grades)}")

summarise([87, 92, 74, 65, 91])

一个日志格式化函数和一个使用它的文件处理函数,带有一个 dry_run 默认参数,在没有显式禁用之前防止副作用:

python
def format_log(level: str, message: str) -> str:
    return f"[{level.upper():5}] {message}"

def process_file(path: str, dry_run: bool = True) -> bool:
    print(format_log("info", f"Processing {path}"))
    if dry_run:
        print(format_log("info", "Dry run, no changes made"))
        return True
    return True

process_file("report.csv")
process_file("report.csv", dry_run=False)

一个单值归一化器和一个基于它构建的列归一化器,带类型提示和文档字符串。列函数计算一次范围,并为每个项重用标量函数:

python
def normalise(value: float, min_val: float, max_val: float) -> float:
    """Scale value to the 0-1 range given known min and max."""
    if max_val == min_val:
        return 0.0
    return (value - min_val) / (max_val - min_val)

def normalise_column(values: list[float]) -> list[float]:
    """Normalise an entire column of values."""
    lo, hi = min(values), max(values)
    return [normalise(v, lo, hi) for v in values]

raw = [10.0, 25.0, 5.0, 40.0, 15.0]
print(normalise_column(raw))

这里的类型提示有两个用途:它们记录函数所期望的内容,并让类型检查器捕获那些错误地传递字符串列表的调用者。